# source: https://raw.githubusercontent.com/Aatif-qmr/cipher/f3735aaf3655923bb1c8ee70c054c6a1a0d8831d/strategies/active/MeanReversionV1.py
# directory_url: https://github.com/Aatif-qmr/cipher/blob/main/strategies/active/
# User: Aatif-qmr
# Repository: cipher
# --------------------import logging
import sys
from datetime import datetime, timedelta
from pathlib import Path

import pandas_ta as ta
from pandas import DataFrame

_BASE = Path(__file__).resolve().parent.parent.parent
if str(_BASE) not in sys.path:
    sys.path.insert(0, str(_BASE))

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

from indicators.macro_merge import merge_macro_data
from qnt.oracle.hmm_regime import detect_regime, get_regime_for_strategy
from risk.correlation_guard import is_blocked as corr_blocked
from risk.risk_manager import run_all_checks
from risk.stake_sizer import get_stake_multiplier
from risk.volatility_breaker import is_vol_elevated
from sentiment.reader import get_current_sentiment, get_funding_rate, get_sentiment_signal

logger = logging.getLogger(__name__)


class Github_Aatif_qmr_cipher__MeanReversionV1__20260901_081406(IStrategy):
    """
    1h mean-reversion on crypto majors.

    Entry: RSI oversold + close below lower BB + RSI rising + volume above median.
    Exit:  RSI overbought (2-candle persistence) OR time-stop after 24h.
    Stop:  ATR-based dynamic stop (2×ATR, bounded [-2%, -6%]).

    All other strategies are ARCHIVED pending this strategy proving positive edge
    in paper trading for 3+ months with 30+ trades. See ARCHITECTURE_PRINCIPLES.md.
    """

    INTERFACE_VERSION = 3
    timeframe = "1h"

    # Hyperoptable Parameters — current values from walk_forward_3window (2026-05-23)
    buy_rsi = IntParameter(20, 40, default=25, space="buy")
    bb_period = IntParameter(10, 30, default=15, space="buy")
    bb_std = DecimalParameter(1.0, 2.5, default=1.5, space="buy")
    sell_rsi = IntParameter(55, 80, default=60, space="sell")

    minimal_roi = {"0": 0.426, "226": 0.13, "650": 0.082, "1867": 0}
    stoploss = -0.04
    stoploss_on_exchange = True
    stoploss_on_exchange_interval = 60

    startup_candle_count: int = 200

    def informative_pairs(self) -> list[tuple[str, str]]:
        return [(pair, "4h") for pair in self.dp.current_whitelist()]

    def load_dynamic_params(self):
        self.buy_rsi_val = self.buy_rsi.value
        self.bb_period_val = self.bb_period.value
        self.bb_std_val = self.bb_std.value
        self.sell_rsi_val = self.sell_rsi.value

        try:
            path = _BASE / "config/dynamic_params.json"
            if path.exists():
                import json

                params = json.loads(path.read_text())
                sp = params.get("Github_Aatif_qmr_cipher__MeanReversionV1__20260901_081406", {})
                if "buy_rsi" in sp:
                    self.buy_rsi_val = int(sp["buy_rsi"])
                if "bb_period" in sp:
                    self.bb_period_val = int(sp["bb_period"])
                if "bb_std" in sp:
                    self.bb_std_val = float(sp["bb_std"])
                if "sell_rsi" in sp:
                    self.sell_rsi_val = int(sp["sell_rsi"])
                logger.info(
                    f"[Github_Aatif_qmr_cipher__MeanReversionV1__20260901_081406] Dynamic params: buy_rsi={self.buy_rsi_val} "
                    f"bb_period={self.bb_period_val} bb_std={self.bb_std_val} "
                    f"sell_rsi={self.sell_rsi_val}"
                )
        except Exception as e:
            logger.warning(f"[Github_Aatif_qmr_cipher__MeanReversionV1__20260901_081406] Failed to load dynamic params: {e}")

    # ── FreqAI hooks (disabled unless freqai.enabled=true in config) ─────────
    # NOTE: sentiment features removed from FreqAI hooks because they introduce
    # look-ahead bias in backtests (current_score.json is not point-in-time).
    # Only price-derived features are safe for FreqAI training.

    def feature_engineering_expand_all(
        self, dataframe: DataFrame, period: int, metadata: dict, **kwargs
    ) -> DataFrame:
        dataframe["%-rsi-period"] = ta.rsi(dataframe["close"], length=period) / 100
        dataframe["%-bb_lower-period"] = dataframe["close"] / (
            dataframe["bb_lower"] if "bb_lower" in dataframe.columns else dataframe["close"]
        )
        return dataframe

    def feature_engineering_expand_basic(
        self, dataframe: DataFrame, metadata: dict, **kwargs
    ) -> DataFrame:
        return dataframe

    def feature_engineering_standard(
        self, dataframe: DataFrame, metadata: dict, **kwargs
    ) -> DataFrame:
        return dataframe

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

    # ── Indicator computation ─────────────────────────────────────────────────

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        self.load_dynamic_params()

        from qnt.polars_indicators import add_atr, add_bollinger_bands, add_ema, add_rsi, add_sma
        from qnt.polars_ohlcv import ohlcv_to_pandas, pandas_to_polars

        df_pl = pandas_to_polars(dataframe)

        df_pl = add_rsi(df_pl, period=14, alias="rsi")
        df_pl = add_bollinger_bands(
            df_pl, period=self.bb_period_val, std_dev=self.bb_std_val, prefix="bb"
        )
        df_pl = df_pl.rename({"bb_mid": "bb_middle"})
        df_pl = add_atr(df_pl, period=14, alias="atr")
        df_pl = add_ema(df_pl, period=200, alias="ema_200")
        # 30-candle rolling median volume for the volume filter
        df_pl = add_sma(df_pl, period=30, column="volume", alias="volume_median_30")

        dataframe = ohlcv_to_pandas(df_pl)

        # 4h EMA_200 — stronger trend filter; forward-filled into 1h bars
        if self.dp:
            inf_4h = self.dp.get_pair_dataframe(pair=metadata["pair"], timeframe="4h")
            if inf_4h is not None and not inf_4h.empty:
                inf_4h = inf_4h.copy()
                inf_4h["ema_200"] = ta.ema(inf_4h["close"], length=200)
                dataframe = merge_informative_pair(
                    dataframe, inf_4h, self.timeframe, "4h", ffill=True
                )

        dataframe = merge_macro_data(dataframe)
        return dataframe

    # ── Entry ─────────────────────────────────────────────────────────────────

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        self.load_dynamic_params()

        is_live = self.config.get("runmode", {}).value in ("live", "dry_run")
        regime_ok = True
        vol_ok = True
        if is_live:
            regime = detect_regime(dataframe, metadata["pair"])
            regime_ok = get_regime_for_strategy("Github_Aatif_qmr_cipher__MeanReversionV1__20260901_081406", regime)
            vol_ok = not is_vol_elevated(dataframe, metadata["pair"])

        # 4h trend filter — pass-through if column absent (e.g. backtests without 4h data)
        if "ema_200_4h" in dataframe.columns:
            ema_4h_ok = dataframe["ema_200_4h"].isna() | (
                dataframe["close"] > dataframe["ema_200_4h"]
            )
        else:
            ema_4h_ok = True

        dataframe.loc[
            (
                # RSI oversold
                (dataframe["rsi"] < self.buy_rsi_val)
                # RSI momentum: bounce must be starting, not still falling
                & (dataframe["rsi"] > dataframe["rsi"].shift(1))
                # Price below lower Bollinger Band
                & (dataframe["close"] < dataframe["bb_lower"])
                # Volume above 30-candle median (avoid illiquid entries)
                & (dataframe["volume"] > dataframe["volume_median_30"])
                # Trend filter: above both 1h EMA_200 and 4h EMA_200 (no falling knives)
                & (dataframe["close"] > dataframe["ema_200"])
                & ema_4h_ok
                # Regime gate (live only — backtest would require point-in-time HMM labels)
                & (regime_ok)
                # Volatility gate (live only — block entries during flash crashes/blow-offs)
                & (vol_ok)
            ),
            "enter_long",
        ] = 1
        return dataframe

    # ── Exit ──────────────────────────────────────────────────────────────────

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        self.load_dynamic_params()

        raw_exit = dataframe["rsi"] > self.sell_rsi_val
        # 2-candle persistence prevents single-candle RSI spikes triggering premature exit
        dataframe.loc[raw_exit & raw_exit.shift(1).fillna(False), "exit_long"] = 1
        return dataframe

    def custom_stoploss(
        self,
        pair: str,
        trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        after_fill: bool,
        **kwargs,
    ) -> float:
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if dataframe is None or dataframe.empty or "atr" not in dataframe.columns:
            return self.stoploss
        atr = dataframe["atr"].iloc[-1]
        if atr and current_rate and trade.open_rate:
            atr_stop = -(2 * atr) / trade.open_rate
            return max(-0.06, min(-0.02, atr_stop))
        return self.stoploss

    def custom_exit(
        self,
        pair: str,
        trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs,
    ):
        hours_open = (current_time - trade.open_date_utc).total_seconds() / 3600

        # 24h time-stop: mean reversion should happen within a day; if not, edge is gone
        if hours_open >= 24 and current_profit < -0.01:
            return "time_stop_24h"

        # In BEAR regime, take profits quickly
        try:
            dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
            if dataframe is not None and not dataframe.empty:
                regime = detect_regime(dataframe, pair)
                if regime == "BEAR" and current_profit >= 0.015 and hours_open >= 2:
                    return "bear_bounce_target"
        except Exception:
            pass

        return None

    def custom_stake_amount(
        self,
        current_time,
        current_rate,
        proposed_stake,
        min_stake,
        max_stake,
        leverage,
        entry_tag,
        side,
        **kwargs,
    ):
        multiplier = get_stake_multiplier("Github_Aatif_qmr_cipher__MeanReversionV1__20260901_081406")
        stake = proposed_stake * multiplier
        if min_stake is not None:
            stake = max(stake, min_stake)
        return min(stake, max_stake)

    # ── Trade confirmation gate ───────────────────────────────────────────────

    def confirm_trade_entry(
        self,
        pair: str,
        order_type: str,
        amount: float,
        rate: float,
        time_in_force: str,
        current_time: datetime,
        entry_tag: str,
        side: str,
        **kwargs,
    ) -> bool:
        if self.config.get("runmode", {}).value not in ("live", "dry_run"):
            return True

        try:
            total_balance = self.wallets.get_total("USDT")

            recent_trades = [
                {
                    "profit_ratio": float(
                        getattr(t, "close_profit", None) or getattr(t, "profit_ratio", None) or 0.0
                    ),
                    "close_date": getattr(t, "close_date", None),
                }
                for t in Trade.get_trades_proxy(is_open=False)
            ][:10]

            one_hour_ago = current_time - timedelta(hours=1)
            trades_last_hour = len(
                [
                    t
                    for t in Trade.get_trades_proxy(is_open=False)
                    if t.close_date and t.close_date >= one_hour_ago
                ]
            )

            import json

            state_file = _BASE / "risk/balance_state.json"
            if state_file.exists():
                with open(state_file) as f:
                    state = json.load(f)
                start_of_day = state.get("start_of_day", total_balance)
                start_of_week = state.get("start_of_week", total_balance)
            else:
                start_of_day = total_balance
                start_of_week = total_balance

            risk_result = run_all_checks(
                current_balance=total_balance,
                start_of_day_balance=start_of_day,
                start_of_week_balance=start_of_week,
                trade_amount_usdt=amount * rate,
                trades_last_hour=trades_last_hour,
                recent_trades=recent_trades,
                min_sentiment="NEUTRAL",
            )

            if not risk_result["safe_to_trade"]:
                logger.info(
                    f"[RISK BLOCK] MeanReversion blocked for {pair}: {risk_result['blocking_reasons']}"
                )
                return False

            sentiment = get_current_sentiment()
            signal = get_sentiment_signal()
            logger.info(
                f"[Sentiment] {pair} | Score: {sentiment['score']:.3f} | Signal: {signal}"
            )

        except Exception as e:
            logger.error(f"[RISK WARNING] Risk check error: {e}")

        base = pair.split("/")[0]
        if corr_blocked(base, side):
            logger.info(f"[CORR BLOCK] Github_Aatif_qmr_cipher__MeanReversionV1__20260901_081406 {pair} — concurrent long limit reached")
            return False

        if side == "long":
            funding = get_funding_rate()
            if funding < -0.5:
                logger.info(
                    f"[FUNDING BLOCK] Github_Aatif_qmr_cipher__MeanReversionV1__20260901_081406 {pair} funding={funding:.2f}"
                )
                return False

        return True
