# source: https://raw.githubusercontent.com/Aymane-lahlou/Trading_Binance/1ba524d6e3a6d37c24bef88d005a9372102e0a7d/user_data/strategies/HalalSpotStrategy.py
from pandas import DataFrame
import talib.abstract as ta
from datetime import datetime

from freqtrade.persistence import Trade
from freqtrade.exchange import timeframe_to_prev_date
from freqtrade.strategy import IStrategy, merge_informative_pair, stoploss_from_absolute
from technical import qtpylib


class Github_Aymane_lahlou_Trading_Binance__HalalSpotStrategy__20260710_203416(IStrategy):
    INTERFACE_VERSION = 3

    timeframe = "15m"
    informative_timeframe = "4h"

    # Spot long positions only.
    can_short = False

    # ROI exits disabled. Profitable exits are handled by the ATR stop
    # and indicator exits.
    minimal_roi = {}

    # Hard fallback. The custom ATR stop normally keeps risk tighter.
    stoploss = -0.06

    # Use an ATR-based stop which adapts to each pair's volatility.
    use_custom_stoploss = True

    # Allow indicator exits to protect against trend reversals,
    # including on losing trades.
    use_exit_signal = True
    exit_profit_only = False

    process_only_new_candles = True
    # EMA indicators need more history than their nominal period to converge.
    # Verify with freqtrade recursive-analysis after backtesting.
    startup_candle_count = 800

    # Risk and signal constants are kept together so variants are easy to test.
    initial_atr_multiplier = 2.5
    trailing_atr_multiplier = 2.0
    max_initial_loss = 0.06
    breakeven_profit_buffer = 0.0025

    @property
    def protections(self):
        return [
            {
                "method": "CooldownPeriod",
                "stop_duration_candles": 4,
            },
            {
                "method": "StoplossGuard",
                "lookback_period_candles": 24,
                "trade_limit": 2,
                "stop_duration_candles": 12,
                "only_per_pair": True,
            },
            {
                "method": "MaxDrawdown",
                "lookback_period_candles": 48,
                "trade_limit": 4,
                "stop_duration_candles": 12,
                "max_allowed_drawdown": 0.10,
            },
        ]

    def informative_pairs(self):
        pairs = self.dp.current_whitelist()

        return [
            (pair, self.informative_timeframe)
            for pair in pairs
        ]

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

        # Main timeframe indicators.
        dataframe["rsi"] = ta.RSI(
            dataframe,
            timeperiod=14,
        )

        dataframe["sma_fast"] = ta.SMA(
            dataframe,
            timeperiod=20,
        )

        dataframe["sma_slow"] = ta.SMA(
            dataframe,
            timeperiod=50,
        )

        dataframe["ema_trend"] = ta.EMA(
            dataframe,
            timeperiod=200,
        )

        dataframe["atr"] = ta.ATR(
            dataframe,
            timeperiod=14,
        )

        dataframe["atr_ratio"] = (
            dataframe["atr"] / dataframe["close"]
        )

        dataframe["ema_trend_slope"] = (
            dataframe["ema_trend"] -
            dataframe["ema_trend"].shift(5)
        )

        dataframe["volume_mean"] = (
            dataframe["volume"]
            .rolling(20)
            .mean()
        )

        dataframe["volume_ratio"] = (
            dataframe["volume"] /
            dataframe["volume_mean"]
        )

        dataframe["rsi_min_5"] = (
            dataframe["rsi"]
            .rolling(5)
            .min()
        )

        # Four-hour trend indicators.
        informative = self.dp.get_pair_dataframe(
            pair=metadata["pair"],
            timeframe=self.informative_timeframe,
        )

        informative["sma_trend"] = ta.SMA(
            informative,
            timeperiod=50,
        )

        dataframe = merge_informative_pair(
            dataframe,
            informative,
            self.timeframe,
            self.informative_timeframe,
            ffill=True,
        )

        return dataframe

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

        htf_close = f"close_{self.informative_timeframe}"
        htf_trend = f"sma_trend_{self.informative_timeframe}"

        market_is_tradable = (
            # Four-hour trend confirmation.
            (dataframe[htf_close] > dataframe[htf_trend])

            # Short-term trend is not bearish.
            & (dataframe["sma_fast"] > dataframe["sma_slow"])

            # Long-term trend is flat or rising.
            & (dataframe["ema_trend_slope"] >= 0)

            # Avoid markets where fees/noise dominate or volatility is extreme.
            & (dataframe["atr_ratio"] > 0.0025)
            & (dataframe["atr_ratio"] < 0.025)

            & (dataframe["volume"] > 0)
        )

        pullback_recovery = (
            market_is_tradable

            # RSI was recently oversold and now produces a one-candle recovery
            # trigger. This avoids a signal remaining active for several candles.
            & (dataframe["rsi_min_5"] <= 35)
            & qtpylib.crossed_above(dataframe["rsi"], 35)
            & (dataframe["rsi"] < 48)

            # Price is recovering rather than continuing to fall.
            & (dataframe["close"] > dataframe["close"].shift(1))
            & (dataframe["volume_ratio"] > 0.8)
        )

        trend_continuation = (
            market_is_tradable

            # Join an existing uptrend on a one-candle momentum trigger.
            & (dataframe["close"] > dataframe["ema_trend"])
            & (dataframe["close"] > dataframe["sma_fast"])
            & qtpylib.crossed_above(dataframe["rsi"], 50)
            & (dataframe["rsi"] <= 62)
            & (dataframe["close"] > dataframe["open"])
            & (dataframe["volume_ratio"] > 1.05)
        )

        dataframe.loc[
            pullback_recovery,
            "enter_long",
        ] = 1
        dataframe.loc[pullback_recovery, "enter_tag"] = "pullback_recovery"

        continuation_only = trend_continuation & ~pullback_recovery
        dataframe.loc[
            continuation_only,
            "enter_long",
        ] = 1
        dataframe.loc[continuation_only, "enter_tag"] = "trend_continuation"

        return dataframe

    def custom_stoploss(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        after_fill: bool,
        **kwargs,
    ) -> float | None:
        dataframe, _ = self.dp.get_analyzed_dataframe(
            pair=pair,
            timeframe=self.timeframe,
        )

        if dataframe.empty:
            return None

        last_candle = dataframe.iloc[-1].squeeze()
        current_atr = last_candle.get("atr")

        if current_atr is None or current_atr <= 0:
            return None

        # Anchor initial risk to volatility at entry instead of allowing the
        # current ATR to silently redefine the original trade risk.
        trade_candle_date = timeframe_to_prev_date(
            self.timeframe,
            trade.open_date_utc,
        )
        entry_candle = dataframe.loc[dataframe["date"] == trade_candle_date]
        entry_atr = (
            entry_candle["atr"].iloc[-1]
            if not entry_candle.empty
            else current_atr
        )

        initial_stop_price = (
            trade.open_rate - (self.initial_atr_multiplier * entry_atr)
        )
        max_loss_price = trade.open_rate * (1 - self.max_initial_loss)
        stop_price = max(initial_stop_price, max_loss_price)
        initial_risk = trade.open_rate - stop_price
        one_r_profit = initial_risk / trade.open_rate

        # At 1R, protect the entry price plus a small fee/slippage buffer.
        if current_profit >= one_r_profit:
            breakeven_price = (
                trade.open_rate * (1 + self.breakeven_profit_buffer)
            )
            stop_price = max(stop_price, breakeven_price)

        # At 2R, trail using current volatility. Freqtrade will only ratchet the
        # stop upward outside an after-fill call.
        if current_profit >= (2 * one_r_profit):
            trailing_stop_price = (
                current_rate - (self.trailing_atr_multiplier * current_atr)
            )
            stop_price = max(stop_price, trailing_stop_price)

        if stop_price >= current_rate:
            return None

        return stoploss_from_absolute(
            stop_price,
            current_rate=current_rate,
            is_short=trade.is_short,
            leverage=trade.leverage,
        )

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

        htf_close = f"close_{self.informative_timeframe}"
        htf_trend = f"sma_trend_{self.informative_timeframe}"

        local_trend_breakdown = (
            # Two closes below EMA200 confirm that this is not a single wick.
            (dataframe["close"] < dataframe["ema_trend"])
            & (
                dataframe["close"].shift(1)
                < dataframe["ema_trend"].shift(1)
            )
            # The moving-average structure has also weakened.
            & (dataframe["sma_fast"] < dataframe["sma_slow"])
            # Do not require exceptional volume during a slow deterioration.
            & (dataframe["volume_ratio"] > 0.8)
            & (dataframe["volume"] > 0)
        )

        higher_timeframe_breakdown = (
            (dataframe[htf_close] < dataframe[htf_trend])
            & (dataframe["close"] < dataframe["sma_slow"])
            & (dataframe["rsi"] < 45)
            & (dataframe["volume"] > 0)
        )

        dataframe.loc[
            local_trend_breakdown,
            ["exit_long", "exit_tag"],
        ] = (1, "local_trend_breakdown")

        dataframe.loc[
            higher_timeframe_breakdown & ~local_trend_breakdown,
            ["exit_long", "exit_tag"],
        ] = (1, "higher_timeframe_breakdown")

        return dataframe
