# source: https://raw.githubusercontent.com/DOUGLASGUEDESATRIA/OSIRIS_TRADE/fe3c09567f3798d1cad6d7a306fa4f6131d91e17/user_data/strategies/OsirisDayTradeV9.py
"""
OSIRIS DAY TRADE v9 — Fixed Stop + Time Profit Edge
================================================================
DIAGNÓSTICO v7 (PF 0.76):
  trailing_stop_loss: 298 trades, WR 25%, -513 USDT = KILLER
  time_force:         150 trades, WR 18%, -151 USDT = bad
  time_profit:        186 trades, WR 100%, +434 USDT = ONLY EDGE

CONCLUSÃO: o trailing progressivo PIORA o resultado. Winners saem
por time_profit, losers saem por trailing (com loss ampliado).

v9 FIX:
  1. STOP FIXO (sem trailing complexo) — 3.5 ATR, ponto FINAL
  2. BREAKEVEN simples em 0.5R (move stop para +0.1%)
  3. TARGET ESCALONADO: 50% profit em 1R, trail o resto
  4. TIME PROFIT expandido (funciona!)
  5. TIME FORCE com stop loss APERTADO (sai cedo se perdendo)
  6. Pullback entries do v7 (funcionam bem)
"""

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__OsirisDayTradeV9__20260329_060843(IStrategy):
    """
    OSIRIS v9 — Fixed stop + time-based profit taking.
    Based on v7 analysis: trailing kills, time_profit is the only edge.
    Strategy: hold position, fixed stop, take profit on time or target.
    """

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

    # Disabled — custom exit handles everything
    minimal_roi = {"0": 0.50}

    # Fixed stoploss (backup only)
    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: fixed ATR distance
    buy_sl_atr = DecimalParameter(3.0, 5.0, default=3.5, decimals=1, space="buy", optimize=True)

    # Target: R multiple
    buy_tp_r = DecimalParameter(1.0, 3.0, default=1.5, decimals=1, space="buy", optimize=True)

    # Breakeven at this R
    buy_be_r = DecimalParameter(0.3, 0.8, default=0.5, decimals=1, space="buy", optimize=True)

    # Trend
    buy_adx_long = IntParameter(15, 28, default=20, space="buy", optimize=True)
    buy_adx_short = IntParameter(20, 35, default=28, space="buy", optimize=True)

    # Pullback
    buy_pb_candles = IntParameter(2, 5, default=3, space="buy", optimize=True)
    buy_rsi_pb_long = IntParameter(30, 48, default=40, space="buy", optimize=True)
    buy_rsi_pb_short = IntParameter(52, 70, default=60, space="buy", optimize=True)

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

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

    # Time params (candles)
    buy_time_profit_min = IntParameter(4, 16, default=8, space="buy", optimize=True)
    buy_time_force_max = IntParameter(24, 64, default=48, space="buy", optimize=True)

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

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

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

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        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)

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

        c, o, h, l = dataframe["close"], dataframe["open"], dataframe["high"], dataframe["low"]
        dataframe["is_green"] = (c > o).astype(int)
        dataframe["is_red"] = (c < o).astype(int)
        dataframe["body"] = abs(c - o)
        dataframe["lower_wick"] = pd.concat([c, o], axis=1).min(axis=1) - l
        dataframe["upper_wick"] = h - pd.concat([c, o], axis=1).max(axis=1)
        dataframe["range"] = h - l

        # Previous candle
        dataframe["prev_red"] = dataframe["is_red"].shift(1)
        dataframe["prev_green"] = dataframe["is_green"].shift(1)
        dataframe["prev_close"] = c.shift(1)
        dataframe["prev_open"] = o.shift(1)

        # Reversal patterns
        atr_safe = dataframe["atr"].replace(0, 1)

        dataframe["bullish_engulf"] = (
            (dataframe["is_green"] == 1) &
            (dataframe["prev_red"] == 1) &
            (o <= dataframe["prev_close"]) &
            (c >= dataframe["prev_open"])
        ).astype(int)

        dataframe["hammer"] = (
            (dataframe["is_green"] == 1) &
            (dataframe["lower_wick"] > 2 * dataframe["body"].replace(0, 0.01)) &
            (dataframe["lower_wick"] > 2 * dataframe["upper_wick"].replace(0, 0.01)) &
            (dataframe["range"] > 0.3 * atr_safe)
        ).astype(int)

        dataframe["bearish_engulf"] = (
            (dataframe["is_red"] == 1) &
            (dataframe["prev_green"] == 1) &
            (o >= dataframe["prev_close"]) &
            (c <= dataframe["prev_open"])
        ).astype(int)

        dataframe["shooting_star"] = (
            (dataframe["is_red"] == 1) &
            (dataframe["upper_wick"] > 2 * dataframe["body"].replace(0, 0.01)) &
            (dataframe["upper_wick"] > 2 * dataframe["lower_wick"].replace(0, 0.01)) &
            (dataframe["range"] > 0.3 * atr_safe)
        ).astype(int)

        dataframe["pat_bull"] = ((dataframe["bullish_engulf"] == 1) | (dataframe["hammer"] == 1)).astype(int)
        dataframe["pat_bear"] = ((dataframe["bearish_engulf"] == 1) | (dataframe["shooting_star"] == 1)).astype(int)

        # Consecutive
        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):
            c_red[i] = c_red[i - 1] + 1 if red[i - 1] else 0
            c_green[i] = c_green[i - 1] + 1 if green[i - 1] else 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)
                dataframe = merge_informative_pair(
                    dataframe, inf_1h, self.timeframe, "1h", ffill=True
                )

        return dataframe

    # ═══════════════════════════════════════════════════════════════════
    # ENTRIES — Same pullback logic as v7 (proven)
    # ═══════════════════════════════════════════════════════════════════

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        has_1h = "ema9_1h" in dataframe.columns
        pb = self.buy_pb_candles.value
        rsi_l = self.buy_rsi_pb_long.value
        rsi_s = self.buy_rsi_pb_short.value
        vol_min = self.buy_vol_min.value

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

        if has_1h:
            h1_up = (dataframe["ema9_1h"] > dataframe["ema21_1h"]) & (dataframe["adx_1h"] > self.buy_adx_long.value)
            h1_dn = (dataframe["ema9_1h"] < dataframe["ema21_1h"]) & (dataframe["adx_1h"] > self.buy_adx_short.value)
        else:
            h1_up = pd.Series(True, index=dataframe.index)
            h1_dn = pd.Series(True, index=dataframe.index)

        # Deep pullback
        pullback_long = (dataframe["consec_red"] >= pb) | (dataframe["rsi"] < rsi_l)
        pullback_short = (dataframe["consec_green"] >= pb) | (dataframe["rsi"] > rsi_s)

        # Pattern entries
        pat_long = dataframe["pat_bull"] == 1
        pat_short = dataframe["pat_bear"] == 1

        # Simple entries (with EMA confirmation)
        simple_long = (dataframe["is_green"] == 1) & (dataframe["ema9"] > dataframe["ema21"])
        simple_short = (dataframe["is_red"] == 1) & (dataframe["ema9"] < dataframe["ema21"])

        go_long = h1_up & pullback_long & (pat_long | (simple_long & (dataframe["close"] > dataframe["ema50"]))) & vol_ok & has_vol & atr_ok
        go_short = h1_dn & pullback_short & (pat_short | (simple_short & (dataframe["close"] < dataframe["ema50"]))) & vol_ok & has_vol & atr_ok

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

        return dataframe

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

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[dataframe["rsi"] > 90, "exit_long"] = 1
        dataframe.loc[dataframe["rsi"] < 10, "exit_short"] = 1
        return dataframe

    # ═══════════════════════════════════════════════════════════════════
    # CUSTOM STOPLOSS — FIXED (no trailing!)
    # ═══════════════════════════════════════════════════════════════════

    def custom_stoploss(self, pair, trade, current_time, current_rate,
                        current_profit, **kwargs) -> float:
        """
        Fixed stop at entry. Only moves to breakeven at 0.5R.
        NO trailing — v7 analysis showed trailing kills profits.
        """
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if len(dataframe) == 0:
            return -0.035

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

        # Fixed stop distance
        r_pct = self.buy_sl_atr.value * atr / trade.open_rate
        r_pct = max(0.015, min(r_pct, 0.05))

        # Breakeven: if profit >= be_r × R → move stop to +0.1%
        be_threshold = self.buy_be_r.value * r_pct
        if current_profit >= be_threshold:
            is_short = trade.is_short if hasattr(trade, "is_short") else False
            return stoploss_from_open(0.001, current_profit, is_short=is_short)

        # Otherwise: fixed stop
        return -r_pct

    # ═══════════════════════════════════════════════════════════════════
    # CUSTOM EXIT — Target + Time Profit (the REAL edge)
    # ═══════════════════════════════════════════════════════════════════

    def custom_exit(self, pair, trade, current_time, current_rate,
                    current_profit, **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
        minutes = (current_time - trade.open_date_utc).total_seconds() / 60
        candles = int(minutes / 15)

        # R calculation
        r_pct = self.buy_sl_atr.value * atr / trade.open_rate
        r_pct = max(0.015, min(r_pct, 0.05))
        target_pct = r_pct * self.buy_tp_r.value

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

        # ── TIME PROFIT (THE EDGE from v7 analysis) ──
        # After N candles, if profitable → take it
        if candles >= self.buy_time_profit_min.value and current_profit > 0.003:
            return "v9_time_profit"

        # ── MOMENTUM EXIT: trend lost while losing ──
        if candles >= 4 and current_profit < -0.003:
            rsi = last.get("rsi", 50)
            ema9 = last.get("ema9", 0)
            ema21 = last.get("ema21", 0)

            if not is_short and rsi < 35 and ema9 < ema21:
                return "v9_momentum_exit_long"
            if is_short and rsi > 65 and ema9 > ema21:
                return "v9_momentum_exit_short"

        # ── TIME FORCE: only for losers ──
        max_min = self.buy_time_force_max.value * 15
        if minutes > max_min and current_profit < 0:
            return "v9_time_force"

        # Winners can run until target or time_profit catches them
        if minutes > max_min and current_profit > 0.002:
            return "v9_time_profit_late"

        # Hard cap at 24h
        if minutes > 1440:
            return "v9_hard_cap"

        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=60):
                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
