# source: https://raw.githubusercontent.com/Zer0phucks/trading-lab/90a6eef67476f5a442f148722ac2f56de8ee2fd1/user_data/strategies/FreqaiRangeReversionV1.py
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401
"""
Github_Zer0phucks_trading_lab__FreqaiRangeReversionV1__20260505_075652

Long-only FreqAI strategy for low-volatility range / mean-reversion regimes.
Designed for research, backtesting, hyperopt, and dry-run in a Freqtrade lab.

Suggested model: LightGBMRegressor or XGBoostRegressor.
Suggested timeframe: 5m or 15m.

Hyperopt only entry/exit thresholds, ROI, stoploss, trailing, and protections.
Do not hyperopt feature_engineering_*() or set_freqai_targets().
"""

from __future__ import annotations

from functools import reduce

import numpy as np
import talib.abstract as ta
from pandas import DataFrame

from freqtrade.strategy import DecimalParameter, IStrategy, IntParameter
from freqtrade.vendor.qtpylib import indicators as qtpylib


class Github_Zer0phucks_trading_lab__FreqaiRangeReversionV1__20260505_075652(IStrategy):
    """
    Mean-reversion strategy. It only enters stretched, relatively quiet ranges when
    FreqAI predicts a positive forward average return.
    """

    INTERFACE_VERSION = 3
    timeframe = "5m"
    can_short = False

    process_only_new_candles = True
    startup_candle_count: int = 400

    use_exit_signal = True
    exit_profit_only = False
    ignore_roi_if_entry_signal = False

    minimal_roi = {
        "0": 0.035,
        "60": 0.020,
        "180": 0.010,
        "480": 0.0,
    }

    stoploss = -0.06

    trailing_stop = True
    trailing_stop_positive = 0.010
    trailing_stop_positive_offset = 0.025
    trailing_only_offset_is_reached = True

    # Entry thresholds.
    buy_z = DecimalParameter(0.10, 1.75, decimals=2, default=0.65, space="buy", optimize=True)
    buy_pred_floor = DecimalParameter(0.001, 0.020, decimals=3, default=0.004, space="buy", optimize=True)
    buy_di_max = DecimalParameter(0.50, 3.00, decimals=2, default=1.60, space="buy", optimize=True)
    buy_rsi_max = IntParameter(15, 45, default=32, space="buy", optimize=True)
    buy_adx_max = IntParameter(10, 32, default=24, space="buy", optimize=True)
    buy_bb_percent_max = DecimalParameter(0.00, 0.35, decimals=2, default=0.12, space="buy", optimize=True)
    buy_bb_width_max = DecimalParameter(0.010, 0.200, decimals=3, default=0.080, space="buy", optimize=True)
    buy_atr_pct_max = DecimalParameter(0.002, 0.080, decimals=3, default=0.030, space="buy", optimize=True)

    # Exit thresholds.
    sell_z = DecimalParameter(0.00, 1.25, decimals=2, default=0.25, space="sell", optimize=True)
    sell_pred_floor = DecimalParameter(-0.020, 0.010, decimals=3, default=-0.002, space="sell", optimize=True)
    sell_bb_percent_min = DecimalParameter(0.45, 1.00, decimals=2, default=0.72, space="sell", optimize=True)
    sell_rsi_min = IntParameter(50, 82, default=64, space="sell", optimize=True)

    # Protections.
    protection_cooldown = IntParameter(1, 24, default=4, space="protection", optimize=True)
    protection_stop_duration = IntParameter(12, 96, default=30, space="protection", optimize=True)

    @property
    def protections(self):
        return [
            {
                "method": "CooldownPeriod",
                "stop_duration_candles": self.protection_cooldown.value,
            },
            {
                "method": "StoplossGuard",
                "lookback_period_candles": 96,
                "trade_limit": 2,
                "stop_duration_candles": self.protection_stop_duration.value,
                "only_per_pair": False,
            },
        ]

    def feature_engineering_expand_all(
        self, dataframe: DataFrame, period: int, metadata: dict, **kwargs
    ) -> DataFrame:
        dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
        dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
        dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
        dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
        dataframe["%-cci-period"] = ta.CCI(dataframe, timeperiod=period)
        dataframe["%-atr-period"] = ta.ATR(dataframe, timeperiod=period)

        bollinger = qtpylib.bollinger_bands(
            qtpylib.typical_price(dataframe), window=period, stds=2.0
        )
        bb_range = (bollinger["upper"] - bollinger["lower"]).replace(0, np.nan)
        dataframe["%-bb_width-period"] = bb_range / bollinger["mid"].replace(0, np.nan)
        dataframe["%-bb_percent-period"] = (dataframe["close"] - bollinger["lower"]) / bb_range

        dataframe["%-relative_volume-period"] = dataframe["volume"] / dataframe[
            "volume"
        ].rolling(period).mean().replace(0, np.nan)
        return dataframe

    def feature_engineering_expand_basic(
        self, dataframe: DataFrame, metadata: dict, **kwargs
    ) -> DataFrame:
        dataframe["%-pct_change"] = dataframe["close"].pct_change()
        dataframe["%-raw_volume"] = dataframe["volume"]
        dataframe["%-raw_price"] = dataframe["close"]
        dataframe["%-candle_body_pct"] = (dataframe["close"] - dataframe["open"]) / dataframe[
            "open"
        ].replace(0, np.nan)
        dataframe["%-upper_wick_pct"] = (
            dataframe["high"] - np.maximum(dataframe["open"], dataframe["close"])
        ) / dataframe["close"].replace(0, np.nan)
        dataframe["%-lower_wick_pct"] = (
            np.minimum(dataframe["open"], dataframe["close"]) - dataframe["low"]
        ) / dataframe["close"].replace(0, np.nan)
        return dataframe

    def feature_engineering_standard(
        self, dataframe: DataFrame, metadata: dict, **kwargs
    ) -> DataFrame:
        dataframe["%%-rsi"] = ta.RSI(dataframe, timeperiod=14)
        dataframe["%%-adx"] = ta.ADX(dataframe, timeperiod=14)
        dataframe["%%-ema_50"] = ta.EMA(dataframe, timeperiod=50)
        dataframe["%%-ema_200"] = ta.EMA(dataframe, timeperiod=200)
        dataframe["%%-atr_pct"] = ta.ATR(dataframe, timeperiod=14) / dataframe["close"].replace(
            0, np.nan
        )

        bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2.0)
        bb_range = (bollinger["upper"] - bollinger["lower"]).replace(0, np.nan)
        dataframe["%%-bb_lower"] = bollinger["lower"]
        dataframe["%%-bb_mid"] = bollinger["mid"]
        dataframe["%%-bb_upper"] = bollinger["upper"]
        dataframe["%%-bb_width"] = bb_range / bollinger["mid"].replace(0, np.nan)
        dataframe["%%-bb_percent"] = (dataframe["close"] - bollinger["lower"]) / bb_range

        dataframe["%%-volume_ratio_20"] = dataframe["volume"] / dataframe["volume"].rolling(
            20
        ).mean().replace(0, np.nan)
        dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
        dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 24
        return dataframe

    def set_freqai_targets(
        self, dataframe: DataFrame, metadata: dict, **kwargs
    ) -> DataFrame:
        label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
        dataframe["&-s_close"] = (
            dataframe["close"].shift(-label_period).rolling(label_period).mean()
            / dataframe["close"]
            - 1.0
        )
        return dataframe

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe = self.freqai.start(dataframe, metadata, self)

        if "do_predict" not in dataframe:
            dataframe["do_predict"] = 0
        if "DI_values" not in dataframe:
            dataframe["DI_values"] = 0.0
        if "&-s_close_mean" not in dataframe:
            dataframe["&-s_close_mean"] = 0.0
        if "&-s_close_std" not in dataframe:
            dataframe["&-s_close_std"] = 0.0

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe["enter_long"] = 0
        dataframe["enter_tag"] = ""

        prediction = dataframe["&-s_close"]
        dynamic_target = dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * self.buy_z.value

        conditions = [
            dataframe["do_predict"] == 1,
            dataframe["DI_values"] < self.buy_di_max.value,
            prediction > dynamic_target,
            prediction > self.buy_pred_floor.value,
            dataframe["%%-bb_percent"] < self.buy_bb_percent_max.value,
            dataframe["%%-bb_width"] < self.buy_bb_width_max.value,
            dataframe["%%-atr_pct"] < self.buy_atr_pct_max.value,
            dataframe["%%-rsi"] < self.buy_rsi_max.value,
            dataframe["%%-adx"] < self.buy_adx_max.value,
            dataframe["close"] < dataframe["%%-bb_lower"],
            dataframe["volume"] > 0,
        ]

        dataframe.loc[reduce(lambda x, y: x & y, conditions), ["enter_long", "enter_tag"]] = (
            1,
            "freqai_range_reversion",
        )
        return dataframe

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

        dynamic_exit = dataframe["&-s_close_mean"] - dataframe["&-s_close_std"] * self.sell_z.value
        model_turn = (dataframe["&-s_close"] < dynamic_exit) | (
            dataframe["&-s_close"] < self.sell_pred_floor.value
        )
        mean_reversion_complete = (
            (dataframe["%%-bb_percent"] > self.sell_bb_percent_min.value)
            | (dataframe["close"] > dataframe["%%-bb_mid"])
            | (dataframe["%%-rsi"] > self.sell_rsi_min.value)
        )

        conditions = [
            dataframe["volume"] > 0,
            model_turn | mean_reversion_complete,
        ]

        dataframe.loc[reduce(lambda x, y: x & y, conditions), ["exit_long", "exit_tag"]] = (
            1,
            "freqai_range_exit",
        )
        return dataframe
