# source: https://raw.githubusercontent.com/DOUGLASGUEDESATRIA/OSIRIS_TRADE/fe3c09567f3798d1cad6d7a306fa4f6131d91e17/user_data/strategies/OsirisScalper.py
"""
Github_DOUGLASGUEDESATRIA_OSIRIS_TRADE__OsirisScalper__20260329_060843 v6 — 1m Mean-Reversion Scalper
==============================================
Pure mean-reversion on 1m with BB bands.

LOGIC:
  Entry:   Price crosses below BB lower → long (mean reversion)
           Price crosses above BB upper → short (mean reversion)
  Filter:  RSI confirms extreme (oversold/overbought)
           5m EMA alignment as trend sanity check
  Exit:    Small fixed TP (0.2-0.5%) — quick scalps
           Tight SL (0.2-0.5%) — cut losses fast
           Breakeven when in profit
           Timeout: 15-30 candles max

WHY 1m MEAN REVERSION:
  - BB bounces on 1m resolve within 5-15 candles (5-15 min)
  - High frequency: 10-30 signals per day on BTC
  - Mean reversion has naturally higher WR than momentum
  - Small TP captures the reversion reliably
"""

import logging
from pandas import DataFrame

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

try:
    from freqtrade.strategy import stoploss_from_open
except ImportError:
    def stoploss_from_open(open_relative_stop, current_profit, is_short=False):
        if current_profit == 0:
            return 1
        if is_short:
            return -1 + ((1 - open_relative_stop) / (1 - current_profit))
        return 1 - ((1 + open_relative_stop) / (1 + current_profit))

logger = logging.getLogger(__name__)


class Github_DOUGLASGUEDESATRIA_OSIRIS_TRADE__OsirisScalper__20260329_060843(IStrategy):
    INTERFACE_VERSION = 3
    can_short = True
    timeframe = "1m"

    stoploss = -0.03
    minimal_roi = {"0": 100}
    trailing_stop = False
    use_custom_stoploss = True
    startup_candle_count = 200
    process_only_new_candles = True

    # ── Risk params (sell) ───────────────────────────────────────────────
    sl_pct = DecimalParameter(
        0.15, 0.60, default=0.35, decimals=2, space="sell", optimize=True
    )
    tp_pct = DecimalParameter(
        0.15, 0.80, default=0.30, decimals=2, space="sell", optimize=True
    )
    be_trigger = DecimalParameter(
        0.10, 0.40, default=0.15, decimals=2, space="sell", optimize=True
    )
    max_hold = IntParameter(5, 45, default=20, space="sell", optimize=True)

    # ── Entry thresholds (buy) ───────────────────────────────────────────
    rsi_os = IntParameter(15, 40, default=30, space="buy", optimize=True)
    rsi_ob = IntParameter(60, 85, default=70, space="buy", optimize=True)
    bb_std = DecimalParameter(
        1.5, 2.8, default=2.0, decimals=1, space="buy", optimize=True
    )

    _last_entry_time: dict = {}

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

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

        # 1m indicators
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)

        # Precompute BB for multiple std dev values
        for std in [1.5, 2.0, 2.5]:
            s = int(std * 10)
            bb = ta.BBANDS(dataframe, timeperiod=20, nbdevup=std, nbdevdn=std)
            dataframe[f"bb_upper_{s}"] = bb["upperband"]
            dataframe[f"bb_mid_{s}"] = bb["middleband"]
            dataframe[f"bb_lower_{s}"] = bb["lowerband"]

        # 5m trend filter (informative)
        inf_5m = self.dp.get_pair_dataframe(pair=pair, timeframe="5m")
        if not inf_5m.empty:
            inf_5m["ema8_5m"] = ta.EMA(inf_5m, timeperiod=8)
            inf_5m["ema21_5m"] = ta.EMA(inf_5m, timeperiod=21)
            merged = merge_informative_pair(
                dataframe,
                inf_5m[["date", "ema8_5m", "ema21_5m"]],
                self.timeframe,
                "5m",
                ffill=True,
            )
            dataframe["ema8_5m"] = merged["ema8_5m_5m"].values
            dataframe["ema21_5m"] = merged["ema21_5m_5m"].values
        else:
            dataframe["ema8_5m"] = dataframe["close"]
            dataframe["ema21_5m"] = dataframe["close"]

        return dataframe

    # ── Entry signals ────────────────────────────────────────────────────
    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        os_val = int(self.rsi_os.value)
        ob_val = int(self.rsi_ob.value)

        # Map bb_std to nearest precomputed column
        std_val = float(self.bb_std.value)
        std_key = min([15, 20, 25], key=lambda x: abs(x - int(std_val * 10)))

        bb_upper = dataframe[f"bb_upper_{std_key}"]
        bb_lower = dataframe[f"bb_lower_{std_key}"]
        bb_mid = dataframe[f"bb_mid_{std_key}"]

        has_data = dataframe["rsi"].notna() & (dataframe["volume"] > 0)

        # 5m trend (optional alignment - gives higher WR when active)
        trend_bull_5m = dataframe["ema8_5m"] > dataframe["ema21_5m"]
        trend_bear_5m = dataframe["ema8_5m"] < dataframe["ema21_5m"]

        # ═══════════════════════════════════════════════════════════════════
        # LONG: Price at/below BB lower + RSI oversold = mean reversion up
        # ═══════════════════════════════════════════════════════════════════
        long_bb = (
            has_data
            & (dataframe["close"] <= bb_lower * 1.001)
            & (dataframe["rsi"] < os_val)
        )

        # LONG with 5m trend alignment (higher quality)
        long_trend = (
            has_data & trend_bull_5m
            & (dataframe["close"] <= bb_mid)
            & (dataframe["rsi"] < os_val + 10)
            & (dataframe["close"] > dataframe["close"].shift(1))  # bouncing
        )

        # ═══════════════════════════════════════════════════════════════════
        # SHORT: Price at/above BB upper + RSI overbought = mean reversion down
        # ═══════════════════════════════════════════════════════════════════
        short_bb = (
            has_data
            & (dataframe["close"] >= bb_upper * 0.999)
            & (dataframe["rsi"] > ob_val)
        )

        # SHORT with 5m trend alignment (higher quality)
        short_trend = (
            has_data & trend_bear_5m
            & (dataframe["close"] >= bb_mid)
            & (dataframe["rsi"] > ob_val - 10)
            & (dataframe["close"] < dataframe["close"].shift(1))  # falling
        )

        dataframe.loc[long_bb | long_trend, "enter_long"] = 1
        dataframe.loc[short_bb | short_trend, "enter_short"] = 1
        return dataframe

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

    # ── Trade management ─────────────────────────────────────────────────
    def confirm_trade_entry(
        self, pair, order_type, amount, rate, time_in_force,
        current_time, entry_tag, side, **kwargs,
    ) -> bool:
        # Min 2 min cooldown between trades per pair
        last = self._last_entry_time.get(pair)
        if last is not None:
            if (current_time - last).total_seconds() < 120:
                return False
        self._last_entry_time[pair] = current_time
        return True

    def custom_stoploss(
        self, pair, trade: Trade, current_time,
        current_rate: float, current_profit: float, **kwargs,
    ) -> float:
        sl = float(self.sl_pct.value) / 100
        be = float(self.be_trigger.value) / 100
        is_short = getattr(trade, "is_short", False)

        # Breakeven when profit >= be_trigger
        if current_profit >= be:
            return stoploss_from_open(0.0001, current_profit, is_short=is_short)

        return -sl

    def custom_exit(
        self, pair, trade, current_time, current_rate, current_profit, **kwargs,
    ):
        tp = float(self.tp_pct.value) / 100

        # Take profit
        if current_profit >= tp:
            return "tp_hit"

        # Timeout
        if trade.open_date_utc:
            candles = (current_time - trade.open_date_utc).total_seconds() / 60
            if candles >= self.max_hold.value:
                return "timeout"
        return None

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