# source: https://raw.githubusercontent.com/DOUGLASGUEDESATRIA/OSIRIS_TRADE/fe3c09567f3798d1cad6d7a306fa4f6131d91e17/user_data/strategies/OsirisDayTradeV4.py
"""
OSIRIS DAY TRADE v4 — 15m Primary, Structural Momentum
================================================================
MANIFESTO INVIOLÁVEL:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  Ativo:       BTC/USDT FUTURES
  Tipo:        DAY TRADE (max 3h por operação)
  Meta:        5-10 operações/dia
  Timeframe:   15m (primário) + 1h (tendência)
  Stops:       LARGOS (2×ATR, ~1-2.5%) — sobrevive ao ruído
  Target:      1.5R (realista, alta fill rate)
  Direção:     LONG e SHORT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

DIAGNÓSTICO DOS v1-v3:
  - 5m: stops de 0.4-1.5% são MENORES que o wick médio do BTC
  - Resultado: 28% WR porque TODA trade é stopada por ruído
  - Não é problema de sinal — é problema de TIMEFRAME + STOP

FIX:
  - 15m base: sinais mais limpos, ATR ~400-800 USD
  - Stop 2×ATR = 1-2.5% tipicamente → sobrevive wicks normais
  - Target 3×ATR = 1.5-3.75% → alcançável em 1-3 horas
  - Menos trades (5-8/dia) mas com EDGE REAL

ENTRIES (baseados em 15m, filtrados pelo 1h):
  1. RSI Momentum Cross: RSI cruza 50 no 15m (momentum shift)
  2. MACD Hist Flip: MACD hist muda de sinal (acceleration)
  3. BB Reversion: Toca BB extrema + vela de reversão
  4. EMA Pullback: Retorno à EMA21 no 15m em tendência ativa
  5. Breakout: Novo high/low de 20 bars com volume alto

1H FILTER:
  - TREND (ADX>20 + EMA aligned): só trade na direção
  - RANGE (ADX<20): BB bounce em ambas direções
"""

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__OsirisDayTradeV4__20260329_060843(IStrategy):
    """
    OSIRIS DAY TRADE v4 — 15m Primary, structural stops, momentum entries.
    Key insight: wider stops survive BTC volatility → higher WR → profit.
    """

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

    # Wide ROI — let custom_exit handle properly
    minimal_roi = {"0": 0.10, "180": 0.005}

    # Safety stoploss: wider than custom to avoid interference
    stoploss = -0.035

    trailing_stop = False
    use_custom_stoploss = True

    startup_candle_count = 200
    process_only_new_candles = True

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

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

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

    # Stop multiplier (ATR units)
    buy_sl_atr_mult = DecimalParameter(1.5, 3.0, default=2.0, decimals=1, space="buy", optimize=True)

    # R:R target
    buy_rr = DecimalParameter(1.2, 2.5, default=1.5, decimals=1, space="buy", optimize=True)

    # RSI thresholds
    buy_rsi_cross = IntParameter(45, 55, default=50, space="buy", optimize=True)

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

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

    # Max hold (15m candles: 12 = 3h, 16 = 4h)
    buy_max_hold = IntParameter(8, 24, default=12, space="buy", optimize=True)

    # Breakout lookback
    buy_breakout_bars = IntParameter(10, 30, default=20, space="buy", optimize=True)

    # Exit RSI
    sell_rsi_long = IntParameter(72, 88, default=80, space="sell", optimize=True)
    sell_rsi_short = IntParameter(12, 28, default=20, space="sell", optimize=True)

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

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

    # ═══════════════════════════════════════════════════════════════════
    # INDICATORS (all on 15m)
    # ═══════════════════════════════════════════════════════════════════

    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"]

        # ── ATR (the KEY metric for stops) ──
        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)

        # ── Donchian channel for breakouts ──
        bk = self.buy_breakout_bars.value
        dataframe["dc_high"] = dataframe["high"].rolling(bk).max().shift(1)
        dataframe["dc_low"] = dataframe["low"].rolling(bk).min().shift(1)

        # ── Previous values for cross detection ──
        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)

        # ── Distance to EMA21 in ATR units ──
        atr_safe = dataframe["atr"].replace(0, 1)
        dataframe["dist_ema21"] = (dataframe["close"] - dataframe["ema21"]) / atr_safe

        # ── 1H informative ──
        if self.dp:
            pair = metadata["pair"]
            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["adx"] = ta.ADX(inf_1h, timeperiod=14)
                inf_1h["rsi"] = ta.RSI(inf_1h, timeperiod=14)
                dataframe = merge_informative_pair(
                    dataframe, inf_1h, self.timeframe, "1h", ffill=True
                )

        return dataframe

    # ═══════════════════════════════════════════════════════════════════
    # ENTRY TREND
    # ═══════════════════════════════════════════════════════════════════

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

        has_1h = "ema9_1h" in dataframe.columns
        vol_min = self.buy_vol_min.value
        rsi_cross = self.buy_rsi_cross.value

        # ── 1H REGIME ──
        if has_1h:
            h1_trending = dataframe["adx_1h"] > self.buy_adx_trend.value
            h1_up = h1_trending & (dataframe["ema9_1h"] > dataframe["ema21_1h"])
            h1_dn = h1_trending & (dataframe["ema9_1h"] < dataframe["ema21_1h"])
            h1_range = ~h1_trending
        else:
            h1_up = pd.Series(False, index=dataframe.index)
            h1_dn = pd.Series(False, index=dataframe.index)
            h1_range = pd.Series(True, index=dataframe.index)

        # ── COMMON ──
        vol_ok = dataframe["vol_ratio"] > vol_min
        has_vol = dataframe["volume"] > 0
        atr_ok = dataframe["atr"] > 1  # at least $1 ATR on 15m

        # ═══════════════════════════════════════════════════════════════
        # ENTRY 1: RSI MOMENTUM CROSS (back-and-forth around centerline)
        # ═══════════════════════════════════════════════════════════════
        rsi_cross_long = (
            (dataframe["rsi"] > rsi_cross) &
            (dataframe["rsi_prev"] <= rsi_cross) &
            (dataframe["close"] > dataframe["ema21"])
        )
        rsi_cross_short = (
            (dataframe["rsi"] < (100 - rsi_cross)) &
            (dataframe["rsi_prev"] >= (100 - rsi_cross)) &
            (dataframe["close"] < dataframe["ema21"])
        )

        # ═══════════════════════════════════════════════════════════════
        # ENTRY 2: MACD HISTOGRAM FLIP (acceleration change)
        # ═══════════════════════════════════════════════════════════════
        macd_flip_long = (
            (dataframe["macd_hist"] > 0) &
            (dataframe["macd_hist_prev"] <= 0) &
            (dataframe["close"] > dataframe["ema21"])
        )
        macd_flip_short = (
            (dataframe["macd_hist"] < 0) &
            (dataframe["macd_hist_prev"] >= 0) &
            (dataframe["close"] < dataframe["ema21"])
        )

        # ═══════════════════════════════════════════════════════════════
        # ENTRY 3: BOLLINGER BAND REVERSION (range mode)
        # ═══════════════════════════════════════════════════════════════
        bb_long = (
            (dataframe["low"] <= dataframe["bb_lower"] * 1.003) &
            (dataframe["is_green"] == 1) &
            (dataframe["rsi"] < 35)
        )
        bb_short = (
            (dataframe["high"] >= dataframe["bb_upper"] * 0.997) &
            (dataframe["is_red"] == 1) &
            (dataframe["rsi"] > 65)
        )

        # ═══════════════════════════════════════════════════════════════
        # ENTRY 4: EMA21 PULLBACK (trend continuation)
        # Price returns to EMA21 zone in established trend
        # ═══════════════════════════════════════════════════════════════
        pullback_long = (
            (dataframe["dist_ema21"] > -0.5) &
            (dataframe["dist_ema21"] < 0.5) &
            (dataframe["is_green"] == 1) &
            (dataframe["ema9"] > dataframe["ema21"]) &
            (dataframe["rsi"] > 40) &
            (dataframe["rsi"] < 60)
        )
        pullback_short = (
            (dataframe["dist_ema21"] < 0.5) &
            (dataframe["dist_ema21"] > -0.5) &
            (dataframe["is_red"] == 1) &
            (dataframe["ema9"] < dataframe["ema21"]) &
            (dataframe["rsi"] > 40) &
            (dataframe["rsi"] < 60)
        )

        # ═══════════════════════════════════════════════════════════════
        # ENTRY 5: DONCHIAN BREAKOUT (new high/low with volume)
        # ═══════════════════════════════════════════════════════════════
        breakout_long = (
            (dataframe["close"] > dataframe["dc_high"]) &
            (dataframe["vol_ratio"] > 1.3)
        )
        breakout_short = (
            (dataframe["close"] < dataframe["dc_low"]) &
            (dataframe["vol_ratio"] > 1.3)
        )

        # ═══════════════════════════════════════════════════════════════
        # COMBINE WITH 1H FILTER
        # ═══════════════════════════════════════════════════════════════

        # Trend entries: MUST align with 1h direction
        trend_entries_long = (rsi_cross_long | macd_flip_long | pullback_long | breakout_long)
        trend_entries_short = (rsi_cross_short | macd_flip_short | pullback_short | breakout_short)

        # Range entries: allowed in both directions when 1h is ranging
        range_entries_long = bb_long
        range_entries_short = bb_short

        go_long = (
            (
                (h1_up & trend_entries_long) |   # trend mode: with 1h
                (h1_range & range_entries_long) |  # range mode: BB bounce
                (h1_range & (rsi_cross_long | macd_flip_long))  # range: momentum in range
            ) &
            vol_ok & has_vol & atr_ok
        )

        go_short = (
            (
                (h1_dn & trend_entries_short) |
                (h1_range & range_entries_short) |
                (h1_range & (rsi_cross_short | macd_flip_short))
            ) &
            vol_ok & has_vol & atr_ok
        )

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

        # Tags
        for cond, tag in [
            (go_long & rsi_cross_long, "rsi_cross_long"),
            (go_long & macd_flip_long, "macd_flip_long"),
            (go_long & bb_long, "bb_bounce_long"),
            (go_long & pullback_long, "pullback_long"),
            (go_long & breakout_long, "breakout_long"),
            (go_short & rsi_cross_short, "rsi_cross_short"),
            (go_short & macd_flip_short, "macd_flip_short"),
            (go_short & bb_short, "bb_bounce_short"),
            (go_short & pullback_short, "pullback_short"),
            (go_short & breakout_short, "breakout_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 — Wide 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.02

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

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

        # Stop: 2 ATR (on 15m this is typically 1-2.5%)
        sl_mult = self.buy_sl_atr_mult.value
        stop_dist_pct = sl_mult * atr / trade.open_rate
        stop_dist_pct = max(0.008, min(stop_dist_pct, 0.03))

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

            if r_mult >= 2.0:
                # Lock 1R
                return stoploss_from_open(
                    stop_dist_pct, current_profit, is_short=is_short
                )
            elif r_mult >= 1.5:
                # Lock 0.5R
                return stoploss_from_open(
                    0.5 * stop_dist_pct, current_profit, is_short=is_short
                )
            elif r_mult >= 1.0:
                # Breakeven
                return stoploss_from_open(
                    0.001, current_profit, is_short=is_short
                )

        return -stop_dist_pct

    # ═══════════════════════════════════════════════════════════════════
    # CUSTOM EXIT — R:R target + time management
    # ═══════════════════════════════════════════════════════════════════

    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 = sl_mult × ATR
        risk_pct = max(self.buy_sl_atr_mult.value * atr / trade.open_rate, 0.008)
        target_pct = risk_pct * self.buy_rr.value

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

        # ── MOMENTUM FADE at 70% target ──
        if current_profit >= target_pct * 0.7:
            rsi = last.get("rsi", 50)
            if is_short and rsi < 22:
                return "v4_fade"
            if not is_short and rsi > 78:
                return "v4_fade"

        # ── TIME EXIT ──
        minutes = (current_time - trade.open_date_utc).total_seconds() / 60
        max_min = self.buy_max_hold.value * 15  # 15m candles

        if minutes > max_min * 0.7 and current_profit > 0.003:
            return "v4_time_profit"

        if minutes > max_min:
            return "v4_time_force"

        return None

    # ═══════════════════════════════════════════════════════════════════
    # CONFIRM ENTRY
    # ═══════════════════════════════════════════════════════════════════

    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

        if self._consecutive_losses >= 3 and self._last_loss_time:
            if current_time < self._last_loss_time + timedelta(minutes=45):
                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:
        if trade.calc_profit_ratio(rate) < 0:
            self._consecutive_losses += 1
            self._last_loss_time = current_time
        else:
            self._consecutive_losses = 0
        return True
