# source: https://raw.githubusercontent.com/Danson77/Freqtrade_AIO_UI/f0bf29851d0577f244c17c0aa7d593ba3a7f3551/user_data/backtest_reports/Registerd_with_100USDT/DS_SOS_5m/DS_SOS_5m.py
# directory_url: https://github.com/Danson77/Freqtrade_AIO_UI/blob/main/user_data/backtest_reports/Registerd_with_100USDT/DS_SOS_5m/
# User: Danson77
# Repository: Freqtrade_AIO_UI
# --------------------from freqtrade.strategy.interface import IStrategy
from typing import Dict, List
from pandas import DataFrame
from datetime import datetime, timedelta

import pandas as pd
import numpy as np
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib

from freqtrade.persistence import Trade
from freqtrade.strategy import (
    stoploss_from_open,
    merge_informative_pair,
    DecimalParameter,
    IntParameter,
)

############################################################################
# Custom indicators and helper functions
############################################################################

def EWO(dataframe: DataFrame, ema_length: int = 5, ema2_length: int = 35) -> DataFrame:
    """
    NOTE: This is not the standard EWO (you used SMA and divide by low).
    Kept as-is to preserve your hyperopt space behaviour.
    """
    df = dataframe.copy()
    sma1 = ta.SMA(df, timeperiod=int(ema_length))
    sma2 = ta.SMA(df, timeperiod=int(ema2_length))
    return (sma1 - sma2) / df["low"] * 100


class Github_Danson77_Freqtrade_AIO_UI__DS_SOS_5m__20260520_191933(IStrategy):

    ########################################################################
    # Hyperopt params (kept)
    ########################################################################
    buy_params = {
        "base_nb_candles_buy": 8,
        "ewo_high": 2.403,
        "ewo_high_2": -5.585,
        "ewo_low": -14.378,
        "lookback_candles": 3,
        "low_offset": 0.984,
        "low_offset_2": 0.942,
        "profit_threshold": 1.008,
        "rsi_buy": 72
    }

    sell_params = {
        "base_nb_candles_sell": 16,
        "high_offset": 1.084,
        "high_offset_2": 1.401,
        "pHSL": -0.15,
        "pPF_1": 0.016,
        "pPF_2": 0.024,
        "pSL_1": 0.014,
        "pSL_2": 0.022
    }

    slippage_protection = {
        "retries": 3,
        "max_slippage": -0.02
    }

    ########################################################################
    # Main config
    ########################################################################
    can_short = False

    minimal_roi = {"0": 10}  # effectively disables ROI exits

    ignore_roi_if_entry_signal = False

    stoploss = -0.25
    use_custom_stoploss = False  # backtest mode default

    trailing_stop = True         # backtest mode default
    trailing_stop_positive = 0.001
    trailing_stop_positive_offset = 0.01
    trailing_only_offset_is_reached = True

    use_entry_signal = True
    use_exit_signal = True       # you can turn off if you want trailing-only exits
    use_custom_exit = False

    exit_profit_only = False
    exit_profit_offset = 0.03

    timeframe = "5m"
    informative = "1h"

    process_only_new_candles = False
    startup_candle_count = 200

    order_types = {
        "entry": "market",
        "exit": "market",
        "trailing_stop_loss": "market",
        "emergency_exit": "market",
        "force_entry": "market",
        "force_exit": "market",
        "stoploss": "market",
        "stoploss_on_exchange": False,
        "stoploss_on_exchange_interval": 60,
        "stoploss_on_exchange_limit_ratio": 0.99,
    }
    order_time_in_force = {"entry": "gtc", "exit": "gtc"}

    plot_config = {
        "main_plot": {
            "ma_buy": {"color": "orange"},
            "ma_sell": {"color": "orange"},
        },
    }

    ########################################################################
    # Trade Protections (kept)
    ########################################################################
    @property
    def protections(self):
        return [
            {"method": "CooldownPeriod", "stop_duration_candles": 5},
            {
                "method": "MaxDrawdown",
                "lookback_period_candles": 72,   # 6h on 5m candles
                "trade_limit": 20,
                "stop_duration_candles": 6,
                "max_allowed_drawdown": 0.03,
            },
            {
                "method": "StoplossGuard",
                "lookback_period_candles": 48,   # 4h
                "trade_limit": 4,
                "stop_duration_candles": 4,
                "only_per_pair": False,
            },
            {
                "method": "LowProfitPairs",
                "lookback_period_candles": 24,   # 2h
                "trade_limit": 2,
                "stop_duration_candles": 12,     # 1h
                "required_profit": 0.02,
            },
            {
                "method": "LowProfitPairs",
                "lookback_period_candles": 144,  # 12h
                "trade_limit": 4,
                "stop_duration_candles": 24,     # 2h
                "required_profit": 0.04,
            },
        ]

    ########################################################################
    # Parameters
    ########################################################################
    base_nb_candles_buy = IntParameter(2, 20, default=buy_params["base_nb_candles_buy"], space="buy", optimize=True)
    base_nb_candles_sell = IntParameter(2, 25, default=sell_params["base_nb_candles_sell"], space="sell", optimize=True)

    low_offset = DecimalParameter(0.90, 0.99, default=buy_params["low_offset"], space="buy", optimize=True)
    low_offset_2 = DecimalParameter(0.90, 0.99, default=buy_params["low_offset_2"], space="buy", optimize=True)

    high_offset = DecimalParameter(0.95, 1.10, default=sell_params["high_offset"], space="sell", optimize=True)
    high_offset_2 = DecimalParameter(0.99, 1.50, default=sell_params["high_offset_2"], space="sell", optimize=True)

    fast_ewo = 50
    slow_ewo = 200

    lookback_candles = IntParameter(1, 24, default=buy_params["lookback_candles"], space="buy", optimize=True)
    profit_threshold = DecimalParameter(1.00, 1.03, default=buy_params["profit_threshold"], space="buy", optimize=True)

    ewo_low = DecimalParameter(-20.0, -8.0, default=buy_params["ewo_low"], space="buy", optimize=True)
    ewo_high = DecimalParameter(2.0, 12.0, default=buy_params["ewo_high"], space="buy", optimize=True)
    ewo_high_2 = DecimalParameter(-6.0, 12.0, default=buy_params["ewo_high_2"], space="buy", optimize=True)

    rsi_buy = IntParameter(50, 100, default=buy_params["rsi_buy"], space="buy", optimize=True)

    # Perkmeister-style custom stoploss params (kept)
    pHSL = DecimalParameter(-0.200, -0.040, default=sell_params["pHSL"], decimals=3, space="sell", optimize=True, load=True)
    pPF_1 = DecimalParameter(0.008, 0.020, default=sell_params["pPF_1"], decimals=3, space="sell", optimize=True, load=True)
    pSL_1 = DecimalParameter(0.008, 0.020, default=sell_params["pSL_1"], decimals=3, space="sell", optimize=True, load=True)

    # FIX: your default 0.024 must be inside range
    pPF_2 = DecimalParameter(0.020, 0.100, default=sell_params["pPF_2"], decimals=3, space="sell", optimize=True, load=True)
    pSL_2 = DecimalParameter(0.020, 0.070, default=sell_params["pSL_2"], decimals=3, space="sell", optimize=True, load=True)

    ########################################################################
    # Custom Stoploss (only used if use_custom_stoploss=True)
    ########################################################################
    def custom_stoploss(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs,
    ) -> float:
        HSL = self.pHSL.value
        PF_1 = self.pPF_1.value
        SL_1 = self.pSL_1.value
        PF_2 = self.pPF_2.value
        SL_2 = self.pSL_2.value

        if current_profit > PF_2:
            sl_profit = SL_2 + (current_profit - PF_2)
        elif current_profit > PF_1:
            sl_profit = SL_1 + ((current_profit - PF_1) * (SL_2 - SL_1) / (PF_2 - PF_1))
        else:
            sl_profit = HSL

        return stoploss_from_open(sl_profit, current_profit)

    ########################################################################
    # Informative pairs
    ########################################################################
    def informative_pairs(self):
        pairs = self.dp.current_whitelist()
        return [(pair, self.informative) for pair in pairs]

    def informative_1h_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        assert self.dp, "DataProvider is required for multiple timeframes."

        inf = self.dp.get_pair_dataframe(pair=metadata["pair"], timeframe=self.informative)

        # No 1h data: return empty with correct merge key dtype
        if inf is None or inf.empty:
            empty = DataFrame(columns=["date"])
            empty["date"] = pd.to_datetime(empty["date"], utc=True)
            return empty

        # Force UTC datetime merge key
        inf["date"] = pd.to_datetime(inf["date"], utc=True, errors="coerce")

        inf["ema_50"] = ta.EMA(inf, timeperiod=50)
        inf["ema_200"] = ta.EMA(inf, timeperiod=200)
        inf["rsi"] = ta.RSI(inf, timeperiod=14)

        bb = qtpylib.bollinger_bands(qtpylib.typical_price(inf), window=20, stds=2)
        inf["bb_lowerband"] = bb["lower"]
        inf["bb_middleband"] = bb["mid"]
        inf["bb_upperband"] = bb["upper"]

        return inf

    ########################################################################
    # Normal timeframe indicators
    ########################################################################
    def normal_tf_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        for val in self.base_nb_candles_buy.range:
            dataframe[f"ma_buy_{val}"] = ta.EMA(dataframe, timeperiod=int(val))

        for val in self.base_nb_candles_sell.range:
            dataframe[f"ma_sell_{val}"] = ta.EMA(dataframe, timeperiod=int(val))

        dataframe["hma_50"] = qtpylib.hull_moving_average(dataframe["close"], window=50)
        dataframe["ema_100"] = ta.EMA(dataframe, timeperiod=100)
        dataframe["sma_9"] = ta.SMA(dataframe, timeperiod=9)

        dataframe["EWO"] = EWO(dataframe, self.fast_ewo, self.slow_ewo)

        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
        dataframe["rsi_fast"] = ta.RSI(dataframe, timeperiod=4)
        dataframe["rsi_slow"] = ta.RSI(dataframe, timeperiod=20)

        return dataframe

    ########################################################################
    # Populate indicators (safe merge)
    ########################################################################
    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # Ensure base timeframe date dtype
        dataframe["date"] = pd.to_datetime(dataframe["date"], utc=True, errors="coerce")

        inf = self.informative_1h_indicators(dataframe, metadata)

        if inf is None or inf.empty:
            # placeholders for logic using close_1h rolling
            dataframe["close_1h"] = np.nan
        else:
            dataframe = merge_informative_pair(
                dataframe,
                inf,
                self.timeframe,
                self.informative,
                ffill=True,
            )

        dataframe = self.normal_tf_indicators(dataframe, metadata)
        return dataframe

    ########################################################################
    # Entry signals
    ########################################################################
    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        if "enter_tag" not in dataframe.columns:
            dataframe["enter_tag"] = np.nan
        dataframe["enter_tag"] = dataframe["enter_tag"].astype(object)

        dataframe["enter_long"] = 0

        # If close_1h is NaN, rolling().max() will be NaN and comparisons will be False.
        dont_enter = (
            dataframe["close_1h"].rolling(self.lookback_candles.value).max()
            < (dataframe["close"] * self.profit_threshold.value)
        )

        ewo1 = (
            (dataframe["rsi_fast"] < 35)
            & (dataframe["close"] < dataframe[f"ma_buy_{self.base_nb_candles_buy.value}"] * self.low_offset.value)
            & (dataframe["EWO"] > self.ewo_high.value)
            & (dataframe["rsi"] < self.rsi_buy.value)
            & (dataframe["volume"] > 0)
            & (dataframe["close"] < dataframe[f"ma_sell_{self.base_nb_candles_sell.value}"] * self.high_offset.value)
        )
        dataframe.loc[ewo1, ["enter_long", "enter_tag"]] = [1, "ewo1"]

        ewo2 = (
            (dataframe["rsi_fast"] < 35)
            & (dataframe["close"] < dataframe[f"ma_buy_{self.base_nb_candles_buy.value}"] * self.low_offset_2.value)
            & (dataframe["EWO"] > self.ewo_high_2.value)
            & (dataframe["rsi"] < self.rsi_buy.value)
            & (dataframe["volume"] > 0)
            & (dataframe["close"] < dataframe[f"ma_sell_{self.base_nb_candles_sell.value}"] * self.high_offset.value)
            & (dataframe["rsi"] < 25)
        )
        dataframe.loc[ewo2, ["enter_long", "enter_tag"]] = [1, "ewo2"]

        ewolow = (
            (dataframe["rsi_fast"] < 35)
            & (dataframe["close"] < dataframe[f"ma_buy_{self.base_nb_candles_buy.value}"] * self.low_offset.value)
            & (dataframe["EWO"] < self.ewo_low.value)
            & (dataframe["volume"] > 0)
            & (dataframe["close"] < dataframe[f"ma_sell_{self.base_nb_candles_sell.value}"] * self.high_offset.value)
        )
        dataframe.loc[ewolow, ["enter_long", "enter_tag"]] = [1, "ewolow"]

        dataframe.loc[dont_enter, ["enter_long", "enter_tag"]] = [0, np.nan]
        return dataframe

    ########################################################################
    # Exit signals
    ########################################################################
    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe["exit_long"] = 0
        dataframe["exit_tag"] = None

        cond_sma_rsi = (
            (dataframe["close"] > dataframe["sma_9"])
            & (dataframe["close"] > (dataframe[f"ma_sell_{self.base_nb_candles_sell.value}"] * self.high_offset_2.value))
            & (dataframe["rsi"] > 50)
            & (dataframe["volume"] > 0)
            & (dataframe["rsi_fast"] > dataframe["rsi_slow"])
        )

        cond_hma_pullback = (
            (dataframe["close"] < dataframe["hma_50"])
            & (dataframe["close"] > (dataframe[f"ma_sell_{self.base_nb_candles_sell.value}"] * self.high_offset.value))
            & (dataframe["volume"] > 0)
            & (dataframe["rsi_fast"] > dataframe["rsi_slow"])
        )

        dataframe.loc[cond_sma_rsi, ["exit_long", "exit_tag"]] = [1, "sma_rsi_exit"]
        dataframe.loc[cond_hma_pullback, ["exit_long", "exit_tag"]] = [1, "hma_pullback_exit"]

        return dataframe

    ########################################################################
    # Confirm exit (kept)
    ########################################################################
    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_candle = dataframe.iloc[-1]

        if last_candle is not None and exit_reason in ["sell_signal"]:
            if (last_candle["hma_50"] * 1.149 > last_candle["ema_100"]) and (last_candle["close"] < last_candle["ema_100"] * 0.951):
                return False

        # slippage protection
        try:
            state = self.slippage_protection["__pair_retries"]
        except KeyError:
            state = self.slippage_protection["__pair_retries"] = {}

        candle = dataframe.iloc[-1].squeeze()
        slippage = (rate / candle["close"]) - 1

        if slippage < self.slippage_protection["max_slippage"]:
            pair_retries = state.get(pair, 0)
            if pair_retries < self.slippage_protection["retries"]:
                state[pair] = pair_retries + 1
                return False

        state[pair] = 0
        return True
