# source: https://raw.githubusercontent.com/DOUGLASGUEDESATRIA/OSIRIS_TRADE/fe3c09567f3798d1cad6d7a306fa4f6131d91e17/user_data/strategies/OsirisDayTradeV8.py
"""
OSIRIS DAY TRADE v8 — Smart Losers, Long Winners
================================================================
v7 → v8: PF 0.76 → target 1.0+

MUDANÇAS CHAVE DO v8:
  1. CORTA LOSERS CEDO: se em 4 candles (1h) a trade está perdendo
     E condições deterioram (RSI + EMA contra) → SAIR COM LOSS MENOR
  2. WINNERS SEM LIMITE DE TEMPO: time_force SÓ para trades perdendo
  3. BREAKEVEN mais agressivo: mover stop para BE em 0.5R
  4. Manter: deep pullback, patterns, ADX filter, progressive trail
"""

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__OsirisDayTradeV8__20260329_060843(IStrategy):
    """
    OSIRIS v8 — Cut losers early, let winners run forever.
    Progressive trailing + conditional loser exit + no time cap for winners.
    """

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

    minimal_roi = {"0": 0.50}

    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
    # ═══════════════════════════════════════════════════════════════════

    # Trailing
    buy_sl_initial = DecimalParameter(3.0, 5.0, default=3.5, 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)

    # 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, 4, default=2, space="buy", optimize=True)
    buy_rsi_pb_long = IntParameter(30, 48, default=42, space="buy", optimize=True)
    buy_rsi_pb_short = IntParameter(52, 70, default=58, 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)

    # Early exit thresholds
    buy_early_exit_candles = IntParameter(3, 8, default=4, space="buy", optimize=True)
    buy_early_exit_rsi_long = IntParameter(28, 40, default=35, space="buy", optimize=True)
    buy_early_exit_rsi_short = IntParameter(60, 72, default=65, space="buy", optimize=True)

    # Time force (only for losers)
    buy_max_hold_loser = 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:
        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 = dataframe["close"]
        o = dataframe["open"]
        h = dataframe["high"]
        l = 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

        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)

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

        atr_safe = dataframe["atr"].replace(0, 1)
        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 candles
        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 — Deep pullback + pattern (from v7, slightly relaxed)
    # ═══════════════════════════════════════════════════════════════════

    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)

        # 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 (green/red after pullback, 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_pat = h1_up & pullback_long & pat_long & vol_ok & has_vol & atr_ok
        go_long_simple = h1_up & pullback_long & simple_long & vol_ok & has_vol & atr_ok
        go_short_pat = h1_dn & pullback_short & pat_short & vol_ok & has_vol & atr_ok
        go_short_simple = h1_dn & pullback_short & simple_short & vol_ok & has_vol & atr_ok

        dataframe.loc[go_long_pat | go_long_simple, "enter_long"] = 1
        dataframe.loc[go_short_pat | go_short_simple, "enter_short"] = 1

        return dataframe

    # ═══════════════════════════════════════════════════════════════════
    # EXIT — Let trailing work
    # ═══════════════════════════════════════════════════════════════════

    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 — Progressive + breakeven at 0.5R
    # ═══════════════════════════════════════════════════════════════════

    def custom_stoploss(self, pair, trade, current_time, current_rate,
                        current_profit, **kwargs) -> float:
        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

        r_pct = self.buy_sl_initial.value * atr / trade.open_rate
        r_pct = max(0.015, min(r_pct, 0.05))

        if current_profit <= 0:
            return -r_pct

        r_mult = current_profit / r_pct

        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
        elif r_mult >= 0.5:
            # New: breakeven at 0.5R
            is_short = trade.is_short if hasattr(trade, "is_short") else False
            return stoploss_from_open(0.001, current_profit, is_short=is_short)
        else:
            return -r_pct

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

    # ═══════════════════════════════════════════════════════════════════
    # CUSTOM EXIT — Early loser cut + time for losers only
    # ═══════════════════════════════════════════════════════════════════

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

        # ── EARLY LOSER CUT ──
        # After N candles, if trade is losing and conditions deteriorating → exit
        if candles >= self.buy_early_exit_candles.value and current_profit < -0.003:
            rsi = last.get("rsi", 50)
            ema9 = last.get("ema9", 0)
            ema21 = last.get("ema21", 0)

            if not is_short:
                # Long: RSI dropping + EMA broken → cut loss early
                if rsi < self.buy_early_exit_rsi_long.value and ema9 < ema21:
                    return "v8_early_cut_long"
            else:
                # Short: RSI rising + EMA crossed up → cut loss early
                if rsi > self.buy_early_exit_rsi_short.value and ema9 > ema21:
                    return "v8_early_cut_short"

        # ── TIME FORCE: ONLY for losers ──
        max_min_loser = self.buy_max_hold_loser.value * 15
        if minutes > max_min_loser and current_profit < 0:
            return "v8_time_force_loser"

        # ── Winners: no time limit! Let trailing work ──
        # (but add a very generous time limit as absolute safety)
        if minutes > 1440 and current_profit > 0.005:  # 24h max
            return "v8_time_profit_24h"

        if minutes > 1440:
            return "v8_time_force_24h"

        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
