# source: https://raw.githubusercontent.com/DOUGLASGUEDESATRIA/OSIRIS_TRADE/fe3c09567f3798d1cad6d7a306fa4f6131d91e17/user_data/strategies/OsirisGoldV7.py
"""
Github_DOUGLASGUEDESATRIA_OSIRIS_TRADE__OsirisGoldV7__20260329_060843 — Research Scoring + V9 Exit Logic (BTC 5m)
==========================================================
EVIDENCE-BASED DESIGN:

V4-V6 proved that simple indicators (RSI/BB/EMA) have ZERO edge on 5m.
OsirisResearch proved that 17-point scoring with microstructure+volume
flow achieves 100% WR at threshold=14 (but only 0.18 trades/day).

STRATEGY:
- ENTRY: Research's 17-point scoring (volume flow + microstructure + 
  innovation + smart money + multi-TF) at LOWER threshold (10-12)
  → ~2-5 trades/day instead of 0.18
  
- EXIT: V9's proven time-profit system:
  1. Fixed TP at R×1.5
  2. TIME PROFIT (the real edge): after 8 candles, take any profit >0.3%
  3. Momentum exit on trend loss
  4. Hard timeout at 48 candles
  
- PROTECTION: XRSI's loss-streak cooldown (5 losses → pause 32min)
- STOP: Fixed 3.5×ATR (no trailing — V7 diagnosis: trailing kills)
- BREAKEVEN: At 0.5×R → lock +0.1%

LONG + SHORT (both directions, scored independently)
"""
import logging
import numpy as np
from datetime import timedelta
from pandas import DataFrame
from typing import Optional

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

logger = logging.getLogger(__name__)


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

    stoploss = -0.05          # Emergency hard stop 5% (custom takes over)
    minimal_roi = {"0": 100}  # Disabled — custom_exit handles all
    trailing_stop = False
    use_custom_stoploss = True
    startup_candle_count = 200
    process_only_new_candles = True

    # ── ENTRY: Score thresholds ──────────────────────────────────────────
    buy_score_min = IntParameter(8, 15, default=11, space="buy", optimize=True)
    sell_score_min = IntParameter(8, 15, default=11, space="buy", optimize=True)

    # ── ENTRY: Volume Flow params ────────────────────────────────────────
    buy_pressure = DecimalParameter(0.45, 0.65, default=0.50, decimals=2, space="buy", optimize=True)
    buy_rvol = DecimalParameter(0.7, 2.0, default=0.9, decimals=1, space="buy", optimize=True)
    buy_cvd_bars = IntParameter(2, 6, default=3, space="buy", optimize=True)
    buy_mfi = IntParameter(15, 40, default=22, decimals=0, space="buy", optimize=True)

    # ── ENTRY: Momentum params ───────────────────────────────────────────
    buy_rsi_min = IntParameter(25, 40, default=29, space="buy", optimize=True)
    buy_rsi_max = IntParameter(60, 75, default=69, space="buy", optimize=True)
    buy_stochrsi = IntParameter(25, 55, default=44, space="buy", optimize=True)
    buy_cci = IntParameter(-150, -40, default=-75, space="buy", optimize=True)

    # ── ENTRY: Trend params ──────────────────────────────────────────────
    buy_adx = IntParameter(12, 30, default=15, space="buy", optimize=True)
    buy_ribbon = IntParameter(1, 4, default=1, space="buy", optimize=True)

    # ── ENTRY: Innovation params ─────────────────────────────────────────
    buy_efficiency = DecimalParameter(0.05, 0.25, default=0.11, decimals=2, space="buy", optimize=True)
    buy_vol_ratio = DecimalParameter(1.0, 2.0, default=1.6, decimals=1, space="buy", optimize=True)

    # ── ENTRY: Multi-TF ──────────────────────────────────────────────────
    buy_rsi_15m = IntParameter(30, 50, default=36, space="buy", optimize=True)

    # ── EXIT: V9-style time-profit ───────────────────────────────────────
    atr_sl_mult = DecimalParameter(2.0, 5.0, default=3.5, decimals=1, space="sell", optimize=True)
    breakeven_r = DecimalParameter(0.3, 0.8, default=0.5, decimals=1, space="sell", optimize=True)
    tp_r_mult = DecimalParameter(1.0, 2.5, default=1.5, decimals=1, space="sell", optimize=True)
    time_profit_candles = IntParameter(6, 16, default=8, space="sell", optimize=True)
    time_profit_min = DecimalParameter(0.1, 0.5, default=0.3, decimals=1, space="sell", optimize=True)
    timeout_candles = IntParameter(30, 60, default=48, space="sell", optimize=True)

    # ── State ────────────────────────────────────────────────────────────
    _last_entry: dict = {}
    _consec_losses: dict = {}
    _trade_R: dict = {}  # Store R value per trade

    def informative_pairs(self):
        pairs = self.dp.current_whitelist() if self.dp else []
        return [(p, "15m") for p in pairs] + [(p, "1h") for p in pairs]

    # =====================================================================
    # VOLUME FLOW (from Research)
    # =====================================================================
    def _calc_volume_flow(self, df: DataFrame) -> DataFrame:
        hl_range = (df["high"] - df["low"]).replace(0, np.nan)
        df["buy_vol"] = (df["volume"] * (df["close"] - df["low"]) / hl_range).fillna(df["volume"] * 0.5)
        df["sell_vol"] = (df["volume"] * (df["high"] - df["close"]) / hl_range).fillna(df["volume"] * 0.5)
        total = (df["buy_vol"] + df["sell_vol"]).replace(0, 1)
        df["pressure_ratio"] = (df["buy_vol"] / total).fillna(0.5)
        df["volume_delta"] = df["buy_vol"] - df["sell_vol"]
        df["cvd"] = df["volume_delta"].cumsum()
        df["cvd_rising"] = (df["cvd"] > df["cvd"].shift(1)).astype(int)
        df["vol_sma20"] = df["volume"].rolling(20).mean()
        df["rvol"] = (df["volume"] / df["vol_sma20"].replace(0, 1)).fillna(1)
        df["mfi"] = ta.MFI(df, timeperiod=14)

        # CMF
        mf_mult = ((df["close"] - df["low"]) - (df["high"] - df["close"])) / hl_range
        mf_mult = mf_mult.fillna(0)
        df["cmf"] = (mf_mult * df["volume"]).rolling(20).sum() / df["volume"].rolling(20).sum().replace(0, 1)
        return df

    # =====================================================================
    # MICROSTRUCTURE (from Research)
    # =====================================================================
    def _calc_microstructure(self, df: DataFrame) -> DataFrame:
        hl_range = (df["high"] - df["low"]).replace(0, np.nan).fillna(0.0001)
        body = (df["close"] - df["open"]).abs()
        df["body_ratio"] = body / hl_range
        df["upper_wick_ratio"] = (df["high"] - df[["open", "close"]].max(axis=1)) / hl_range
        df["lower_wick_ratio"] = (df[["open", "close"]].min(axis=1) - df["low"]) / hl_range

        df["is_absorption"] = ((df["body_ratio"] < 0.30) & (df["rvol"] > 1.5)).astype(int)
        df["is_pin_bar_bull"] = ((df["lower_wick_ratio"] > 0.60) & (df["body_ratio"] < 0.25) & (df["upper_wick_ratio"] < 0.15)).astype(int)
        df["is_pin_bar_bear"] = ((df["upper_wick_ratio"] > 0.60) & (df["body_ratio"] < 0.25) & (df["lower_wick_ratio"] < 0.15)).astype(int)

        recent_high = df["high"].rolling(20).max().shift(1)
        recent_low = df["low"].rolling(20).min().shift(1)
        df["liq_sweep_low"] = ((df["low"] < recent_low) & (df["close"] > recent_low)).astype(int)
        df["liq_sweep_high"] = ((df["high"] > recent_high) & (df["close"] < recent_high)).astype(int)

        return df

    # =====================================================================
    # INNOVATION (from Research)
    # =====================================================================
    def _calc_innovation(self, df: DataFrame) -> DataFrame:
        period = 20
        direction = (df["close"] - df["close"].shift(period)).abs()
        volatility = df["close"].diff().abs().rolling(period).sum()
        df["efficiency_ratio"] = (direction / volatility.replace(0, 1)).fillna(0)

        atr14 = ta.ATR(df, timeperiod=14)
        atr50 = ta.ATR(df, timeperiod=50)
        df["volatility_ratio"] = (atr14 / atr50.replace(0, 1)).fillna(1)

        # Squeeze: BB inside Keltner
        kelt_ema = ta.EMA(df, timeperiod=20)
        kelt_atr = ta.ATR(df, timeperiod=10)
        bb = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
        squeeze_on = ((bb["lowerband"] > kelt_ema - kelt_atr * 1.5) & (bb["upperband"] < kelt_ema + kelt_atr * 1.5)).astype(int)
        df["squeeze_release_bull"] = ((squeeze_on.shift(1) == 1) & (squeeze_on == 0) & (df["close"] > df["close"].shift(1))).astype(int)
        df["squeeze_release_bear"] = ((squeeze_on.shift(1) == 1) & (squeeze_on == 0) & (df["close"] < df["close"].shift(1))).astype(int)

        return df

    # =====================================================================
    # INDICATORS
    # =====================================================================
    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        pair = metadata["pair"]

        # Standard
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
        macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
        dataframe["macd_hist"] = macd["macdhist"]

        dataframe["ema8"] = ta.EMA(dataframe, timeperiod=8)
        dataframe["ema13"] = ta.EMA(dataframe, timeperiod=13)
        dataframe["ema21"] = ta.EMA(dataframe, timeperiod=21)
        dataframe["ema50"] = ta.EMA(dataframe, timeperiod=50)
        dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
        dataframe["ema_ribbon"] = (
            (dataframe["ema8"] > dataframe["ema13"]).astype(int)
            + (dataframe["ema13"] > dataframe["ema21"]).astype(int)
            + (dataframe["ema21"] > dataframe["ema50"]).astype(int)
            + (dataframe["ema50"] > dataframe["ema200"]).astype(int)
        )
        dataframe["ema_ribbon_bear"] = (
            (dataframe["ema8"] < dataframe["ema13"]).astype(int)
            + (dataframe["ema13"] < dataframe["ema21"]).astype(int)
            + (dataframe["ema21"] < dataframe["ema50"]).astype(int)
            + (dataframe["ema50"] < dataframe["ema200"]).astype(int)
        )

        dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)

        stoch = ta.STOCHRSI(dataframe, timeperiod=14, fastk_period=3, fastd_period=3)
        dataframe["stochrsi_k"] = stoch["fastk"]
        dataframe["cci"] = ta.CCI(dataframe, timeperiod=14)
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)

        # VWAP rolling 50
        tp = (dataframe["high"] + dataframe["low"] + dataframe["close"]) / 3
        vwap_vol = dataframe["volume"].rolling(50).sum().replace(0, 1)
        dataframe["vwap"] = (tp * dataframe["volume"]).rolling(50).sum() / vwap_vol

        # Proprietary
        dataframe = self._calc_volume_flow(dataframe)
        dataframe = self._calc_microstructure(dataframe)
        dataframe = self._calc_innovation(dataframe)

        # 15m
        inf_15m = self.dp.get_pair_dataframe(pair=pair, timeframe="15m") if self.dp else DataFrame()
        if not inf_15m.empty:
            inf_15m["rsi_15m"] = ta.RSI(inf_15m, timeperiod=14)
            inf_15m["ema50_15m"] = ta.EMA(inf_15m, timeperiod=50)
            m15 = merge_informative_pair(
                dataframe, inf_15m[["date", "rsi_15m", "ema50_15m"]],
                self.timeframe, "15m", ffill=True,
            )
            dataframe["rsi_15m"] = m15["rsi_15m_15m"].values
            dataframe["ema50_15m"] = m15["ema50_15m_15m"].values
        else:
            dataframe["rsi_15m"] = 50
            dataframe["ema50_15m"] = dataframe["close"]

        # 1h
        inf_1h = self.dp.get_pair_dataframe(pair=pair, timeframe="1h") if self.dp else DataFrame()
        if not inf_1h.empty:
            inf_1h["ema9_1h"] = ta.EMA(inf_1h, timeperiod=9)
            inf_1h["ema21_1h"] = ta.EMA(inf_1h, timeperiod=21)
            inf_1h["adx_1h"] = ta.ADX(inf_1h, timeperiod=14)
            m1h = merge_informative_pair(
                dataframe, inf_1h[["date", "ema9_1h", "ema21_1h", "adx_1h"]],
                self.timeframe, "1h", ffill=True,
            )
            dataframe["ema9_1h"] = m1h["ema9_1h_1h"].values
            dataframe["ema21_1h"] = m1h["ema21_1h_1h"].values
            dataframe["adx_1h"] = m1h["adx_1h_1h"].values
        else:
            dataframe["ema9_1h"] = dataframe["close"]
            dataframe["ema21_1h"] = dataframe["close"]
            dataframe["adx_1h"] = 25

        return dataframe

    # =====================================================================
    # SCORING SYSTEM — LONG (17 points max, mirrors Research)
    # =====================================================================
    def _score_long(self, df: DataFrame) -> np.ndarray:
        s = np.zeros(len(df))

        # Volume Flow (4 pts)
        s += (df["pressure_ratio"] > self.buy_pressure.value).astype(int)         # 1
        s += (df["rvol"] > self.buy_rvol.value).astype(int)                       # 2
        cvd_ct = df["cvd_rising"].rolling(int(self.buy_cvd_bars.value)).sum()
        s += (cvd_ct >= self.buy_cvd_bars.value).astype(int)                      # 3
        s += (df["mfi"] > self.buy_mfi.value).astype(int)                         # 4

        # Momentum (4 pts)
        s += ((df["rsi"] > self.buy_rsi_min.value) & (df["rsi"] < self.buy_rsi_max.value)).astype(int)  # 5
        s += ((df["macd_hist"] > 0) | (df["macd_hist"] > df["macd_hist"].shift(1))).astype(int)         # 6
        s += (df["stochrsi_k"] < self.buy_stochrsi.value).astype(int)                                   # 7
        s += ((df["cci"] > self.buy_cci.value) & (df["cci"].shift(1) <= self.buy_cci.value)).astype(int) # 8

        # Trend (3 pts)
        s += (df["adx"] > self.buy_adx.value).astype(int)                         # 9
        s += (df["ema_ribbon"] >= self.buy_ribbon.value).astype(int)               # 10
        s += (df["close"] > df["vwap"]).astype(int)                                # 11

        # Microstructure (3 pts)
        s += (df["is_absorption"].rolling(3).sum() > 0).astype(int)                # 12
        s += ((df["is_pin_bar_bull"] == 1) | (df["liq_sweep_low"] == 1)).astype(int)  # 13
        s += (df["squeeze_release_bull"] == 1).astype(int)                         # 14

        # Innovation (2 pts)
        s += (df["efficiency_ratio"] > self.buy_efficiency.value).astype(int)      # 15
        s += (df["volatility_ratio"] < self.buy_vol_ratio.value).astype(int)       # 16

        # Multi-TF (1 pt)
        s += (df["rsi_15m"] > self.buy_rsi_15m.value).astype(int)                 # 17

        return s

    # =====================================================================
    # SCORING SYSTEM — SHORT (17 points max, inverted)
    # =====================================================================
    def _score_short(self, df: DataFrame) -> np.ndarray:
        s = np.zeros(len(df))

        # Volume Flow (4 pts) — INVERTED
        s += (df["pressure_ratio"] < (1 - self.buy_pressure.value)).astype(int)    # 1
        s += (df["rvol"] > self.buy_rvol.value).astype(int)                        # 2
        cvd_fall = (1 - df["cvd_rising"]).rolling(int(self.buy_cvd_bars.value)).sum()
        s += (cvd_fall >= self.buy_cvd_bars.value).astype(int)                     # 3
        s += (df["mfi"] < (100 - self.buy_mfi.value)).astype(int)                 # 4

        # Momentum (4 pts) — INVERTED
        rsi_low = 100 - self.buy_rsi_max.value
        rsi_high = 100 - self.buy_rsi_min.value
        s += ((df["rsi"] > rsi_low) & (df["rsi"] < rsi_high)).astype(int)          # 5
        s += ((df["macd_hist"] < 0) | (df["macd_hist"] < df["macd_hist"].shift(1))).astype(int)  # 6
        s += (df["stochrsi_k"] > (100 - self.buy_stochrsi.value)).astype(int)      # 7
        cci_inv = -self.buy_cci.value
        s += ((df["cci"] < cci_inv) & (df["cci"].shift(1) >= cci_inv)).astype(int) # 8

        # Trend (3 pts) — INVERTED
        s += (df["adx"] > self.buy_adx.value).astype(int)                          # 9
        s += (df["ema_ribbon_bear"] >= self.buy_ribbon.value).astype(int)           # 10
        s += (df["close"] < df["vwap"]).astype(int)                                 # 11

        # Microstructure (3 pts) — INVERTED
        s += (df["is_absorption"].rolling(3).sum() > 0).astype(int)                 # 12
        s += ((df["is_pin_bar_bear"] == 1) | (df["liq_sweep_high"] == 1)).astype(int)  # 13
        s += (df["squeeze_release_bear"] == 1).astype(int)                          # 14

        # Innovation (2 pts) — same
        s += (df["efficiency_ratio"] > self.buy_efficiency.value).astype(int)       # 15
        s += (df["volatility_ratio"] < self.buy_vol_ratio.value).astype(int)        # 16

        # Multi-TF (1 pt) — INVERTED
        s += (df["rsi_15m"] < (100 - self.buy_rsi_15m.value)).astype(int)          # 17

        return s

    # =====================================================================
    # ENTRY
    # =====================================================================
    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        has_data = dataframe["rsi"].notna() & (dataframe["volume"] > 0)

        sl = self._score_long(dataframe)
        ss = self._score_short(dataframe)

        thr_l = int(self.buy_score_min.value)
        thr_s = int(self.sell_score_min.value)

        long_sig = has_data & (sl >= thr_l)
        short_sig = has_data & (ss >= thr_s)

        dataframe.loc[long_sig, "enter_long"] = 1
        dataframe.loc[long_sig, "enter_tag"] = "L" + sl[long_sig].astype(int).astype(str)
        dataframe.loc[short_sig, "enter_short"] = 1
        dataframe.loc[short_sig, "enter_tag"] = "S" + ss[short_sig].astype(int).astype(str)

        return dataframe

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

    # =====================================================================
    # TRADE MANAGEMENT — V9 style
    # =====================================================================
    def confirm_trade_entry(
        self, pair, order_type, amount, rate, time_in_force,
        current_time, entry_tag, side, **kwargs,
    ) -> bool:
        # XRSI loss-streak cooldown
        losses = self._consec_losses.get(pair, 0)
        if losses >= 5:
            self._consec_losses[pair] = 0  # Reset, try again after 1 skip
            return False

        # 2 candle cooldown
        last = self._last_entry.get(pair)
        if last is not None and (current_time - last).total_seconds() < 600:
            return False
        self._last_entry[pair] = current_time
        return True

    def custom_stoploss(
        self, pair, trade, current_time, current_rate, current_profit,
        after_fill, **kwargs,
    ) -> Optional[float]:
        """Fixed ATR stop + breakeven. NO trailing."""
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if dataframe.empty or len(dataframe) < 2:
            return None

        last = dataframe.iloc[-1]
        atr = last.get("atr", 0)
        if atr <= 0 or trade.open_rate <= 0:
            return None

        # R = risk in percent
        R = (atr * float(self.atr_sl_mult.value)) / trade.open_rate
        R = max(0.015, min(R, 0.05))  # Clamp 1.5% — 5%

        # Store R for TP calculation
        trade_id = f"{pair}_{trade.open_date_utc}"
        self._trade_R[trade_id] = R

        # Breakeven: once profit >= breakeven_r × R → lock +0.1%
        be_trigger = R * float(self.breakeven_r.value)
        if current_profit >= be_trigger:
            return -0.001  # Lock at +0.1%

        return -R

    def custom_exit(
        self, pair, trade, current_time, current_rate, current_profit, **kwargs,
    ):
        """V9-style time-profit exit system."""
        trade_id = f"{pair}_{trade.open_date_utc}"
        R = self._trade_R.get(trade_id, 0.02)

        elapsed = 0
        if trade.open_date_utc:
            elapsed = (current_time - trade.open_date_utc).total_seconds() / 300

        # 1. Fixed TP at R × mult
        tp = R * float(self.tp_r_mult.value)
        if current_profit >= tp:
            self._update_streak(pair, True)
            return "v7_tp"

        # 2. TIME PROFIT (THE EDGE): after N candles, take modest profit
        tp_candles = int(self.time_profit_candles.value)
        tp_min = float(self.time_profit_min.value) / 100
        if elapsed >= tp_candles and current_profit >= tp_min:
            self._update_streak(pair, True)
            return "v7_time_profit"

        # 3. Momentum exit: after 4 candles, if losing + RSI contra
        if elapsed >= 4 and current_profit < 0:
            dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
            if not dataframe.empty:
                last = dataframe.iloc[-1]
                rsi = last.get("rsi", 50)
                is_short = getattr(trade, "is_short", False)
                # Long losing + RSI > 65 (overbought = missed move)
                if not is_short and rsi > 65:
                    self._update_streak(pair, False)
                    return "v7_momentum_exit"
                # Short losing + RSI < 35 (oversold = missed move)
                if is_short and rsi < 35:
                    self._update_streak(pair, False)
                    return "v7_momentum_exit"

        # 4. Hard timeout
        timeout = int(self.timeout_candles.value)
        if elapsed >= timeout:
            is_win = current_profit > 0
            self._update_streak(pair, is_win)
            return "v7_timeout"

        # 5. Late profit (after timeout/2, take any small profit)
        if elapsed >= timeout / 2 and current_profit >= 0.002:
            self._update_streak(pair, True)
            return "v7_late_profit"

        return None

    def _update_streak(self, pair, is_win):
        if is_win:
            self._consec_losses[pair] = 0
        else:
            self._consec_losses[pair] = self._consec_losses.get(pair, 0) + 1

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