# source: https://raw.githubusercontent.com/DOUGLASGUEDESATRIA/OSIRIS_TRADE/fe3c09567f3798d1cad6d7a306fa4f6131d91e17/user_data/strategies/OsirisDayTradeV6.py
"""
OSIRIS DAY TRADE v6 — Trailing Pullback + Funding Rate
================================================================
MANIFESTO:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  Ativo:       BTC/USDT FUTURES
  Tipo:        DAY TRADE
  Meta:        5-10 operações/dia
  Timeframe:   15m (primário) + 1h (tendência)
  Stop:        4×ATR inicial (~3-5%)
  Target:      NENHUM FIXO — trailing puro (deja winners rodarem)
  Trail:       Progressivo: 3.5ATR → 3ATR → 2ATR conforme lucro
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

INSIGHT CHAVE (v1-v5):
  Stops < 2× wick_médio → stopado por ruído (28-43% WR)
  Target fixo 1:1 → limita winners, não compensa losers
  SOLUÇÃO: trailing PURO — winners rodam até o trail pegar

ENTRADA: PULLBACK EM TENDÊNCIA
  LONG:
    1. 1h trend UP (EMA9 > EMA21 + ADX > 18)
    2. 15m pullback (2+ candles vermelhas OU RSI < 45)
    3. 15m reversão (candle verde com volume)
  SHORT: espelho

  FUNDING RATE (contrarian filter):
    - Funding > +0.02% → evita long (crowded)
    - Funding < -0.01% → evita short (crowded)

TRAILING PROGRESSIVO:
  profit < 1R:  stop a -4 ATR (deixa respirar)
  profit >= 1R: stop a -3 ATR (protege)
  profit >= 2R: stop a -2 ATR (lock hard)
  profit >= 3R: stop a -1.5 ATR (squeeze)
"""

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__OsirisDayTradeV6__20260329_060843(IStrategy):
    """
    OSIRIS v6 — Trailing-only pullback strategy.
    No fixed target. Trailing stop lets winners run.
    Pullback entry gives better prices than momentum chase.
    """

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

    # Wide ROI — trailing does the work
    minimal_roi = {"0": 0.50}  # basically disabled

    # Very wide safety stoploss
    stoploss = -0.06

    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 sizes (ATR multiples)
    buy_sl_initial = DecimalParameter(3.0, 5.0, default=4.0, decimals=1, space="buy", optimize=True)
    buy_sl_trail_1 = DecimalParameter(2.5, 4.0, default=3.0, decimals=1, space="buy", optimize=True)
    buy_sl_trail_2 = DecimalParameter(1.5, 3.0, default=2.0, decimals=1, space="buy", optimize=True)
    buy_sl_trail_3 = DecimalParameter(1.0, 2.0, default=1.5, decimals=1, space="buy", optimize=True)

    # 1h ADX minimum
    buy_adx_min = IntParameter(12, 25, default=18, space="buy", optimize=True)

    # Pullback detection
    buy_pullback_candles = IntParameter(1, 4, default=2, space="buy", optimize=True)
    buy_rsi_pullback_long = IntParameter(30, 50, default=45, space="buy", optimize=True)
    buy_rsi_pullback_short = IntParameter(50, 70, default=55, space="buy", optimize=True)

    # Volume
    buy_vol_min = DecimalParameter(0.4, 1.2, default=0.7, decimals=1, space="buy", optimize=True)

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

    # Max hold (15m candles)
    buy_max_hold = IntParameter(16, 48, default=32, 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["ema50"] = ta.EMA(dataframe, timeperiod=50)
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)

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

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

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

        # Count consecutive red/green candles (for pullback detection)
        dataframe["consec_red"] = 0
        dataframe["consec_green"] = 0

        red = dataframe["is_red"].values
        green = dataframe["is_green"].values
        n = len(dataframe)
        c_red = np.zeros(n, dtype=int)
        c_green = np.zeros(n, dtype=int)

        for i in range(1, n):
            if red[i - 1]:
                c_red[i] = c_red[i - 1] + 1
            else:
                c_red[i] = 0
            if green[i - 1]:
                c_green[i] = c_green[i - 1] + 1
            else:
                c_green[i] = 0

        dataframe["consec_red"] = c_red
        dataframe["consec_green"] = c_green

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

        return dataframe

    # ═══════════════════════════════════════════════════════════════════
    # ENTRIES — Pullback in trend
    # ═══════════════════════════════════════════════════════════════════

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        has_1h = "ema9_1h" in dataframe.columns
        pb_candles = self.buy_pullback_candles.value
        rsi_pb_long = self.buy_rsi_pullback_long.value
        rsi_pb_short = self.buy_rsi_pullback_short.value
        vol_min = self.buy_vol_min.value

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

        # ── 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)

        # ── PULLBACK DETECTION ──
        # Long pullback: N consecutive red candles OR RSI dipped below threshold
        pullback_long = (
            (dataframe["consec_red"] >= pb_candles) |
            (dataframe["rsi"] < rsi_pb_long)
        )

        # Short pullback: N consecutive green candles OR RSI spiked above threshold
        pullback_short = (
            (dataframe["consec_green"] >= pb_candles) |
            (dataframe["rsi"] > rsi_pb_short)
        )

        # ── REVERSAL CANDLE (entry trigger) ──
        reversal_long = dataframe["is_green"] == 1
        reversal_short = dataframe["is_red"] == 1

        # ── TREND ALIGNMENT ON 15m ──
        # For longs: 15m still in uptrend (EMA9 > EMA21) despite pullback
        ema_trend_up = dataframe["ema9"] > dataframe["ema21"]
        ema_trend_dn = dataframe["ema9"] < dataframe["ema21"]

        # ── COMBINE ──
        go_long = (
            h1_up &
            pullback_long &
            reversal_long &
            ema_trend_up &  # pullback within uptrend, not reversal
            vol_ok & has_vol & atr_ok
        )

        go_short = (
            h1_dn &
            pullback_short &
            reversal_short &
            ema_trend_dn &
            vol_ok & has_vol & atr_ok
        )

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

        # Tags
        dataframe.loc[go_long & (dataframe["consec_red"] >= pb_candles), "enter_tag"] = "pullback_candles_long"
        dataframe.loc[go_long & (dataframe["rsi"] < rsi_pb_long), "enter_tag"] = "pullback_rsi_long"
        dataframe.loc[go_short & (dataframe["consec_green"] >= pb_candles), "enter_tag"] = "pullback_candles_short"
        dataframe.loc[go_short & (dataframe["rsi"] > rsi_pb_short), "enter_tag"] = "pullback_rsi_short"

        return dataframe

    # ═══════════════════════════════════════════════════════════════════
    # EXIT — Minimal: let trailing do the work
    # ═══════════════════════════════════════════════════════════════════

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # Only exit on extreme conditions
        dataframe.loc[dataframe["rsi"] > 88, "exit_long"] = 1
        dataframe.loc[dataframe["rsi"] < 12, "exit_short"] = 1
        return dataframe

    # ═══════════════════════════════════════════════════════════════════
    # CUSTOM STOPLOSS — Progressive ATR 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.04

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

        # R = initial risk (4 ATR from entry)
        r_pct = self.buy_sl_initial.value * atr / trade.open_rate
        r_pct = max(0.02, min(r_pct, 0.05))

        if current_profit <= 0:
            # No profit: use initial wide stop
            return -r_pct

        # R-multiple achieved
        r_mult = current_profit / r_pct

        # Progressive trailing: tighter as profit grows
        if r_mult >= 3.0:
            trail = self.buy_sl_trail_3.value * atr / trade.open_rate
        elif r_mult >= 2.0:
            trail = self.buy_sl_trail_2.value * atr / trade.open_rate
        elif r_mult >= 1.0:
            trail = self.buy_sl_trail_1.value * atr / trade.open_rate
        else:
            # < 1R profit: still use initial stop
            return -r_pct

        trail = max(0.008, min(trail, 0.04))
        return -trail

    # ═══════════════════════════════════════════════════════════════════
    # CUSTOM EXIT — Time management only
    # ═══════════════════════════════════════════════════════════════════

    def custom_exit(
        self,
        pair: str,
        trade: Trade,
        current_time,
        current_rate: float,
        current_profit: float,
        **kwargs,
    ) -> Optional[str]:
        minutes = (current_time - trade.open_date_utc).total_seconds() / 60
        max_min = self.buy_max_hold.value * 15

        # If profitable and running long, take it
        if minutes > max_min * 0.7 and current_profit > 0.005:
            return "v6_time_profit"

        # Hard max hold
        if minutes > max_min:
            return "v6_time_force"

        # Exit if 1h trend flips against position
        dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
        if len(dataframe) > 0:
            last = dataframe.iloc[-1]
            is_short = trade.is_short if hasattr(trade, "is_short") else False

            if "ema9_1h" in last.index and "ema21_1h" in last.index:
                h1_up = last.get("ema9_1h", 0) > last.get("ema21_1h", 0)
                h1_dn = last.get("ema9_1h", 0) < last.get("ema21_1h", 0)

                # Trend flipped against us AND we're in small profit/loss
                if not is_short and h1_dn and current_profit > -0.005:
                    return "v6_trend_flip"
                if is_short and h1_up and current_profit > -0.005:
                    return "v6_trend_flip"

        return None

    # ═══════════════════════════════════════════════════════════════════
    # CONFIRM
    # ═══════════════════════════════════════════════════════════════════

    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
