# source: https://raw.githubusercontent.com/xyc-stu/tradebot/59d6a269a2aa032abca98b77009a43443c3e043f/user_data/strategies/DynamicFreqAIBotStrategy.py
from datetime import datetime
from functools import reduce

from pandas import DataFrame
from pandas import notna
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
import logging

from freqtrade.persistence import Trade
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy

logger = logging.getLogger(__name__)


class Github_xyc_stu_tradebot__DynamicFreqAIBotStrategy__20260516_060442(IStrategy):
    """
    A conservative FreqAI trend strategy.

    The model predicts forward average return, while hand-written filters keep
    entries aligned with trend, momentum, liquidity, and volatility regimes.
    """

    INTERFACE_VERSION = 3

    can_short = False
    timeframe = "15m"
    startup_candle_count = 220
    process_only_new_candles = True

    minimal_roi = {}
    stoploss = -0.99
    trailing_stop = False
    use_custom_stoploss = True

    use_exit_signal = True
    exit_profit_only = False
    ignore_roi_if_entry_signal = False
    use_custom_roi = True

    buy_zscore_multiplier = DecimalParameter(0.80, 1.80, default=1.10, decimals=2, space="buy", optimize=True)
    sell_zscore_multiplier = DecimalParameter(0.80, 1.80, default=1.10, decimals=2, space="sell", optimize=True)
    min_expected_return = DecimalParameter(0.002, 0.020, default=0.006, decimals=3, space="buy", optimize=True)
    max_negative_return = DecimalParameter(-0.030, -0.002, default=-0.006, decimals=3, space="sell", optimize=True)
    buy_rsi_min = IntParameter(45, 60, default=52, space="buy", optimize=True)
    buy_rsi_max = IntParameter(58, 75, default=68, space="buy", optimize=True)
    buy_adx_min = IntParameter(15, 30, default=20, space="buy", optimize=True)
    sell_rsi_min = IntParameter(35, 55, default=46, space="sell", optimize=True)
    max_atr_pct = DecimalParameter(0.020, 0.080, default=0.055, decimals=3, space="buy", optimize=True)

    def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
                                       metadata: dict, **kwargs) -> DataFrame:
        """
        Create the features that FreqAI will use to "learn" from the market.
        FreqAI will automatically generate these features for all timeframes
        defined in config.freqai.json (e.g. 15m and 1h).
        """
        dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
        dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
        dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
        
        # Bollinger Bands
        bollinger = qtpylib.bollinger_bands(dataframe["close"], window=period, stds=2)
        dataframe["%-bb_lowerband-period"] = bollinger["lower"]
        dataframe["%-bb_upperband-period"] = bollinger["upper"]
        dataframe["%-bb_width-period"] = (bollinger["upper"] - bollinger["lower"]) / dataframe["close"]
        dataframe["%-close-bb_lower-distance-period"] = (
            dataframe["close"] / bollinger["lower"] - 1
        )
        dataframe["%-close-bb_upper-distance-period"] = (
            dataframe["close"] / bollinger["upper"] - 1
        )
        
        # MACD
        macd = ta.MACD(dataframe)
        dataframe["%-macd-period"] = macd["macd"]
        dataframe["%-macdsignal-period"] = macd["macdsignal"]
        dataframe["%-macdhist-period"] = macd["macdhist"]
        dataframe["%-ema-distance-period"] = (
            dataframe["close"] / ta.EMA(dataframe, timeperiod=period) - 1
        )

        return dataframe

    def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
        """
        Features that are independent of period.
        """
        dataframe["%-pct-change"] = dataframe["close"].pct_change()
        dataframe["%-pct-change-3"] = dataframe["close"].pct_change(3)
        dataframe["%-pct-change-12"] = dataframe["close"].pct_change(12)
        dataframe["%-raw_volume"] = dataframe["volume"]
        dataframe["%-raw_price"] = dataframe["close"]
        dataframe["%-volume-ratio-24"] = dataframe["volume"] / dataframe["volume"].rolling(24).mean()
        dataframe["%-candle-body"] = (dataframe["close"] - dataframe["open"]) / dataframe["open"]
        dataframe["%-high-low-range"] = (dataframe["high"] - dataframe["low"]) / dataframe["close"]
        return dataframe

    def feature_engineering_standard(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
        """
        Standard features (e.g. day of week, hour of day).
        """
        dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
        dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
        return dataframe

    def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
        """
        The "Target" (what FreqAI tries to predict).
        We tell the AI to predict whether price goes UP or DOWN in the next N candles.
        """
        # Read label_period_candles from config (works in both live and backtest mode)
        label_period_candles = self.freqai_info["feature_parameters"]["label_period_candles"]

        dataframe["&-s_close"] = (
            dataframe["close"]
            .shift(-label_period_candles)
            .rolling(label_period_candles)
            .mean()
            / dataframe["close"]
            - 1
        )

        return dataframe

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # Ask FreqAI to populate all the features and predictions
        dataframe = self.freqai.start(dataframe, metadata, self)
        dataframe["ema12"] = ta.EMA(dataframe, timeperiod=12)
        dataframe["ema36"] = ta.EMA(dataframe, timeperiod=36)
        dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
        dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
        dataframe["volume_mean_24"] = dataframe["volume"].rolling(24).mean()
        dataframe["atr_pct"] = dataframe["atr"] / dataframe["close"]
        dataframe["target_roi"] = dataframe["&-s_close"].rolling(200, min_periods=50).quantile(0.65)
        dataframe["sell_roi"] = dataframe["&-s_close"].rolling(200, min_periods=50).quantile(0.35)

        if "&-s_close_mean" in dataframe and "&-s_close_std" in dataframe:
            dataframe["target_roi"] = (
                dataframe["&-s_close_mean"] +
                dataframe["&-s_close_std"] * self.buy_zscore_multiplier.value
            )
            dataframe["sell_roi"] = (
                dataframe["&-s_close_mean"] -
                dataframe["&-s_close_std"] * self.sell_zscore_multiplier.value
            )

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        ai_signal = (
            (dataframe["&-s_close"] > dataframe["target_roi"]) &
            (dataframe["&-s_close"] > self.min_expected_return.value)
        )
        trend_filter = (
            (dataframe["ema12"] > dataframe["ema36"]) &
            (dataframe["ema36"] > dataframe["ema200"]) &
            (dataframe["close"] > dataframe["ema12"])
        )
        momentum_filter = (
            (dataframe["rsi"] >= self.buy_rsi_min.value) &
            (dataframe["rsi"] <= self.buy_rsi_max.value) &
            (dataframe["adx"] >= self.buy_adx_min.value)
        )
        participation_filter = (
            (dataframe["volume"] > 0) &
            (dataframe["volume"] >= dataframe["volume_mean_24"])
        )
        volatility_filter = (
            dataframe["atr_pct"].notna() &
            (dataframe["atr_pct"] <= self.max_atr_pct.value)
        )

        enter_long_conditions = [
            dataframe["do_predict"] == 1,
            ai_signal,
            trend_filter,
            momentum_filter,
            participation_filter,
            volatility_filter,
        ]

        if enter_long_conditions:
            dataframe.loc[
                reduce(lambda x, y: x & y, enter_long_conditions), ["enter_long", "enter_tag"]
            ] = (1, "freqai_trend_buy")

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        ai_exit_signal = (
            (dataframe["&-s_close"] < dataframe["sell_roi"]) |
            (dataframe["&-s_close"] < self.max_negative_return.value)
        )
        trend_break = (
            qtpylib.crossed_below(dataframe["close"], dataframe["ema12"]) |
            (dataframe["ema12"] < dataframe["ema36"])
        )
        momentum_fade = dataframe["rsi"] < self.sell_rsi_min.value

        exit_long_conditions = [
            dataframe["do_predict"] == 1,
            ai_exit_signal | trend_break | momentum_fade,
        ]

        if exit_long_conditions:
            dataframe.loc[
                reduce(lambda x, y: x & y, exit_long_conditions), ["exit_long", "exit_tag"]
            ] = (1, "freqai_trend_sell")

        return dataframe

    def custom_roi(self, pair: str, trade: Trade, current_time: datetime, trade_duration: int,
                   entry_tag: str | None, side: str, **kwargs) -> float:
        """
        Lower profit targets over time so capital is not trapped waiting for
        the model to recover a stale entry.
        """
        if trade_duration > 240:
            return 0.01
        if trade_duration > 120:
            return 0.02
        if trade_duration > 60:
            return 0.03
        return 0.05

    def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
                        current_rate: float, current_profit: float, **kwargs) -> float:
        """
        Use ATR for the initial stoploss and tighten aggressively once price
        moves in our favor.
        """
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if dataframe is None or dataframe.empty:
            return -0.05

        last_candle = dataframe.iloc[-1].squeeze()

        if current_profit > 0.04:
            return -0.015
        if current_profit > 0.02:
            return -0.01

        atr_val = last_candle.get("atr")
        if atr_val is not None and notna(atr_val) and current_rate > 0:
            stop_distance = (atr_val * 2.5) / current_rate
            return max(min(-stop_distance, -0.02), -0.08)

        return -0.05
