# source: https://raw.githubusercontent.com/DOUGLASGUEDESATRIA/OSIRIS_TRADE/fe3c09567f3798d1cad6d7a306fa4f6131d91e17/user_data/strategies/OsirisFlowDT.py
"""
Github_DOUGLASGUEDESATRIA_OSIRIS_TRADE__OsirisFlowDT__20260329_060843 — Mean Reversion Day Trade Strategy
==================================================
THESIS: Order flow data has ZERO predictive power for CONTINUATION on BTC 5m.
BUT it has genuine edge for MEAN REVERSION (exhaustion → bounce).

PROVEN EDGE (validated on 129K candles, 15 months):
  - RSI < 25 + delta_zscore < -2.0 → LONG → 57.8-61.8% WR at 6-candle horizon
  - RSI < 20 + delta_zscore < -2.0 → LONG → 61.8% WR (n=131, 0.3/day)
  - dz > 3.0 exhaustion (any RSI) → SHORT → 56.0% WR at 3-candle horizon
  - SHORT side is weaker, asymmetric (longs more reliable)

PHILOSOPHY: Few, high-conviction mean reversion trades.
  - Primary: RSI extreme + OF exhaustion = entry trigger
  - Confirmation: Absorption/sweep patterns, BB touch
  - Exit: Quick TP (ATR-based), tight SL, max 2h hold

TARGET: 0.5-2 trades/day, >55% WR, R:R >= 1.5
"""

import logging
from pathlib import Path
import numpy as np
import pandas as pd
from pandas import DataFrame

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

logger = logging.getLogger(__name__)

OF_PATH = Path(__file__).resolve().parent.parent / "data" / "orderflow" / "BTCUSDT-orderflow-5m.feather"


class Github_DOUGLASGUEDESATRIA_OSIRIS_TRADE__OsirisFlowDT__20260329_060843(IStrategy):
    """
    Github_DOUGLASGUEDESATRIA_OSIRIS_TRADE__OsirisFlowDT__20260329_060843 — Mean reversion using RSI extremes + Order Flow exhaustion.
    """

    timeframe = "5m"
    can_short = True

    # ROI: wide targets, exit mostly via trailing or signal
    minimal_roi = {"0": 0.03, "12": 0.018, "24": 0.01}

    stoploss = -0.012
    trailing_stop = True
    trailing_stop_positive = 0.005
    trailing_stop_positive_offset = 0.015  # Only trail after +1.5%
    trailing_only_offset_is_reached = True
    use_custom_stoploss = False

    startup_candle_count = 200
    process_only_new_candles = True

    # ═══════════════════════════════════════════════════════════════════
    # HYPEROPT PARAMETERS — ENTRY
    # ═══════════════════════════════════════════════════════════════════

    # RSI oversold/overbought thresholds
    buy_rsi_max = IntParameter(15, 35, default=25, space="buy", optimize=True)
    sell_rsi_min = IntParameter(65, 85, default=75, space="buy", optimize=True)

    # OF delta z-score exhaustion threshold
    buy_dz_min = DecimalParameter(1.0, 3.0, default=2.0, decimals=1, space="buy", optimize=True)
    sell_dz_min = DecimalParameter(1.0, 3.0, default=2.0, decimals=1, space="buy", optimize=True)

    # Extra filters (confirmations)
    use_bb_touch = CategoricalParameter([True, False], default=True, space="buy", optimize=True)
    use_absorption = CategoricalParameter([True, False], default=True, space="buy", optimize=True)
    use_trend_filter = CategoricalParameter([True, False], default=False, space="buy", optimize=True)

    # Minimum score (confirmation points needed beyond RSI+OF)
    confirm_min = IntParameter(0, 3, default=0, space="buy", optimize=True)

    # ═══════════════════════════════════════════════════════════════════
    # HYPEROPT PARAMETERS — EXIT
    # ═══════════════════════════════════════════════════════════════════

    exit_rsi_long = IntParameter(45, 70, default=55, space="sell", optimize=True)
    exit_rsi_short = IntParameter(30, 55, default=45, space="sell", optimize=True)
    exit_dz_reversal = DecimalParameter(0.0, 1.5, default=0.5, decimals=1, space="sell", optimize=True)

    # ── Internal state ──────────────────────────────────────────────────
    _of_data: pd.DataFrame = None
    _of_loaded = False

    # ═══════════════════════════════════════════════════════════════════
    # ORDER FLOW DATA LOADER
    # ═══════════════════════════════════════════════════════════════════

    def _load_orderflow(self):
        if self._of_loaded:
            return
        self._of_loaded = True
        if OF_PATH.exists():
            try:
                of = pd.read_feather(OF_PATH)
                of["date"] = pd.to_datetime(of["date"], utc=True)
                of = of.set_index("date").sort_index()
                keep = ["delta", "buy_ratio", "delta_zscore", "absorption",
                        "sweep_up", "sweep_down", "big_imbalance", "trade_burst",
                        "aggression", "delta_flips"]
                available = [c for c in keep if c in of.columns]
                self._of_data = of[available]
                logger.info(f"Github_DOUGLASGUEDESATRIA_OSIRIS_TRADE__OsirisFlowDT__20260329_060843: Loaded {len(of)} OF rows")
            except Exception as e:
                logger.warning(f"Github_DOUGLASGUEDESATRIA_OSIRIS_TRADE__OsirisFlowDT__20260329_060843: OF load failed: {e}")
                self._of_data = None
        else:
            logger.warning(f"Github_DOUGLASGUEDESATRIA_OSIRIS_TRADE__OsirisFlowDT__20260329_060843: OF not found at {OF_PATH}")
            self._of_data = None

    def _merge_orderflow(self, df: DataFrame) -> DataFrame:
        self._load_orderflow()
        if self._of_data is None:
            for c in ["of_delta_zscore", "of_buy_ratio", "of_absorption",
                       "of_sweep_up", "of_sweep_down", "of_aggression"]:
                df[c] = 0.0
            return df

        of = self._of_data.copy()
        of = of.rename(columns={
            "delta_zscore": "of_delta_zscore",
            "buy_ratio": "of_buy_ratio",
            "absorption": "of_absorption",
            "sweep_up": "of_sweep_up",
            "sweep_down": "of_sweep_down",
            "aggression": "of_aggression",
        })

        merge_cols = [c for c in of.columns if c.startswith("of_")]
        of_merge = of[merge_cols].reset_index()

        df_date = df["date"].copy()
        if df_date.dt.tz is None:
            df_date = df_date.dt.tz_localize("UTC")
        else:
            df_date = df_date.dt.tz_convert("UTC")

        of_merge["date"] = pd.to_datetime(of_merge["date"], utc=True)
        df["_merge_date"] = df_date
        of_merge = of_merge.rename(columns={"date": "_merge_date"})
        merged = pd.merge(df, of_merge, on="_merge_date", how="left")
        merged = merged.drop(columns=["_merge_date"])

        for c in merge_cols:
            if c in merged.columns:
                merged[c] = merged[c].fillna(0)
        return merged

    # ═══════════════════════════════════════════════════════════════════
    # INDICATORS
    # ═══════════════════════════════════════════════════════════════════

    def informative_pairs(self):
        pairs = self.dp.current_whitelist()
        return [(pair, "15m") for pair in pairs]

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # RSI (primary signal)
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)

        # Bollinger Bands (confirmation)
        bb = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
        dataframe["bb_lower"] = bb["lowerband"]
        dataframe["bb_upper"] = bb["upperband"]
        dataframe["bb_mid"] = bb["middleband"]

        # ATR (for dynamic SL/TP)
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
        dataframe["atr_pct"] = dataframe["atr"] / dataframe["close"] * 100

        # EMAs for trend filter
        dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=50)
        dataframe["ema_200"] = ta.EMA(dataframe, timeperiod=200)

        # StochRSI for overbought/oversold depth
        stoch = ta.STOCHRSI(dataframe, timeperiod=14, fastk_period=3, fastd_period=3)
        dataframe["stochrsi_k"] = stoch["fastk"]

        # CCI for mean reversion confirmation
        dataframe["cci"] = ta.CCI(dataframe, timeperiod=14)

        # Volume
        hl = (dataframe["high"] - dataframe["low"]).replace(0, np.nan)
        bv = (dataframe["volume"] * (dataframe["close"] - dataframe["low"]) / hl).fillna(dataframe["volume"] * 0.5)
        sv = (dataframe["volume"] * (dataframe["high"] - dataframe["close"]) / hl).fillna(dataframe["volume"] * 0.5)
        dataframe["pressure_ratio"] = (bv / (bv + sv).replace(0, 1)).fillna(0.5)
        dataframe["rvol"] = dataframe["volume"] / dataframe["volume"].rolling(20).mean().replace(0, 1)

        # Order Flow
        dataframe = self._merge_orderflow(dataframe)

        # 15m MTF (trend context)
        if self.dp:
            inf_15m = self.dp.get_pair_dataframe(pair=metadata["pair"], timeframe="15m")
            if not inf_15m.empty:
                inf_15m["rsi"] = ta.RSI(inf_15m, timeperiod=14)
                inf_15m["ema_50"] = ta.EMA(inf_15m, timeperiod=50)
                dataframe = merge_informative_pair(dataframe, inf_15m, self.timeframe, "15m", ffill=True)

        return dataframe

    # ═══════════════════════════════════════════════════════════════════
    # ENTRY — MEAN REVERSION
    # ═══════════════════════════════════════════════════════════════════

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

        # ── LONG: RSI oversold + OF selling exhaustion → bounce ─────
        long_base = (
            (dataframe["rsi"] < self.buy_rsi_max.value)
            & (dataframe["of_delta_zscore"] < -self.buy_dz_min.value)
            & (dataframe["volume"] > 0)
        )

        # Confirmation points
        confirm = np.zeros(len(dataframe))
        if self.use_bb_touch.value:
            confirm += (dataframe["close"] <= dataframe["bb_lower"]).astype(int)
        if self.use_absorption.value:
            confirm += (dataframe["of_absorption"] > 0.5).astype(int)
        if self.use_trend_filter.value:
            confirm += (dataframe["close"] > dataframe["ema_200"]).astype(int)

        # Additional confirmations: StochRSI extreme, CCI extreme, high volume
        confirm += (dataframe["stochrsi_k"] < 15).astype(int)
        confirm += (dataframe["cci"] < -150).astype(int)
        confirm += (dataframe["rvol"] > 1.5).astype(int)

        dataframe.loc[long_base & (confirm >= self.confirm_min.value), "enter_long"] = 1

        # ── SHORT: RSI overbought + OF buying exhaustion → drop ─────
        short_base = (
            (dataframe["rsi"] > self.sell_rsi_min.value)
            & (dataframe["of_delta_zscore"] > self.sell_dz_min.value)
            & (dataframe["volume"] > 0)
        )

        confirm_s = np.zeros(len(dataframe))
        if self.use_bb_touch.value:
            confirm_s += (dataframe["close"] >= dataframe["bb_upper"]).astype(int)
        if self.use_absorption.value:
            confirm_s += (dataframe["of_absorption"] > 0.5).astype(int)
        if self.use_trend_filter.value:
            confirm_s += (dataframe["close"] < dataframe["ema_200"]).astype(int)

        confirm_s += (dataframe["stochrsi_k"] > 85).astype(int)
        confirm_s += (dataframe["cci"] > 150).astype(int)
        confirm_s += (dataframe["rvol"] > 1.5).astype(int)

        dataframe.loc[short_base & (confirm_s >= self.confirm_min.value), "enter_short"] = 1

        return dataframe

    # ═══════════════════════════════════════════════════════════════════
    # EXIT — Quick out when reversion happens
    # ═══════════════════════════════════════════════════════════════════

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # Exit long: RSI recovered to neutral + OF delta flipped
        dataframe.loc[
            (dataframe["rsi"] > self.exit_rsi_long.value)
            & (dataframe["of_delta_zscore"] > self.exit_dz_reversal.value)
            & (dataframe["volume"] > 0),
            "exit_long"
        ] = 1

        # Exit short: RSI fell to neutral + OF delta flipped negative
        dataframe.loc[
            (dataframe["rsi"] < self.exit_rsi_short.value)
            & (dataframe["of_delta_zscore"] < -self.exit_dz_reversal.value)
            & (dataframe["volume"] > 0),
            "exit_short"
        ] = 1

        return dataframe

    # ═══════════════════════════════════════════════════════════════════
    # CUSTOM STOPLOSS — ATR-based, tight for mean reversion
    # ═══════════════════════════════════════════════════════════════════

    def custom_stoploss(self, pair, trade: Trade, current_time,
                        current_rate, current_profit, after_fill, **kwargs) -> float:
        return -1  # Disabled — using static stoploss

    # ═══════════════════════════════════════════════════════════════════
    # CUSTOM EXIT — max hold 2h for mean reversion
    # ═══════════════════════════════════════════════════════════════════

    def custom_exit(self, pair, trade, current_time, current_rate, current_profit, **kwargs):
        if not trade.open_date_utc:
            return None
        hours = (current_time - trade.open_date_utc).total_seconds() / 3600

        # Max hold: 4 hours — mean reversion should resolve by then
        if hours >= 4:
            if current_profit > 0:
                return "mr_profit_4h"
            return "mr_timeout_4h"

        return None

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