# source: https://raw.githubusercontent.com/DOUGLASGUEDESATRIA/OSIRIS_TRADE/fe3c09567f3798d1cad6d7a306fa4f6131d91e17/user_data/strategies/OsirisDayTradeV5.py
"""
OSIRIS DAY TRADE v5 — Wide Stop Trend Follower
================================================================
MANIFESTO:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  Ativo:       BTC/USDT FUTURES
  Tipo:        DAY TRADE
  Meta:        ~10 operações/dia
  Timeframe:   15m (primário) + 1h (tendência)
  Stop:        3×ATR (~2-4%) — o wick NÃO ALCANÇA
  Target:      1:1 R:R (stop = target → WR natural ~50%)
  Trail:       Breakeven em 0.7R
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

DIAGNÓSTICO v1-v4:
  - v1-v3: stops 0.4-1.5% → BTC wick médio de 0.5-1% → stopado
  - v4: stops ~2% → WR subiu de 28% para 33%
  - Conclusão: stops DEVEM ser > 2×wick_médio para sobreviver
  - Com stop=target (1:1), WR teórica = 50% em random walk
  - Qualquer edge direcional → WR > 50% → LUCRO

ENTRADAS (SIMPLES — 3 condições simultâneas):
  LONG:
    1. 1h trend UP (EMA9_1h > EMA21_1h)
    2. 15m momentum (RSI > 50 ou close > EMA21)
    3. 15m candle verde (confirmação de preço)
  SHORT:
    1. 1h trend DOWN (EMA9_1h < EMA21_1h)
    2. 15m momentum (RSI < 50 ou close < EMA21)
    3. 15m candle vermelha
"""

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__OsirisDayTradeV5__20260329_060843(IStrategy):
    """
    OSIRIS DAY TRADE v5 — Wide stops (3 ATR), 1:1 R:R, simple trend entries.
    Key: let the STOPS be wide enough to survive BTC noise.
    Target ONLY >50% WR, not trying for high R:R.
    """

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

    minimal_roi = {"0": 0.10, "240": 0.003}

    # Wide safety stoploss
    stoploss = -0.05

    trailing_stop = False
    use_custom_stoploss = True

    startup_candle_count = 200
    process_only_new_candles = True

    _daily_trades = {}
    _consecutive_losses = 0
    _last_loss_time = None

    # ═══════════════════════════════════════════════════════════════════
    # PARAMETERS
    # ═══════════════════════════════════════════════════════════════════

    # Stop size (ATR multiples)
    buy_sl_atr = DecimalParameter(2.0, 4.0, default=3.0, decimals=1, space="buy", optimize=True)

    # R:R (1.0 = same as stop)
    buy_rr = DecimalParameter(0.8, 1.5, default=1.0, decimals=1, space="buy", optimize=True)

    # RSI filter for entry
    buy_rsi_long_min = IntParameter(45, 55, default=50, space="buy", optimize=True)
    buy_rsi_short_max = IntParameter(45, 55, default=50, space="buy", optimize=True)

    # 1h ADX minimum for trend
    buy_adx_min = IntParameter(15, 30, default=18, space="buy", optimize=True)

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

    # Max hold (15m candles)
    buy_max_hold = IntParameter(12, 32, default=16, space="buy", optimize=True)

    # Breakeven point (R multiple)
    buy_be_mult = DecimalParameter(0.4, 0.9, default=0.7, decimals=1, space="buy", optimize=True)

    # ═══════════════════════════════════════════════════════════════════
    # INFORMATIVE
    # ═══════════════════════════════════════════════════════════════════

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

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

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # 15m indicators
        dataframe["ema9"] = ta.EMA(dataframe, timeperiod=9)
        dataframe["ema21"] = ta.EMA(dataframe, timeperiod=21)
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
        dataframe["vol_sma"] = ta.SMA(dataframe["volume"], timeperiod=20)
        dataframe["vol_ratio"] = dataframe["volume"] / dataframe["vol_sma"].replace(0, 1)

        # Candle
        dataframe["is_green"] = (dataframe["close"] > dataframe["open"]).astype(int)
        dataframe["is_red"] = (dataframe["close"] < dataframe["open"]).astype(int)

        # 1h merge
        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["adx"] = ta.ADX(inf_1h, timeperiod=14)
                dataframe = merge_informative_pair(
                    dataframe, inf_1h, self.timeframe, "1h", ffill=True
                )

        return dataframe

    # ═══════════════════════════════════════════════════════════════════
    # ENTRIES — Simple 3-condition trend following
    # ═══════════════════════════════════════════════════════════════════

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        has_1h = "ema9_1h" in dataframe.columns
        rsi_long = self.buy_rsi_long_min.value
        rsi_short = self.buy_rsi_short_max.value

        # 1h trend
        if has_1h:
            h1_up = (dataframe["ema9_1h"] > dataframe["ema21_1h"]) & (dataframe["adx_1h"] > self.buy_adx_min.value)
            h1_dn = (dataframe["ema9_1h"] < dataframe["ema21_1h"]) & (dataframe["adx_1h"] > self.buy_adx_min.value)
        else:
            h1_up = pd.Series(True, index=dataframe.index)
            h1_dn = pd.Series(True, index=dataframe.index)

        # Common
        vol_ok = dataframe["vol_ratio"] > 0.5
        has_vol = dataframe["volume"] > 0
        atr_ok = dataframe["atr"] > 1

        # ── LONG: 1h up + 15m momentum + green candle ──
        long_entry = (
            h1_up &
            (
                (dataframe["rsi"] > rsi_long) |
                (dataframe["close"] > dataframe["ema21"])
            ) &
            (dataframe["is_green"] == 1) &
            vol_ok & has_vol & atr_ok
        )

        # ── SHORT: 1h down + 15m momentum + red candle ──
        short_entry = (
            h1_dn &
            (
                (dataframe["rsi"] < rsi_short) |
                (dataframe["close"] < dataframe["ema21"])
            ) &
            (dataframe["is_red"] == 1) &
            vol_ok & has_vol & atr_ok
        )

        dataframe.loc[long_entry, "enter_long"] = 1
        dataframe.loc[short_entry, "enter_short"] = 1

        return dataframe

    # ═══════════════════════════════════════════════════════════════════
    # EXIT
    # ═══════════════════════════════════════════════════════════════════

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # Let custom_exit and stoploss handle exits
        # Only exit on extreme RSI as safety
        dataframe.loc[dataframe["rsi"] > 85, "exit_long"] = 1
        dataframe.loc[dataframe["rsi"] < 15, "exit_short"] = 1
        return dataframe

    # ═══════════════════════════════════════════════════════════════════
    # CUSTOM STOPLOSS — Wide ATR + Breakeven 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.03

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

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

        # Wide stop: sl_atr × ATR
        stop_pct = self.buy_sl_atr.value * atr / trade.open_rate
        stop_pct = max(0.015, min(stop_pct, 0.045))

        # Breakeven trail
        if current_profit > 0:
            r_mult = current_profit / stop_pct
            if r_mult >= self.buy_be_mult.value:
                return stoploss_from_open(0.001, current_profit, is_short=is_short)

        return -stop_pct

    # ═══════════════════════════════════════════════════════════════════
    # CUSTOM EXIT — 1:1 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

        # Target = R:R × stop
        risk_pct = max(self.buy_sl_atr.value * atr / trade.open_rate, 0.015)
        target_pct = risk_pct * self.buy_rr.value

        if current_profit >= target_pct:
            return "v5_tp"

        # Time exit
        minutes = (current_time - trade.open_date_utc).total_seconds() / 60
        max_min = self.buy_max_hold.value * 15

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

        if minutes > max_min:
            return "v5_time_force"

        return None

    # ═══════════════════════════════════════════════════════════════════
    # TRADE ENTRY/EXIT MANAGEMENT
    # ═══════════════════════════════════════════════════════════════════

    def confirm_trade_entry(self, pair, order_type, amount, rate, time_in_force,
                            current_time, entry_tag, side, **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, trade, order_type, amount, rate,
                           time_in_force, exit_reason, current_time, **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
