# source: https://raw.githubusercontent.com/jaredrsommer/freqtradestrategies/9d66f71ce21f2f65feaad6bb967215be22a3cfd6/PnF%20(copy).py
import logging
from functools import reduce
import datetime
import talib.abstract as ta
import pandas_ta as pta
import logging
import numpy as np
import pandas as pd
import time
import freqtrade.vendor.qtpylib.indicators as qtpylib
from technical import qtpylib
from datetime import timedelta, datetime, timezone
from pandas import DataFrame, Series
from technical import qtpylib
from typing import Optional
from freqtrade.strategy.interface import IStrategy
from technical.pivots_points import pivots_points
from freqtrade.exchange import timeframe_to_prev_date, timeframe_to_minutes
from freqtrade.persistence import Trade
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter, 
                                IStrategy, IntParameter, RealParameter, merge_informative_pair)
from scipy.signal import argrelextrema
from typing import Optional, Union, Tuple
from functools import reduce


logger = logging.getLogger(__name__)

class Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114(IStrategy):
    exit_profit_only = True ### No selling at a loss
    use_custom_stoploss = False
    trailing_stop = False
    position_adjustment_enable = False
    ignore_roi_if_entry_signal = True
    max_entry_position_adjustment = 3
    max_dca_multiplier = 3.5
    process_only_new_candles = True
    can_short = False
    use_exit_signal = True
    startup_candle_count = 200
    stoploss = -0.99
    timeframe = '15m'

    locked_stoploss = {}
    minimal_roi = {}

    plot_config = {}
    # DCA
    position_adjustment_enable = True
    # Example specific variables
    max_entry_position_adjustment = 3
    # This number is explained a bit further down
    # max_dca_multiplier = 3
    max_dca_multiplier = DecimalParameter(low=2.0, high=4.0, default=3, decimals=1 ,space='buy', optimize=True, load=True)
    box_size = DecimalParameter(low=0.5, high=10.0, default=1.5, decimals=1 ,space='buy', optimize=True, load=True)
    reversal = DecimalParameter(low=0.6, high=1.5, default=1.1, decimals=1 ,space='sell', optimize=True, load=True)
    filldelay = IntParameter(10, 120, default = 17 ,space='buy', optimize=True, load=True)
    roll = IntParameter(10, 40, default = 17 ,space='buy', optimize=True, load=True)

    # protections
    cooldown_lookback = IntParameter(12, 48, default=12, space="protection", optimize=True)
    stop_duration = IntParameter(12, 48, default=12, space="protection", optimize=True)
    use_stop_protection = BooleanParameter(default=True, space="protection", optimize=True)

    ### protections ###
    @property
    def protections(self):
        prot = []

        prot.append({
            "method": "CooldownPeriod",
            "stop_duration_candles": self.cooldown_lookback.value
        })
        if self.use_stop_protection.value:
            prot.append({
                "method": "StoplossGuard",
                "lookback_period_candles": 24 * 3,
                "trade_limit": 2,
                "stop_duration_candles": self.stop_duration.value,
                "only_per_pair": False
            })

        return prot


    # This is called when placing the initial order (opening trade)
    def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
                            proposed_stake: float, min_stake: Optional[float], max_stake: float,
                            leverage: float, entry_tag: Optional[str], side: str,
                            **kwargs) -> float:

        # We need to leave most of the funds for possible further DCA orders
        # This also applies to fixed stakes
        return proposed_stake / self.max_dca_multiplier.value

    def adjust_trade_position(self, trade: Trade, current_time: datetime,
                              current_rate: float, current_profit: float,
                              min_stake: Optional[float], max_stake: float,
                              current_entry_rate: float, current_exit_rate: float,
                              current_entry_profit: float, current_exit_profit: float,
                              **kwargs
                              ) -> Union[Optional[float], Tuple[Optional[float], Optional[str]]]:
        """
        Custom trade adjustment logic, returning the stake amount that a trade should be
        increased or decreased.
        This means extra entry or exit orders with additional fees.
        Only called when `position_adjustment_enable` is set to True.

        For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/

        When not implemented by a strategy, returns None

        :param trade: trade object.
        :param current_time: datetime object, containing the current datetime
        :param current_rate: Current entry rate (same as current_entry_profit)
        :param current_profit: Current profit (as ratio), calculated based on current_rate 
                               (same as current_entry_profit).
        :param min_stake: Minimal stake size allowed by exchange (for both entries and exits)
        :param max_stake: Maximum stake allowed (either through balance, or by exchange limits).
        :param current_entry_rate: Current rate using entry pricing.
        :param current_exit_rate: Current rate using exit pricing.
        :param current_entry_profit: Current profit using entry pricing.
        :param current_exit_profit: Current profit using exit pricing.
        :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
        :return float: Stake amount to adjust your trade,
                       Positive values to increase position, Negative values to decrease position.
                       Return None for no action.
                       Optionally, return a tuple with a 2nd element with an order reason
        """

        dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
                # Only buy when not actively falling price.
        last_candle = dataframe.iloc[-1].squeeze()
        filled_entries = trade.select_filled_orders(trade.entry_side)
        last_fill = (current_time - trade.date_last_filled_utc).seconds / 60 
        count_of_entries = trade.nr_of_successful_entries
        count_of_exits = trade.nr_of_successful_exits
        buy_sig = last_candle['enter_long']
        sell_sig = last_candle['exit_long']
        if sell_sig ==1:
            if current_profit > 0.003 and count_of_entries == 1:
                # Take half of the profit at +5%
                return -(trade.stake_amount / count_of_entries)
            if current_profit > 0.003 and count_of_entries == 2 and count_of_exits == 0:
                # Take half of the profit at +5%
                return -(trade.stake_amount / count_of_entries)
            if current_profit > 0.003 and count_of_entries == 2 and count_of_exits == 1:
                # Take half of the profit at +5%
                return -(trade.stake_amount)
            if current_profit > 0.003 and count_of_entries == 3 and count_of_exits == 0:
                # Take half of the profit at +5%
                return -(trade.stake_amount / count_of_entries)
            if current_profit > 0.003 and count_of_entries == 3 and count_of_exits == 1:
                # Take half of the profit at +5%
                return -(trade.stake_amount)


        if buy_sig == 1 and (last_fill > self.filldelay.value):
            try:
                # This returns first order stake size
                stake_amount = filled_entries[0].stake_amount

                return stake_amount, "Grid Order"
            except Exception as exception:
                return None

            return None

    # def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
    #                    rate: float, time_in_force: str, exit_reason: str,
    #                    current_time: datetime, **kwargs) -> bool:
        
    #     dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
    #     last_fill = (current_time - trade.date_last_filled_utc).seconds / 60 
    #     count_of_entries = trade.nr_of_successful_entries
    #     count_of_exits = trade.nr_of_successful_exits
    #     buy_sig = last_candle['enter_long']
    #     sell_sig = last_candle['exit_long']

    #     if exit_reason == 'roi' and trade.calc_profit_ratio(rate) < 0.003:
    #         logger.info(f"{trade.pair} ROI is below 0%")
    #         # self.dp.send_msg(f'{trade.pair} ROI is below 0')
    #         return False

    #     if exit_reason == 'partial_exit' and trade.calc_profit_ratio(rate) < 0.005:
    #         logger.info(f"{trade.pair} partial exit is below 0%")
    #         # self.dp.send_msg(f'{trade.pair} partial exit is below 0')
    #         return False

    #     if exit_reason == 'exit_long' and trade.calc_profit_ratio(rate) < 0.01:
    #         logger.info(f"{trade.pair} preparing trade adjustment")
    #         # self.dp.send_msg(f'{trade.pair} partial exit is below 0')
    #         return False

    #     return True


    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:

        # Apply rolling window operation to the 'OHLC4' column
        rolling_window = dataframe['close'].rolling(self.roll.value) 

        # Calculate the peak-to-peak value on the resulting rolling window data
        ptp_value = rolling_window.apply(lambda x: np.ptp(x))

        # Assign the calculated peak-to-peak value to the DataFrame column
        dataframe['cycle_move'] = ptp_value / dataframe['close']
        dataframe['cycle_move_mean'] = dataframe['cycle_move'].rolling(self.roll.value).mean()  
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=12)
        dataframe["atrpercent"] = (dataframe["atr"] / dataframe['close']) * 100
        box_size_percentage = self.box_size.value
        # box_size_percentage = dataframe['cycle_move_mean'].iloc[-1] * 100
        reversal_size = box_size_percentage * self.reversal.value
        calculate_pnf_with_percentage(dataframe, box_size_percentage, reversal_size)

        return dataframe


    def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:

        df.loc[
            (
                (df["Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Column"] == 1) & 
                (df["Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Column"].shift() != 1) & 
                (df['volume'] > 0)   # Make sure Volume is not 0

            ),
            ['enter_long', 'enter_tag']] = (1, 'downtrend ends')

        return df

    def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:

        df.loc[
            (
                (df["Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Column"] == -1) & 
                (df["Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Column"].shift() != -1) & 
                (df['volume'] > 0)   # Make sure Volume is not 0

            ),
            ['exit_long', 'exit_tag']] = (1, 'uptrend ends')

        return df


def calculate_pnf_with_percentage(dataframe, box_size_percentage, reversal_size):
    # Initialize columns
    dataframe["Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Column"] = ""
    dataframe["Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Direction"] = ""
    dataframe["Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Value"] = None
    
    current_column = None
    current_direction = None
    current_value = None
    
    for index, row in dataframe.iterrows():
        close = row["close"]
        box_size = close * (box_size_percentage / 100)  # Calculate box size as a percentage
        
        # Default values for the current row
        dataframe.loc[index, "Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Column"] = current_column
        dataframe.loc[index, "Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Direction"] = current_direction
        dataframe.loc[index, "Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Value"] = current_value
        
        # First row initialization
        if current_value is None:
            current_value = close
            current_column = 1  # Assume first column is an uptrend (X)
            current_direction = "Up"
            dataframe.loc[index, "Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Column"] = current_column
            dataframe.loc[index, "Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Direction"] = current_direction
            dataframe.loc[index, "Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Value"] = current_value
            continue
        
        # Determine the price change
        price_change = close - current_value
        
        if current_direction == "Up":
            # Continue uptrend or reversal
            if price_change >= box_size:
                current_value += box_size
                dataframe.loc[index, "Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Column"] = 1
                dataframe.loc[index, "Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Direction"] = "Up"
                dataframe.loc[index, "Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Value"] = current_value
            elif price_change <= -reversal_size * box_size:
                current_value -= box_size
                current_column = -1
                current_direction = "Down"
                dataframe.loc[index, "Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Column"] = current_column
                dataframe.loc[index, "Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Direction"] = current_direction
                dataframe.loc[index, "Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Value"] = current_value
        elif current_direction == "Down":
            # Continue downtrend or reversal
            if price_change <= -box_size:
                current_value -= box_size
                dataframe.loc[index, "Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Column"] = -1
                dataframe.loc[index, "Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Direction"] = "Down"
                dataframe.loc[index, "Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Value"] = current_value
            elif price_change >= reversal_size * box_size:
                current_value += box_size
                current_column = 1
                current_direction = "Up"
                dataframe.loc[index, "Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Column"] = current_column
                dataframe.loc[index, "Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Direction"] = current_direction
                dataframe.loc[index, "Github_jaredrsommer_freqtradestrategies__PnF_copy___20260518_195114_Value"] = current_value

    return dataframe
