# source: https://raw.githubusercontent.com/DOUGLASGUEDESATRIA/OSIRIS_TRADE/fe3c09567f3798d1cad6d7a306fa4f6131d91e17/user_data/strategies/OsirisGoldV5.py
"""
Github_DOUGLASGUEDESATRIA_OSIRIS_TRADE__OsirisGoldV5__20260329_060843 — Pure Mean-Reversion + Volatility Scalper (BTC 5m)
=================================================================
INSIGHT: Traditional indicators have ZERO directional edge on 5m.
  V4 proved WR=49% at all score levels = coin flip.
  
NEW APPROACH: Don't predict direction. Exploit mean-reversion.
  When price stretches beyond BB bands + volume spike → it snaps back.
  Entry: Mean-reversion (at extremes, bet on reversion)
  Exit: Quick scalp to BB middle or small fixed target
  
PHYSICS:
  BTC 5m candle avg range = ~$300 (0.3%)
  BB(20,2) captures 95% of moves → touch of band = 2.5% of candles = ~7/day
  With volume filter = ~4-5/day
  Mean-reversion to BB mid = ~$150-200 profit = 0.15-0.2%
  SL = below/above the extreme wick = ~0.3-0.5%

ENTRIES:
  LONG:  Close <= BB lower (or wick below) + volume spike + green candle
  SHORT: Close >= BB upper (or wick above) + volume spike + red candle
  
  NO TREND FILTER (mean-reversion works in ALL regimes)
  
EXITS:
  TP: BB middle (natural mean-reversion target)
  SL: Fixed % below entry (wider than noise)
  Timeout: 6 candles (30min) — if no reversion, exit
"""
import logging
import numpy as np
from pandas import DataFrame

from freqtrade.persistence import Trade
from freqtrade.strategy import (
    IStrategy,
    DecimalParameter,
    IntParameter,
)
import talib.abstract as ta

logger = logging.getLogger(__name__)


class Github_DOUGLASGUEDESATRIA_OSIRIS_TRADE__OsirisGoldV5__20260329_060843(IStrategy):
    INTERFACE_VERSION = 3
    can_short = True
    timeframe = "5m"

    stoploss = -0.015  # Emergency SL 1.5%
    minimal_roi = {"0": 100}  # Disabled — custom_exit handles TP
    trailing_stop = False
    use_custom_stoploss = False
    startup_candle_count = 50
    process_only_new_candles = True

    # ── Mean-reversion params ────────────────────────────────────────────────
    bb_period = IntParameter(15, 30, default=20, space="buy", optimize=True)
    bb_std = DecimalParameter(1.5, 2.5, default=2.0, decimals=1, space="buy", optimize=True)
    vol_mult = DecimalParameter(1.0, 2.5, default=1.3, decimals=1, space="buy", optimize=True)
    rsi_extreme_low = IntParameter(20, 40, default=30, space="buy", optimize=True)
    rsi_extreme_high = IntParameter(60, 80, default=70, space="buy", optimize=True)

    # ── Exit params ──────────────────────────────────────────────────────────
    sl_pct = DecimalParameter(0.3, 1.5, default=0.8, decimals=1, space="sell", optimize=True)
    tp_to_mid_pct = DecimalParameter(0.3, 1.0, default=0.7, decimals=1, space="sell", optimize=True)
    timeout_candles = IntParameter(3, 12, default=6, space="sell", optimize=True)

    _last_entry_time: dict = {}

    def informative_pairs(self):
        return []  # No need for multi-TF — pure price action

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # Bollinger Bands
        for period in [15, 20, 25, 30]:
            for std in [1.5, 2.0, 2.5]:
                bb = ta.BBANDS(dataframe, timeperiod=period, nbdevup=std, nbdevdn=std)
                col_suffix = f"_{period}_{str(std).replace('.','')}"
                dataframe[f"bb_upper{col_suffix}"] = bb["upperband"]
                dataframe[f"bb_lower{col_suffix}"] = bb["lowerband"]
                dataframe[f"bb_mid{col_suffix}"] = bb["middleband"]

        # Current active BB (we'll use populate_entry_trend to select)
        bb = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
        dataframe["bb_upper"] = bb["upperband"]
        dataframe["bb_lower"] = bb["lowerband"]
        dataframe["bb_mid"] = bb["middleband"]
        dataframe["bb_width"] = (bb["upperband"] - bb["lowerband"]) / bb["middleband"]

        # RSI for confirmation
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)

        # Volume spike detection
        dataframe["volume_sma"] = ta.SMA(dataframe["volume"], timeperiod=20)
        dataframe["rvol"] = dataframe["volume"] / dataframe["volume_sma"].replace(0, np.nan)

        # Candle properties
        dataframe["is_green"] = (dataframe["close"] > dataframe["open"]).astype(int)
        dataframe["is_red"] = (dataframe["close"] < dataframe["open"]).astype(int)

        # Distance from BB bands (normalized)
        dataframe["dist_lower"] = (dataframe["close"] - dataframe["bb_lower"]) / dataframe["close"] * 100
        dataframe["dist_upper"] = (dataframe["bb_upper"] - dataframe["close"]) / dataframe["close"] * 100

        # ATR for dynamic SL
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        bb_p = int(self.bb_period.value)
        bb_s = float(self.bb_std.value)
        col = f"_{bb_p}_{str(bb_s).replace('.','')}"
        vol_m = float(self.vol_mult.value)
        rsi_low = int(self.rsi_extreme_low.value)
        rsi_high = int(self.rsi_extreme_high.value)

        has_data = dataframe["rsi"].notna() & (dataframe["volume"] > 0)
        bb_lower = dataframe[f"bb_lower{col}"]
        bb_upper = dataframe[f"bb_upper{col}"]
        bb_mid = dataframe[f"bb_mid{col}"]

        # ═════════════════════════════════════════════════════════════════════
        # LONG: Price at/below lower BB + volume spike + oversold RSI
        # Mean-reversion: extreme low → expect bounce to middle
        # ═════════════════════════════════════════════════════════════════════
        long_signal = (
            has_data
            & (dataframe["low"] <= bb_lower)  # Wick touches lower BB
            & (dataframe["close"] > bb_lower)  # But closes above (bounce)
            & (dataframe["is_green"] == 1)     # Green candle (buying pressure)
            & (dataframe["rsi"] < rsi_low)     # Oversold
            & (dataframe["rvol"] > vol_m)      # Volume spike (participation)
        )

        # ═════════════════════════════════════════════════════════════════════
        # SHORT: Price at/above upper BB + volume spike + overbought RSI
        # Mean-reversion: extreme high → expect drop to middle
        # ═════════════════════════════════════════════════════════════════════
        short_signal = (
            has_data
            & (dataframe["high"] >= bb_upper)  # Wick touches upper BB
            & (dataframe["close"] < bb_upper)  # Closes below (rejection)
            & (dataframe["is_red"] == 1)       # Red candle (selling pressure)
            & (dataframe["rsi"] > rsi_high)    # Overbought
            & (dataframe["rvol"] > vol_m)      # Volume spike
        )

        dataframe.loc[long_signal, "enter_long"] = 1
        dataframe.loc[long_signal, "enter_tag"] = "bb_bounce"
        dataframe.loc[short_signal, "enter_short"] = 1
        dataframe.loc[short_signal, "enter_tag"] = "bb_reject"

        return dataframe

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

    def custom_exit(
        self, pair, trade, current_time, current_rate, current_profit, **kwargs,
    ):
        # Get BB middle for target
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if dataframe.empty:
            return None

        bb_p = int(self.bb_period.value)
        bb_s = float(self.bb_std.value)
        col = f"_{bb_p}_{str(bb_s).replace('.','')}"
        last = dataframe.iloc[-1]
        bb_mid = last.get(f"bb_mid{col}", 0)

        is_short = getattr(trade, "is_short", False)

        # TP: Price reaches BB middle (mean-reversion target)
        if not is_short and current_rate >= bb_mid and current_profit > 0:
            return "mr_tp_mid"
        if is_short and current_rate <= bb_mid and current_profit > 0:
            return "mr_tp_mid"

        # SL: Fixed percentage
        sl = float(self.sl_pct.value) / 100
        if current_profit <= -sl:
            return "mr_sl"

        # Timeout: Exit after N candles regardless
        if trade.open_date_utc:
            elapsed = (current_time - trade.open_date_utc).total_seconds() / 300
            if elapsed >= self.timeout_candles.value:
                return "mr_timeout"

        return None

    def confirm_trade_entry(
        self, pair, order_type, amount, rate, time_in_force,
        current_time, entry_tag, side, **kwargs,
    ) -> bool:
        # Cooldown: 2 candles minimum between entries
        last = self._last_entry_time.get(pair)
        if last is not None and (current_time - last).total_seconds() < 600:
            return False
        self._last_entry_time[pair] = current_time
        return True

    def leverage(self, pair, current_time, current_rate, proposed_leverage,
                 max_leverage, entry_tag, side, **kwargs) -> float:
        return 1.0
