# source: https://raw.githubusercontent.com/DOUGLASGUEDESATRIA/OSIRIS_TRADE/fe3c09567f3798d1cad6d7a306fa4f6131d91e17/user_data/strategies/OsirisGoldV2.py
"""
Github_DOUGLASGUEDESATRIA_OSIRIS_TRADE__OsirisGoldV2__20260329_060843 — High-Frequency Mean-Reversion Scalper (BTC/USDT 5m)
====================================================================
TARGET: 10+ trades/day | 65%+ Win Rate | 2:1+ R:R

MATH:
  288 candles/day on 5m. Need ~10 entries = trigger ~3.5% of candles.
  RSI(7) crosses above 40 from below = ~5% of candles
  + 1h trend alignment = ~2.5%
  + candle color confirmation = ~1.8%
  Multiple patterns combined = ~3-5% trigger rate = 10-14 trades/day

PATTERNS:
  A) RSI_BOUNCE: RSI(7) crosses 40 from below + green candle + 1h up
  B) EMA_TOUCH:  Price touches EMA(21) from above in uptrend + bounce
  C) BB_MEAN:    Close below BB middle, bounces back + volume
  D) RSI_SHORT:  RSI(7) crosses 60 from above + red candle + 1h down
  E) EMA_REJ:    Price touches EMA(21) from below in downtrend + rejected

RISK:
  SL = 0.35% (ATR-calibrated micro stop)
  TP = 0.70% (2:1 ratio)
  BE at 1x SL
  Max hold: 12 candles (1h)
"""
import logging
import numpy as np
import pandas as pd
from datetime import datetime
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__OsirisGoldV2__20260329_060843(IStrategy):
    INTERFACE_VERSION = 3
    can_short = True
    timeframe = "5m"

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

    # ── Risk params ──────────────────────────────────────────────────────────
    sl_pct = DecimalParameter(
        0.15, 0.60, default=0.35, decimals=2, space="sell", optimize=True
    )
    rr_ratio = DecimalParameter(
        1.5, 3.0, default=2.0, decimals=1, space="sell", optimize=True
    )
    max_hold = IntParameter(6, 24, default=12, space="sell", optimize=True)

    # ── Entry params ─────────────────────────────────────────────────────────
    rsi_bounce = IntParameter(30, 45, default=40, space="buy", optimize=True)
    rsi_reject = IntParameter(55, 70, default=60, space="buy", optimize=True)
    ema_period = IntParameter(15, 30, default=21, space="buy", optimize=True)
    ema_tol = DecimalParameter(
        0.05, 0.30, default=0.15, decimals=2, space="buy", optimize=True
    )
    cooldown = IntParameter(1, 4, default=2, space="buy", optimize=True)

    _last_entry_time: dict = {}

    # ── Informative pairs ────────────────────────────────────────────────────
    def informative_pairs(self):
        pairs = self.dp.current_whitelist() if self.dp else []
        return [(p, "1h") for p in pairs]

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

        # ── 5m ───────────────────────────────────────────────────────────────
        dataframe["rsi7"] = ta.RSI(dataframe, timeperiod=7)
        dataframe["rsi7_prev"] = dataframe["rsi7"].shift(1)
        dataframe["ema"] = ta.EMA(dataframe, timeperiod=ema_p)
        dataframe["ema_prev"] = dataframe["ema"].shift(1)

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

        dataframe["volume_sma"] = ta.SMA(dataframe["volume"], timeperiod=20)
        dataframe["is_green"] = (dataframe["close"] > dataframe["open"]).astype(int)
        dataframe["is_red"] = (dataframe["close"] < dataframe["open"]).astype(int)

        # Price relative to EMA (%)
        dataframe["ema_dist"] = (dataframe["close"] - dataframe["ema"]) / dataframe["ema"] * 100

        # ── 1h trend ─────────────────────────────────────────────────────────
        inf_1h = self.dp.get_pair_dataframe(pair=pair, timeframe="1h")
        if not inf_1h.empty:
            inf_1h["ema50_1h"] = ta.EMA(inf_1h, timeperiod=50)
            inf_1h["ema20_1h"] = ta.EMA(inf_1h, timeperiod=20)
            m1h = merge_informative_pair(
                dataframe,
                inf_1h[["date", "ema50_1h", "ema20_1h", "close"]],
                self.timeframe, "1h", ffill=True,
            )
            dataframe["ema50_1h"] = m1h["ema50_1h_1h"].values
            dataframe["ema20_1h"] = m1h["ema20_1h_1h"].values
            dataframe["close_1h"] = m1h["close_1h"].values
        else:
            dataframe["ema50_1h"] = dataframe["close"]
            dataframe["ema20_1h"] = dataframe["close"]
            dataframe["close_1h"] = dataframe["close"]

        # 1h trend signals
        dataframe["trend_up"] = (
            (dataframe["close_1h"] > dataframe["ema50_1h"]) &
            (dataframe["ema20_1h"] > dataframe["ema50_1h"])
        ).astype(int)
        dataframe["trend_dn"] = (
            (dataframe["close_1h"] < dataframe["ema50_1h"]) &
            (dataframe["ema20_1h"] < dataframe["ema50_1h"])
        ).astype(int)

        return dataframe

    # ── Entry signals ────────────────────────────────────────────────────────
    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        bounce = int(self.rsi_bounce.value)
        reject = int(self.rsi_reject.value)
        tol = float(self.ema_tol.value)

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

        # ═════════════════════════════════════════════════════════════════════
        # LONG A: RSI(7) bounce from oversold + green + uptrend
        # RSI was below threshold, now crossing back above
        # ═════════════════════════════════════════════════════════════════════
        long_rsi = (
            has_data
            & (dataframe["trend_up"] == 1)
            & (dataframe["rsi7_prev"] < bounce)
            & (dataframe["rsi7"] >= bounce)
            & (dataframe["is_green"] == 1)
        )

        # ═════════════════════════════════════════════════════════════════════
        # LONG B: EMA touch (pullback to EMA in uptrend)
        # Price dipped close to EMA and bounced
        # ═════════════════════════════════════════════════════════════════════
        long_ema = (
            has_data
            & (dataframe["trend_up"] == 1)
            & (dataframe["low"] <= dataframe["ema"] * (1 + tol / 100))
            & (dataframe["close"] > dataframe["ema"])
            & (dataframe["is_green"] == 1)
            & (dataframe["rsi7"] > 35)
            & (dataframe["rsi7"] < 55)
        )

        # ═════════════════════════════════════════════════════════════════════
        # LONG C: BB middle bounce (mean reversion in uptrend)
        # Price was below BB middle, closing above
        # ═════════════════════════════════════════════════════════════════════
        long_bb = (
            has_data
            & (dataframe["trend_up"] == 1)
            & (dataframe["low"] <= dataframe["bb_mid"])
            & (dataframe["close"] > dataframe["bb_mid"])
            & (dataframe["is_green"] == 1)
            & (dataframe["volume"] > dataframe["volume_sma"] * 0.8)
        )

        # ═════════════════════════════════════════════════════════════════════
        # SHORT D: RSI(7) rejection from overbought + red + downtrend
        # ═════════════════════════════════════════════════════════════════════
        short_rsi = (
            has_data
            & (dataframe["trend_dn"] == 1)
            & (dataframe["rsi7_prev"] > reject)
            & (dataframe["rsi7"] <= reject)
            & (dataframe["is_red"] == 1)
        )

        # ═════════════════════════════════════════════════════════════════════
        # SHORT E: EMA rejection (bounce off EMA from below in downtrend)
        # ═════════════════════════════════════════════════════════════════════
        short_ema = (
            has_data
            & (dataframe["trend_dn"] == 1)
            & (dataframe["high"] >= dataframe["ema"] * (1 - tol / 100))
            & (dataframe["close"] < dataframe["ema"])
            & (dataframe["is_red"] == 1)
            & (dataframe["rsi7"] > 45)
            & (dataframe["rsi7"] < 65)
        )

        # ═════════════════════════════════════════════════════════════════════
        # COMBINE with tags
        # ═════════════════════════════════════════════════════════════════════
        dataframe.loc[long_rsi, ["enter_long", "enter_tag"]] = [1, "rsi_bounce"]
        dataframe.loc[long_ema & ~long_rsi, ["enter_long", "enter_tag"]] = [1, "ema_touch"]
        dataframe.loc[long_bb & ~long_rsi & ~long_ema, ["enter_long", "enter_tag"]] = [1, "bb_mean"]
        dataframe.loc[short_rsi, ["enter_short", "enter_tag"]] = [1, "rsi_reject"]
        dataframe.loc[short_ema & ~short_rsi, ["enter_short", "enter_tag"]] = [1, "ema_reject"]

        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:
        last = self._last_entry_time.get(pair)
        cd_seconds = int(self.cooldown.value) * 300
        if last is not None:
            if (current_time - last).total_seconds() < cd_seconds:
                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
        # Breakeven at 1x SL profit
        if current_profit >= sl:
            is_short = getattr(trade, "is_short", False)
            return stoploss_from_open(0.001, current_profit, is_short=is_short)
        return -sl

    def custom_exit(
        self, pair, trade, current_time, current_rate, current_profit, **kwargs,
    ):
        sl = float(self.sl_pct.value) / 100
        tp = sl * float(self.rr_ratio.value)
        if current_profit >= tp:
            return "tp_hit"
        if trade.open_date_utc:
            elapsed = (current_time - trade.open_date_utc).total_seconds() / 300
            if elapsed >= 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
