# source: https://raw.githubusercontent.com/DOUGLASGUEDESATRIA/OSIRIS_TRADE/fe3c09567f3798d1cad6d7a306fa4f6131d91e17/user_data/strategies/OsirisDayTradeV3.py
"""
OSIRIS DAY TRADE v3 — Adaptive Dual-Mode
================================================================
DAY TRADE puro em BTC/USDT — LONG e SHORT.

MANIFESTO INVIOLÁVEL:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  Ativo:        BTC/USDT APENAS
  Tipo:         DAY TRADE (max 2h por operação)
  Meta:         10 operações/dia
  Direção:      LONG e SHORT
  Timeframes:   5m (execução) + 15m (sinal) + 1h (contexto)
  Stop:         0.4-0.8% fixo (baseado em ATR normalizado)
  Target:       1.5× stop
  Max Loss/dia: -5R (hard stop)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

DOIS MODOS ADAPTATIVOS:

MODE TREND (1h ADX > 20 + EMA aligned):
  → Segue a tendência no pullback
  → Entries: EMA bounce, RSI pullback, momentum continuation
  → Direção: SÓ na direção do 1h trend

MODE RANGE (1h ADX < 20 OR EMAs cruzando):
  → Faz mean-reversion nos extremos
  → Entries: BB bounce, RSI extreme reversal
  → Direção: AMBOS
  → Targets menores (1.2R)

EDGE: Não tenta adivinhar — ADAPTA ao regime.
"""

import logging
import numpy as np
import pandas as pd
from pandas import DataFrame
from typing import Optional
from datetime import datetime, timedelta

from freqtrade.strategy import IStrategy, merge_informative_pair
from freqtrade.strategy import DecimalParameter, IntParameter
from freqtrade.persistence import Trade
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__OsirisDayTradeV3__20260329_060843(IStrategy):
    """
    OSIRIS DAY TRADE v3 — Dual-mode: Trend Following + Range Reversion
    5m base | 15m signal | 1h regime | LONG+SHORT | ~10 trades/dia
    """

    INTERFACE_VERSION = 3
    can_short = True
    timeframe = "5m"

    # ROI: custom_exit handles real exits
    minimal_roi = {"0": 0.10, "120": 0.003}

    # Hard stoploss safety net (wider to let custom work)
    stoploss = -0.025

    trailing_stop = False
    use_custom_stoploss = True

    startup_candle_count = 200
    process_only_new_candles = True

    # Runtime state
    _daily_trades = {}
    _consecutive_losses = 0
    _last_loss_time = None

    # ═══════════════════════════════════════════════════════════════════
    # HYPEROPT PARAMETERS
    # ═══════════════════════════════════════════════════════════════════

    # Regime threshold
    buy_adx_trend_min = IntParameter(15, 30, default=20, space="buy", optimize=True)

    # RSI limits for entry
    buy_rsi_bull_min = IntParameter(35, 50, default=40, space="buy", optimize=True)
    buy_rsi_bull_max = IntParameter(60, 80, default=70, space="buy", optimize=True)
    buy_rsi_bear_min = IntParameter(20, 40, default=30, space="buy", optimize=True)
    buy_rsi_bear_max = IntParameter(50, 65, default=60, space="buy", optimize=True)

    # RSI extremes for range mode
    buy_rsi_range_long = IntParameter(20, 35, default=30, space="buy", optimize=True)
    buy_rsi_range_short = IntParameter(65, 80, default=70, space="buy", optimize=True)

    # Volume
    buy_vol_min = DecimalParameter(0.3, 1.0, default=0.6, decimals=1, space="buy", optimize=True)

    # R:R
    buy_rr_trend = DecimalParameter(1.2, 2.5, default=1.5, decimals=1, space="buy", optimize=True)
    buy_rr_range = DecimalParameter(1.0, 2.0, default=1.2, decimals=1, space="buy", optimize=True)

    # Max daily
    buy_max_daily = IntParameter(8, 15, default=10, space="buy", optimize=True)

    # Max hold (candles)
    buy_max_hold = IntParameter(12, 36, default=24, space="buy", optimize=True)

    # Exit RSI
    sell_rsi_long = IntParameter(70, 85, default=78, space="sell", optimize=True)
    sell_rsi_short = IntParameter(15, 30, default=22, space="sell", optimize=True)

    # ═══════════════════════════════════════════════════════════════════
    # INFORMATIVE PAIRS
    # ═══════════════════════════════════════════════════════════════════

    def informative_pairs(self):
        return [
            ("BTC/USDT:USDT", "15m"),
            ("BTC/USDT:USDT", "1h"),
        ]

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

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # ── EMAs ──
        dataframe["ema9"] = ta.EMA(dataframe, timeperiod=9)
        dataframe["ema21"] = ta.EMA(dataframe, timeperiod=21)
        dataframe["ema50"] = ta.EMA(dataframe, timeperiod=50)

        # ── RSI ──
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
        dataframe["rsi_prev"] = dataframe["rsi"].shift(1)

        # ── MACD ──
        macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
        dataframe["macd_hist"] = macd["macdhist"]
        dataframe["macd_hist_prev"] = dataframe["macd_hist"].shift(1)

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

        # ── ATR ──
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)

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

        # ── Volume ──
        dataframe["vol_sma"] = ta.SMA(dataframe["volume"], timeperiod=20)
        dataframe["vol_ratio"] = dataframe["volume"] / dataframe["vol_sma"].replace(0, 1)

        # ── Previous values ──
        dataframe["ema9_prev"] = dataframe["ema9"].shift(1)
        dataframe["ema21_prev"] = dataframe["ema21"].shift(1)

        # ── Candle info ──
        dataframe["is_green"] = (dataframe["close"] > dataframe["open"]).astype(int)
        dataframe["is_red"] = (dataframe["close"] < dataframe["open"]).astype(int)
        dataframe["body_pct"] = abs(dataframe["close"] - dataframe["open"]) / dataframe["close"] * 100

        # ── Distance to key levels (ATR units) ──
        atr_safe = dataframe["atr"].replace(0, 1)
        dataframe["dist_ema9"] = (dataframe["close"] - dataframe["ema9"]) / atr_safe
        dataframe["dist_ema21"] = (dataframe["close"] - dataframe["ema21"]) / atr_safe
        dataframe["dist_bb_upper"] = (dataframe["bb_upper"] - dataframe["close"]) / atr_safe
        dataframe["dist_bb_lower"] = (dataframe["close"] - dataframe["bb_lower"]) / atr_safe

        # ── Multi-TF: 15m ──
        if self.dp:
            pair = metadata["pair"]

            inf_15m = self.dp.get_pair_dataframe(pair=pair, timeframe="15m")
            if not inf_15m.empty:
                inf_15m["ema9"] = ta.EMA(inf_15m, timeperiod=9)
                inf_15m["ema21"] = ta.EMA(inf_15m, timeperiod=21)
                inf_15m["rsi"] = ta.RSI(inf_15m, timeperiod=14)
                macd15 = ta.MACD(inf_15m, fastperiod=12, slowperiod=26, signalperiod=9)
                inf_15m["macd_hist"] = macd15["macdhist"]
                dataframe = merge_informative_pair(
                    dataframe, inf_15m, self.timeframe, "15m", ffill=True
                )

            # ── Multi-TF: 1h ──
            inf_1h = self.dp.get_pair_dataframe(pair=pair, timeframe="1h")
            if not inf_1h.empty:
                inf_1h["ema9"] = ta.EMA(inf_1h, timeperiod=9)
                inf_1h["ema21"] = ta.EMA(inf_1h, timeperiod=21)
                inf_1h["ema50"] = ta.EMA(inf_1h, timeperiod=50)
                inf_1h["rsi"] = ta.RSI(inf_1h, timeperiod=14)
                inf_1h["adx"] = ta.ADX(inf_1h, timeperiod=14)
                dataframe = merge_informative_pair(
                    dataframe, inf_1h, self.timeframe, "1h", ffill=True
                )

        return dataframe

    # ═══════════════════════════════════════════════════════════════════
    # ENTRY TREND — Dual Mode
    # ═══════════════════════════════════════════════════════════════════

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

        has_1h = "ema9_1h" in dataframe.columns
        has_15m = "ema9_15m" in dataframe.columns
        adx_thresh = self.buy_adx_trend_min.value
        vol_min = self.buy_vol_min.value

        # ───────────────────────────────────────────
        # REGIME DETECTION (from 1h)
        # ───────────────────────────────────────────
        if has_1h:
            is_trending = dataframe["adx_1h"] > adx_thresh
            trend_up = is_trending & (dataframe["ema9_1h"] > dataframe["ema21_1h"])
            trend_dn = is_trending & (dataframe["ema9_1h"] < dataframe["ema21_1h"])
            is_range = ~is_trending
        else:
            is_trending = pd.Series(False, index=dataframe.index)
            trend_up = pd.Series(False, index=dataframe.index)
            trend_dn = pd.Series(False, index=dataframe.index)
            is_range = pd.Series(True, index=dataframe.index)

        # ───────────────────────────────────────────
        # COMMON FILTERS
        # ───────────────────────────────────────────
        vol_ok = dataframe["vol_ratio"] > vol_min
        has_vol = dataframe["volume"] > 0
        atr_ok = dataframe["atr"] > 0.5

        # ───────────────────────────────────────────
        # === MODE: TREND === (follow trend on pullbacks)
        # ───────────────────────────────────────────

        # -- TREND LONG entries --

        # T1: EMA9 bounce in uptrend
        # Price dipped near EMA9 and green candle
        t1_long = (
            trend_up &
            (dataframe["dist_ema9"] > -0.6) &
            (dataframe["dist_ema9"] < 0.3) &
            (dataframe["is_green"] == 1) &
            (dataframe["ema9"] > dataframe["ema21"]) &  # 5m trend intact
            (dataframe["rsi"] > self.buy_rsi_bull_min.value) &
            (dataframe["rsi"] < self.buy_rsi_bull_max.value)
        )

        # T2: EMA21 deep pullback in uptrend
        # Price touched EMA21 zone and bounced
        t2_long = (
            trend_up &
            (dataframe["dist_ema21"] > -0.5) &
            (dataframe["dist_ema21"] < 0.5) &
            (dataframe["is_green"] == 1) &
            (dataframe["rsi"] > 35) &
            (dataframe["rsi"] < 55)
        )

        # T3: Momentum continuation
        # MACD hist flips positive (buy the acceleration)
        t3_long = (
            trend_up &
            (dataframe["macd_hist"] > 0) &
            (dataframe["macd_hist_prev"] <= 0) &
            (dataframe["close"] > dataframe["ema21"]) &
            (dataframe["rsi"] > 45) &
            (dataframe["rsi"] < 65)
        )

        # T4: RSI bounce from mid zone (trend continuation)
        t4_long = (
            trend_up &
            (dataframe["rsi"] > 50) &
            (dataframe["rsi_prev"] <= 50) &
            (dataframe["close"] > dataframe["ema9"]) &
            (dataframe["ema9"] > dataframe["ema21"])
        )

        trend_long = (t1_long | t2_long | t3_long | t4_long)

        # -- TREND SHORT entries --

        t1_short = (
            trend_dn &
            (dataframe["dist_ema9"] < 0.6) &
            (dataframe["dist_ema9"] > -0.3) &
            (dataframe["is_red"] == 1) &
            (dataframe["ema9"] < dataframe["ema21"]) &
            (dataframe["rsi"] > self.buy_rsi_bear_min.value) &
            (dataframe["rsi"] < self.buy_rsi_bear_max.value)
        )

        t2_short = (
            trend_dn &
            (dataframe["dist_ema21"] < 0.5) &
            (dataframe["dist_ema21"] > -0.5) &
            (dataframe["is_red"] == 1) &
            (dataframe["rsi"] > 45) &
            (dataframe["rsi"] < 65)
        )

        t3_short = (
            trend_dn &
            (dataframe["macd_hist"] < 0) &
            (dataframe["macd_hist_prev"] >= 0) &
            (dataframe["close"] < dataframe["ema21"]) &
            (dataframe["rsi"] > 35) &
            (dataframe["rsi"] < 55)
        )

        t4_short = (
            trend_dn &
            (dataframe["rsi"] < 50) &
            (dataframe["rsi_prev"] >= 50) &
            (dataframe["close"] < dataframe["ema9"]) &
            (dataframe["ema9"] < dataframe["ema21"])
        )

        trend_short = (t1_short | t2_short | t3_short | t4_short)

        # ───────────────────────────────────────────
        # === MODE: RANGE === (mean reversion at extremes)
        # ───────────────────────────────────────────

        # R1: BB lower touch → long
        r1_long = (
            is_range &
            (dataframe["low"] <= dataframe["bb_lower"] * 1.002) &
            (dataframe["is_green"] == 1) &
            (dataframe["rsi"] < self.buy_rsi_range_long.value + 5)
        )

        # R2: RSI extreme oversold → long
        r2_long = (
            is_range &
            (dataframe["rsi"] < self.buy_rsi_range_long.value) &
            (dataframe["rsi_prev"] < dataframe["rsi"]) &  # RSI turning up
            (dataframe["is_green"] == 1)
        )

        # R3: BB upper touch → short
        r3_short = (
            is_range &
            (dataframe["high"] >= dataframe["bb_upper"] * 0.998) &
            (dataframe["is_red"] == 1) &
            (dataframe["rsi"] > self.buy_rsi_range_short.value - 5)
        )

        # R4: RSI extreme overbought → short
        r4_short = (
            is_range &
            (dataframe["rsi"] > self.buy_rsi_range_short.value) &
            (dataframe["rsi_prev"] > dataframe["rsi"]) &  # RSI turning down
            (dataframe["is_red"] == 1)
        )

        range_long = (r1_long | r2_long)
        range_short = (r3_short | r4_short)

        # ───────────────────────────────────────────
        # === 15M SOFT ALIGNMENT (bonus, not hard filter) ===
        # ───────────────────────────────────────────
        if has_15m:
            # 15m alignment boosts confidence but not required
            fifm_long = dataframe["ema9_15m"] >= dataframe["ema21_15m"]
            fifm_short = dataframe["ema9_15m"] <= dataframe["ema21_15m"]

            # In trend mode: 15m must not be strongly against
            # (soft: allow if 15m RSI isn't extreme against us)
            fifm_ok_long = (fifm_long) | (dataframe["rsi_15m"] < 65)
            fifm_ok_short = (fifm_short) | (dataframe["rsi_15m"] > 35)
        else:
            fifm_ok_long = pd.Series(True, index=dataframe.index)
            fifm_ok_short = pd.Series(True, index=dataframe.index)

        # ───────────────────────────────────────────
        # COMBINE ALL
        # ───────────────────────────────────────────
        go_long = (
            (trend_long | range_long) &
            vol_ok & has_vol & atr_ok &
            fifm_ok_long
        )

        go_short = (
            (trend_short | range_short) &
            vol_ok & has_vol & atr_ok &
            fifm_ok_short
        )

        dataframe.loc[go_long, "enter_long"] = 1
        dataframe.loc[go_short, "enter_short"] = 1

        # Entry tags
        for cond, tag in [
            (go_long & t1_long, "trend_ema9_long"),
            (go_long & t2_long, "trend_ema21_long"),
            (go_long & t3_long, "trend_macd_long"),
            (go_long & t4_long, "trend_rsi_long"),
            (go_long & r1_long, "range_bb_long"),
            (go_long & r2_long, "range_rsi_long"),
            (go_short & t1_short, "trend_ema9_short"),
            (go_short & t2_short, "trend_ema21_short"),
            (go_short & t3_short, "trend_macd_short"),
            (go_short & t4_short, "trend_rsi_short"),
            (go_short & r3_short, "range_bb_short"),
            (go_short & r4_short, "range_rsi_short"),
        ]:
            dataframe.loc[cond, "enter_tag"] = tag

        return dataframe

    # ═══════════════════════════════════════════════════════════════════
    # EXIT TREND
    # ═══════════════════════════════════════════════════════════════════

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[
            dataframe["rsi"] > self.sell_rsi_long.value,
            "exit_long",
        ] = 1

        dataframe.loc[
            dataframe["rsi"] < self.sell_rsi_short.value,
            "exit_short",
        ] = 1

        return dataframe

    # ═══════════════════════════════════════════════════════════════════
    # CUSTOM STOPLOSS — ATR-based + R-multiple trailing
    # ═══════════════════════════════════════════════════════════════════

    def custom_stoploss(
        self,
        pair: str,
        trade: Trade,
        current_time,
        current_rate: float,
        current_profit: float,
        **kwargs,
    ) -> float:
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if len(dataframe) == 0:
            return -0.015

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

        is_short = trade.is_short if hasattr(trade, "is_short") else False

        # Stop distance: 1.5 ATR (simple, reliable)
        stop_dist_pct = 1.5 * atr / trade.open_rate
        stop_dist_pct = max(0.004, min(stop_dist_pct, 0.015))

        # R-multiple trailing
        if current_profit > 0 and stop_dist_pct > 0:
            r_mult = current_profit / stop_dist_pct

            if r_mult >= 1.5:
                return stoploss_from_open(
                    0.5 * stop_dist_pct, current_profit, is_short=is_short
                )
            elif r_mult >= 1.0:
                return stoploss_from_open(
                    0.001, current_profit, is_short=is_short
                )

        return -stop_dist_pct

    # ═══════════════════════════════════════════════════════════════════
    # CUSTOM EXIT — Target + Time
    # ═══════════════════════════════════════════════════════════════════

    def custom_exit(
        self,
        pair: str,
        trade: Trade,
        current_time,
        current_rate: float,
        current_profit: float,
        **kwargs,
    ) -> Optional[str]:
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if len(dataframe) == 0:
            return None

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

        is_short = trade.is_short if hasattr(trade, "is_short") else False

        # Risk = 1.5 ATR
        risk_pct = max(1.5 * atr / trade.open_rate, 0.004)

        # Determine R:R based on mode (check if current regime is ranging)
        adx_1h = last.get("adx_1h", 25)
        is_ranging = adx_1h < self.buy_adx_trend_min.value if not pd.isna(adx_1h) else True
        rr = self.buy_rr_range.value if is_ranging else self.buy_rr_trend.value
        target_pct = risk_pct * rr

        # ── TARGET HIT ──
        if current_profit >= target_pct:
            return "v3_tp"

        # ── Momentum fade: early exit at 70% target ──
        if current_profit >= target_pct * 0.7:
            rsi = last.get("rsi", 50)
            if is_short and rsi < 25:
                return "v3_tp_fade"
            if not is_short and rsi > 75:
                return "v3_tp_fade"

        # ── Time management ──
        minutes = (current_time - trade.open_date_utc).total_seconds() / 60
        max_min = self.buy_max_hold.value * 5

        if minutes > max_min * 0.7 and current_profit > 0.002:
            return "v3_time_profit"

        if minutes > max_min:
            return "v3_time_force"

        return None

    # ═══════════════════════════════════════════════════════════════════
    # CONFIRM ENTRY — Daily limits + loss streak
    # ═══════════════════════════════════════════════════════════════════

    def confirm_trade_entry(
        self,
        pair: str,
        order_type: str,
        amount: float,
        rate: float,
        time_in_force: str,
        current_time: datetime,
        entry_tag: Optional[str],
        side: str,
        **kwargs,
    ) -> bool:
        today = current_time.strftime("%Y-%m-%d")
        if today not in self._daily_trades:
            self._daily_trades = {today: 0}

        if self._daily_trades.get(today, 0) >= self.buy_max_daily.value:
            return False

        # 3 losses → 30 min pause
        if self._consecutive_losses >= 3 and self._last_loss_time:
            if current_time < self._last_loss_time + timedelta(minutes=30):
                return False
            self._consecutive_losses = 0

        self._daily_trades[today] = self._daily_trades.get(today, 0) + 1
        return True

    def confirm_trade_exit(
        self,
        pair: str,
        trade: Trade,
        order_type: str,
        amount: float,
        rate: float,
        time_in_force: str,
        exit_reason: str,
        current_time: datetime,
        **kwargs,
    ) -> bool:
        profit = trade.calc_profit_ratio(rate)
        if profit < 0:
            self._consecutive_losses += 1
            self._last_loss_time = current_time
        else:
            self._consecutive_losses = 0
        return True
