# source: https://raw.githubusercontent.com/TraderAlice/Auto-Quant/34ba6b6ee6aa69813a50a72158d4c089d97afb96/versions/0.4.1/strategies/CrashRebound.py
"""
Github_TraderAlice_Auto_Quant__CrashRebound__20260508_103218 — buy alts after a -25% drawdown from rolling 30d high

Paradigm: other (counter-trend / drawdown-rebound)
Hypothesis: AltsBollBreak's r0-r4 trajectory established that breakout
            paradigms on alts have structural winter fragility — every
            defense (1d position filter, stoploss, slope filter) cut bull
            profit while only partially silencing winter. The opposite
            structural exposure is interesting: a counter-trend strategy
            that ENTERS on drawdowns. Crashes happen in every regime
            (bull pullbacks, winter capitulations, recovery shakeouts);
            the rebound size varies but the directional edge is generally
            positive on liquid majors. Trigger: 1h close < 30d-rolling-max
            × 0.75 (i.e., 25% off 30-day peak) AND 1h RSI(14) < 35
            (oversold confirmation — don't catch knives mid-fall, wait
            for a stretch). Exit: 1h close > 1h SMA50 (mean-reversion
            target, halfway-back-to-prior-trend). Universe: alts only
            (SOL, AVAX, BNB) — alts have larger drawdowns than majors,
            and the v0.4.0 surfacing was that BTC/ETH have less
            mean-reverting structure. test_timeranges spans bull/winter/
            recovery/full to make the cross-regime claim falsifiable.
            Equal-weight sizing.
Parent: root (replaces AltsBollBreak which was killed at r5 after 5
        rounds of failed winter defenses)
Created: pending — fill in after first commit
Status: active
Uses MTF: no (1h-only on entry/exit; 30d max needs 720 bars warmup)
"""

from pandas import DataFrame
import talib.abstract as ta

from freqtrade.strategy import IStrategy, informative


class Github_TraderAlice_Auto_Quant__CrashRebound__20260508_103218(IStrategy):
    INTERFACE_VERSION = 3

    timeframe = "1h"
    can_short = False

    minimal_roi = {"0": 100}
    stoploss = -0.99
    trailing_stop = False
    process_only_new_candles = True
    use_exit_signal = True
    exit_profit_only = False
    ignore_roi_if_entry_signal = False

    # 30d at 1h = 720 bars warmup
    startup_candle_count: int = 760

    # r19: revert r18 expansion. r18 finding: full-5pair lifted full_5y
    # Sharpe 0.29→0.40 and bull 0.85→1.07 / recovery 0.19→0.33, BUT
    # winter regressed 0.085→0.013 (BTC/ETH winter drawdown-bounces are
    # weaker than alts'). Net robust 0.085→0.013 — a clean Pareto move
    # where best-case lifted but worst-case dropped. Drawdown-rebound
    # paradigm DOES generalize to majors in directional regimes but
    # NOT under v0.4.1 robust-sharpe honesty bar. Reverting to 3-pair
    # alts+BNB basket which holds robust 0.085.
    pair_basket = ["SOL/USDT", "AVAX/USDT", "BNB/USDT"]

    test_timeranges = [
        ("bull_2021",      "20210101-20211231"),
        ("winter_2022",    "20220101-20221231"),
        ("recovery_23_25", "20230101-20251231"),
        ("full_5y",        "20210101-20251231"),
    ]

    @informative("1d")
    def populate_indicators_1d(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
        dataframe["ema200_slope_up"] = (
            dataframe["ema200"] > dataframe["ema200"].shift(7)
        ).astype(int)
        return dataframe

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # 30-day rolling high (720 1h bars). Drawdown trigger uses prior bar
        # to avoid current-bar self-reference.
        dataframe["high_30d"] = dataframe["high"].rolling(720).max().shift(1)
        dataframe["drawdown_pct"] = (
            dataframe["close"] / dataframe["high_30d"] - 1.0
        )
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
        # r15: revert r14 SMA100 → SMA50. Patient exit BACKFIRED on this
        # paradigm — drawdown-rebounds revert quickly; letting the bounce
        # ride past SMA50 to SMA100 turns winners into losers. v0.4.0 r13
        # patient-exit finding is paradigm-specific to BREAKOUTS, not
        # counter-trend MR. Cross-version finding.
        dataframe["sma50"] = ta.SMA(dataframe, timeperiod=50)
        dataframe["volume_sma20"] = dataframe["volume"].rolling(20).mean()
        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # r24: revert r23 multi-bar DD confirmation. r23 finding: 3-bar
        # sustained-DD drops the inflection-point entries (bounces start
        # WITHIN the 3-bar window) — robust 0.085→0.0095. Single-bar
        # DD<-20% trigger IS the local optimum; capitulation signals
        # don't need post-confirmation when paired with RSI<35 already.
        dataframe.loc[
            (dataframe["drawdown_pct"] < -0.20)
            & (dataframe["rsi"] < 35)
            & (dataframe["ema200_slope_up_1d"] == 1)
            & (dataframe["volume"] > 1.3 * dataframe["volume_sma20"]),
            "enter_long",
        ] = 1
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # r15: revert r14 SMA100 → SMA50.
        dataframe.loc[dataframe["close"] > dataframe["sma50"], "exit_long"] = 1
        return dataframe

    def custom_stake_amount(
        self,
        pair: str,
        current_time,
        current_rate: float,
        proposed_stake: float,
        min_stake,
        max_stake: float,
        leverage: float,
        entry_tag: str,
        side: str,
        **kwargs,
    ) -> float:
        # r29: revert r28 regime composition. r28 finding: composing
        # regime×DD-conviction INVERSE-amplifies because regime and DD-
        # signal are NEGATIVELY correlated (bull = small DDs, winter =
        # big DDs). Bull entries got down-sized (0.5×1.5=0.75) just
        # when DD-signal is weakest; winter entries cancel out (2.0×
        # 0.5=1.0) erasing the conviction lift. Robust crashed
        # 0.085→-0.083. Cross-paradigm composition rule: only works
        # when regime ⊥ signal-strength (BNB-RSI case) or positively
        # correlated. Reverting to pure DD-conviction (r16 optimum).
        df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if df.empty or "drawdown_pct" not in df.columns:
            return proposed_stake
        dd = df["drawdown_pct"].iloc[-1]
        if dd != dd or dd >= 0:
            return proposed_stake
        scale = abs(float(dd)) / 0.20
        scale = max(0.5, min(2.0, scale))
        stake = proposed_stake * scale
        return max(min_stake or 0.0, min(max_stake, stake))
