# source: https://raw.githubusercontent.com/NorthStartLabas/crypto-llm-trader/2baa23b931375de2f0caeb5e2557ec8f8cf1c4bb/user_data/strategies/BBRsiMeanReversionStrategy.py
"""
Github_NorthStartLabas_crypto_llm_trader__BBRsiMeanReversionStrategy__20260520_011426 — Phase 1.4

Replaces momentum-following (RSI crossover) with mean reversion:
  buy when price is genuinely oversold below the lower Bollinger Band,
  sell when price recovers to the middle Band (mean) or RSI normalises.

Why this works better in bear markets:
  - Bear markets have frequent oversold bounces even during downtrends.
  - Entries are at beaten-down prices (better risk/reward than chasing momentum).
  - Exit target is the mean (realistic 2-4%), not a wide ROI table.

Hyperopt spaces: buy, sell, roi, stoploss.
In-sample: 20250524– (full history)
Out-of-sample: TIMERANGE=20260101- bash scripts/backtest.sh

Not financial advice. Do not use with real funds without independent review.
"""

from typing import Dict, List

import talib.abstract as ta
from pandas import DataFrame

from freqtrade.optimize.space import SKDecimal
from freqtrade.strategy import IntParameter, IStrategy


class Github_NorthStartLabas_crypto_llm_trader__BBRsiMeanReversionStrategy__20260520_011426(IStrategy):
    INTERFACE_VERSION = 3

    can_short = False
    timeframe = "1h"
    startup_candle_count = 200

    # ------------------------------------------------------------------ #
    # Exit targets — tighter than momentum strategy; mean reversion       #
    # captures small bounces quickly rather than riding trends.           #
    # ------------------------------------------------------------------ #
    minimal_roi = {
        "0": 0.03,
        "60": 0.02,
        "120": 0.01,
    }

    stoploss = -0.04
    trailing_stop = False
    process_only_new_candles = True

    # ------------------------------------------------------------------ #
    # Hyperopt parameters                                                  #
    # ------------------------------------------------------------------ #
    buy_rsi_oversold = IntParameter(15, 40, default=30, space="buy", optimize=True)
    sell_rsi_recover = IntParameter(50, 75, default=60, space="sell", optimize=True)

    # ------------------------------------------------------------------ #
    # Constrained Hyperopt spaces                                         #
    # ------------------------------------------------------------------ #
    @staticmethod
    def stoploss_space() -> List:
        # Cap at -8 % — mean reversion entries are at better prices so a
        # wider stop is rarely justified.
        return [SKDecimal(-0.08, -0.02, decimals=3, name="stoploss")]

    @staticmethod
    def generate_roi_table(params: Dict) -> Dict[int, float]:
        roi_table: Dict[int, float] = {}
        roi_table[0] = params["roi_p1"] + params["roi_p2"] + params["roi_p3"]
        roi_table[params["roi_t3"]] = params["roi_p1"] + params["roi_p2"]
        roi_table[params["roi_t3"] + params["roi_t2"]] = params["roi_p1"]
        # Floor at 1 % — never hold to breakeven
        roi_table[params["roi_t3"] + params["roi_t2"] + params["roi_t1"]] = 0.01
        return roi_table

    # ------------------------------------------------------------------ #
    # Indicators                                                           #
    # ------------------------------------------------------------------ #
    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        bb = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
        dataframe["bb_upper"] = bb["upperband"]
        dataframe["bb_middle"] = bb["middleband"]
        dataframe["bb_lower"] = bb["lowerband"]
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
        dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
        return dataframe

    # ------------------------------------------------------------------ #
    # Entry — buy genuine oversold dips, not momentum breakouts           #
    # ------------------------------------------------------------------ #
    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[
            (
                # Price has pierced the lower Bollinger Band
                (dataframe["close"] < dataframe["bb_lower"])
                # RSI confirms oversold — not just noisy band touch
                & (dataframe["rsi"] < self.buy_rsi_oversold.value)
                # Not in a catastrophic downtrend (within 15 % of EMA200)
                & (dataframe["close"] > dataframe["ema200"] * 0.85)
                & (dataframe["volume"] > 0)
            ),
            ["enter_long", "enter_tag"],
        ] = [1, "bb_rsi_oversold"]

        return dataframe

    # ------------------------------------------------------------------ #
    # Exit — take profit at the mean, not via trend signals               #
    # ------------------------------------------------------------------ #
    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # Exit 1: mean reversion complete — price back at middle BB
        dataframe.loc[
            (
                (dataframe["close"] >= dataframe["bb_middle"])
                & (dataframe["volume"] > 0)
            ),
            ["exit_long", "exit_tag"],
        ] = [1, "bb_middle_reached"]

        # Exit 2: RSI has normalised before price reached the middle band
        dataframe.loc[
            (
                (dataframe["rsi"] > self.sell_rsi_recover.value)
                & (dataframe["close"] < dataframe["bb_middle"])
                & (dataframe["volume"] > 0)
            ),
            ["exit_long", "exit_tag"],
        ] = [1, "rsi_recovered"]

        return dataframe
