# source: https://raw.githubusercontent.com/Alejop27/trading-bot/06a97d5c50fcec2586b207748e6a610bdcc8d1ce/user_data/strategies/SMC_ML_Strategy.py
# ─────────────────────────────────────────────────────────────────────────────
# Github_Alejop27_trading_bot__SMC_ML_Strategy__20260604_012059.py — v8 — Futures + momentum_failure exit (bugs corregidos)
# Exchange: Bybit | Par: BTC/USDT | Timeframe: 15m
# ─────────────────────────────────────────────────────────────────────────────

import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.strategy import IStrategy, informative
import ta
import joblib
import os

class Github_Alejop27_trading_bot__SMC_ML_Strategy__20260604_012059(IStrategy):

    INTERFACE_VERSION = 3
    timeframe = '15m'
    can_short = True   # futuros permiten short

    minimal_roi = {
        "0":   0.06,
        "60":  0.04,
        "180": 0.02,
        "360": 0.01
    }

    stoploss = -0.02
    trailing_stop = True
    trailing_stop_positive = 0.01
    trailing_stop_positive_offset = 0.015
    trailing_only_offset_is_reached = True
    use_exit_signal = True

    # ── Momentum failure exit — valores corregidos ────────────────────────────
    # Bug en v7: MFE_CANDLES_WAIT=92 (23h) → el SL cerraba antes de evaluar
    #            MFE_MIN_THRESHOLD=0.005 (0.5%) → umbral demasiado laxo
    # Correcto:  8 velas = 2h | 0.008 = 0.8% (margen bajo el 1.0% observado)
    #
    # Evidencia: 14/14 stops → MFE < 1.0% (avg 0.519%)
    #             0/44 winners → MFE < 1.0% (avg 1.560%)
    MFE_CANDLES_WAIT  = 12      # velas 15m antes de evaluar = 2 horas
    MFE_MIN_THRESHOLD = 0.005  # 0.8% mínimo de excursión favorable

    startup_candle_count = 200
    process_only_new_candles = True

    ml_score_threshold = 0.62
    ml_model  = None
    ml_scaler = None

    ml_model_path  = os.path.join(os.path.dirname(__file__), '..', 'ml_models', 'smc_ml_model.pkl')
    ml_scaler_path = os.path.join(os.path.dirname(__file__), '..', 'ml_models', 'smc_ml_scaler.pkl')

    FEATURE_COLS = [
        'htf_bias_1h', 'htf_ema20_50_dist_1h',
        'htf_ema50_200_dist_1h', 'htf_price_ema200_dist_1h',
        'dist_to_ob', 'ob_size', 'ob_age',
        'dist_to_fvg', 'fvg_size',
        'dist_to_swing_high', 'dist_to_swing_low',
        'volume_ratio', 'volume_zscore',
        'atr_norm', 'atr_ratio', 'bb_width',
        'rsi', 'macd_hist',
        'in_kill_zone', 'bos_bullish',
    ]

    def bot_start(self, **kwargs):
        model_ok  = os.path.exists(self.ml_model_path)
        scaler_ok = os.path.exists(self.ml_scaler_path)
        if model_ok and scaler_ok:
            self.ml_model  = joblib.load(self.ml_model_path)
            self.ml_scaler = joblib.load(self.ml_scaler_path)
            print(f"✅ Modelo ML cargado")
            print(f"✅ Scaler ML cargado")
        else:
            print("⚠️  Modelo o scaler no encontrado. Operando sin filtro ML.")

    def leverage(self, pair: str, current_time, current_rate: float,
                 proposed_leverage: float, max_leverage: float,
                 entry_tag: str, side: str) -> float:
        """
        Leverage fijo conservador.
        Con DD histórico de 1.45% en spot, en 3x el DD esperado es ~4.3%.
        En futuros el DD real puede diferir por funding rates y liquidaciones.
        Empezar conservador — ajustar después de validar en paper trading.
        """
        return 3.0

    @informative('1h')
    def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe['ema_20_1h']  = ta.trend.ema_indicator(dataframe['close'], window=20)
        dataframe['ema_50_1h']  = ta.trend.ema_indicator(dataframe['close'], window=50)
        dataframe['ema_200_1h'] = ta.trend.ema_indicator(dataframe['close'], window=200)

        dataframe['htf_bias'] = 0
        dataframe.loc[
            (dataframe['ema_20_1h'] > dataframe['ema_50_1h']) &
            (dataframe['ema_50_1h'] > dataframe['ema_200_1h']),
            'htf_bias'
        ] = 1
        dataframe.loc[
            (dataframe['ema_20_1h'] < dataframe['ema_50_1h']) &
            (dataframe['ema_50_1h'] < dataframe['ema_200_1h']),
            'htf_bias'
        ] = -1

        dataframe['htf_ema20_50_dist'] = (
            (dataframe['ema_20_1h'] - dataframe['ema_50_1h']) / dataframe['ema_50_1h']
        )
        dataframe['htf_ema50_200_dist'] = (
            (dataframe['ema_50_1h'] - dataframe['ema_200_1h']) / dataframe['ema_200_1h']
        )
        dataframe['htf_price_ema200_dist'] = (
            (dataframe['close'] - dataframe['ema_200_1h']) / dataframe['ema_200_1h']
        )
        dataframe['atr_1h'] = ta.volatility.average_true_range(
            dataframe['high'], dataframe['low'], dataframe['close'], window=14
        )
        return dataframe

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe['ema_9']   = ta.trend.ema_indicator(dataframe['close'], window=9)
        dataframe['ema_21']  = ta.trend.ema_indicator(dataframe['close'], window=21)
        dataframe['ema_50']  = ta.trend.ema_indicator(dataframe['close'], window=50)
        dataframe['ema_200'] = ta.trend.ema_indicator(dataframe['close'], window=200)

        dataframe['rsi'] = ta.momentum.rsi(dataframe['close'], window=14)
        macd = ta.trend.MACD(dataframe['close'])
        dataframe['macd']        = macd.macd()
        dataframe['macd_signal'] = macd.macd_signal()
        dataframe['macd_hist']   = macd.macd_diff()

        dataframe['atr'] = ta.volatility.average_true_range(
            dataframe['high'], dataframe['low'], dataframe['close'], window=14
        )
        bb = ta.volatility.BollingerBands(dataframe['close'], window=20, window_dev=2)
        dataframe['bb_upper'] = bb.bollinger_hband()
        dataframe['bb_lower'] = bb.bollinger_lband()
        dataframe['bb_mid']   = bb.bollinger_mavg()
        dataframe['bb_width'] = (dataframe['bb_upper'] - dataframe['bb_lower']) / dataframe['bb_mid']

        dataframe['volume_ma_20'] = dataframe['volume'].rolling(20).mean()
        dataframe['volume_ratio'] = dataframe['volume'] / dataframe['volume_ma_20']
        vol_mean = dataframe['volume'].rolling(50).mean()
        vol_std  = dataframe['volume'].rolling(50).std()
        dataframe['volume_zscore'] = (dataframe['volume'] - vol_mean) / vol_std

        dataframe = self._detect_swing_points(dataframe)
        dataframe = self._detect_order_blocks(dataframe)
        dataframe = self._detect_fvg(dataframe)
        dataframe = self._detect_liquidity(dataframe)
        dataframe = self._detect_kill_zone(dataframe)
        dataframe = self._detect_bos(dataframe)
        dataframe = self._compute_smc_features(dataframe)
        dataframe = self._compute_ml_score(dataframe)

        return dataframe

    def _detect_swing_points(self, df: DataFrame, lookback: int = 11) -> DataFrame:
        df['swing_high'] = (
            df['high'] == df['high'].rolling(lookback).max()
        ).astype(int)
        df['swing_low'] = (
            df['low'] == df['low'].rolling(lookback).min()
        ).astype(int)
        df['last_swing_high'] = df['high'].where(df['swing_high'] == 1).ffill()
        df['last_swing_low']  = df['low'].where(df['swing_low'] == 1).ffill()
        return df

    def _detect_bos(self, df: DataFrame) -> DataFrame:
        df['bos_bullish'] = (
            (df['close'] > df['last_swing_high'].shift(1)) &
            (df['close'].shift(1) <= df['last_swing_high'].shift(1))
        ).astype(int)
        df['bos_bearish'] = (
            (df['close'] < df['last_swing_low'].shift(1)) &
            (df['close'].shift(1) >= df['last_swing_low'].shift(1))
        ).astype(int)
        return df

    def _detect_order_blocks(self, df: DataFrame) -> DataFrame:
        body     = abs(df['close'] - df['open'])
        avg_body = body.rolling(20).mean()
        impulse  = body > avg_body * 1.5

        df['ob_bullish'] = (
            (df['close'].shift(1) < df['open'].shift(1)) &
            impulse &
            (df['close'] > df['open'])
        ).astype(int)
        df['ob_bull_top']    = df['open'].shift(1).where(df['ob_bullish'] == 1).ffill()
        df['ob_bull_bottom'] = df['close'].shift(1).where(df['ob_bullish'] == 1).ffill()

        df['ob_bearish'] = (
            (df['close'].shift(1) > df['open'].shift(1)) &
            impulse &
            (df['close'] < df['open'])
        ).astype(int)
        df['ob_bear_top']    = df['close'].shift(1).where(df['ob_bearish'] == 1).ffill()
        df['ob_bear_bottom'] = df['open'].shift(1).where(df['ob_bearish'] == 1).ffill()
        return df

    def _detect_fvg(self, df: DataFrame) -> DataFrame:
        df['fvg_bullish']     = (df['high'].shift(2) < df['low']).astype(int)
        df['fvg_bull_bottom'] = df['high'].shift(2).where(df['fvg_bullish'] == 1).ffill()
        df['fvg_bull_top']    = df['low'].where(df['fvg_bullish'] == 1).ffill()
        df['fvg_bearish']     = (df['low'].shift(2) > df['high']).astype(int)
        return df

    def _detect_liquidity(self, df: DataFrame, tolerance: float = 0.001) -> DataFrame:
        df['liquidity_high'] = (
            (df['high'].rolling(20).max() - df['high']).abs() / df['high'] < tolerance
        ).astype(int)
        df['liquidity_low'] = (
            (df['low'].rolling(20).min() - df['low']).abs() / df['low'] < tolerance
        ).astype(int)
        return df

    def _detect_kill_zone(self, df: DataFrame) -> DataFrame:
        hour = df['date'].dt.hour
        df['in_kill_zone'] = (
            ((hour >= 7) & (hour < 10)) |
            ((hour >= 12) & (hour < 15))
        ).astype(int)
        df['london_open'] = ((hour >= 7)  & (hour < 10)).astype(int)
        df['ny_open']     = ((hour >= 12) & (hour < 15)).astype(int)
        return df

    def _compute_smc_features(self, df: DataFrame) -> DataFrame:
        df['dist_to_ob'] = (
            (df['close'] - df['ob_bull_top']) / df['close']
        ).fillna(0)
        df['ob_size'] = (
            (df['ob_bull_top'] - df['ob_bull_bottom']) / df['close']
        ).fillna(0)

        ob_age, last_ob = [], np.nan
        for i in range(len(df)):
            if df['ob_bullish'].iloc[i] == 1:
                last_ob = i
            ob_age.append(i - last_ob if not np.isnan(last_ob) else 999)
        df['ob_age'] = ob_age

        df['dist_to_fvg'] = (
            (df['close'] - df['fvg_bull_top']) / df['close']
        ).fillna(0)
        df['fvg_size'] = (
            (df['fvg_bull_top'] - df['fvg_bull_bottom']) / df['close']
        ).fillna(0)
        df['dist_to_swing_high'] = (
            (df['last_swing_high'] - df['close']) / df['close']
        ).fillna(0)
        df['dist_to_swing_low'] = (
            (df['close'] - df['last_swing_low']) / df['close']
        ).fillna(0)
        df['atr_norm']  = df['atr'] / df['close']
        df['atr_ratio'] = df['atr'] / df['atr_1h_1h'].replace(0, np.nan).ffill()
        return df

    def _compute_ml_score(self, df: DataFrame) -> DataFrame:
        df['ml_score'] = 0.5
        if self.ml_model is None or self.ml_scaler is None:
            return df
        try:
            features        = df[self.FEATURE_COLS].fillna(0)
            features_scaled = self.ml_scaler.transform(features)
            df['ml_score']  = self.ml_model.predict_proba(features_scaled)[:, 1]
        except Exception as e:
            print(f"⚠️  Error en ML score: {e}")
        return df

    def custom_exit(self, pair: str, trade, current_time,
                    current_rate: float, current_profit: float, **kwargs):
        """
        Momentum failure exit — salida temprana por falta de follow-through.

        Evidencia estadística (14 stops vs 44 winners del backtest v6):
          - 14/14 stops  → MFE < 1.0% | promedio MFE = 0.519%
          -  0/44 winners → MFE < 1.0% | promedio MFE = 1.560%
          - Separación perfecta sin excepciones → discriminador robusto

        Lógica:
          Después de MFE_CANDLES_WAIT velas (2h), si el precio nunca llegó
          a MFE_MIN_THRESHOLD (0.8%) de excursión favorable → momentum failure.
          Salir con ~-0.5% en lugar de esperar SL a -2%.

        Efecto esperado: -$104 USDT en stops → ~-$25 USDT = +$79 USDT recuperados.

        Bugs corregidos respecto a v7:
          - MFE_CANDLES_WAIT: 92 → 8  (92 velas = 23h, SL actúa antes)
          - MFE_MIN_THRESHOLD: 0.005 → 0.008  (0.5% demasiado laxo)
        """
        if trade.max_rate is None or trade.open_rate is None:
            return None

        candles_since_entry = int(
            (current_time - trade.open_date_utc).total_seconds() / (15 * 60)
        )

        if candles_since_entry < self.MFE_CANDLES_WAIT:
            return None

        mfe = (trade.max_rate - trade.open_rate) / trade.open_rate

        if mfe < self.MFE_MIN_THRESHOLD:
            return "momentum_failure"

        return None

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        if self.ml_model is not None and self.ml_scaler is not None:
            ml_filter = dataframe['ml_score'] >= self.ml_score_threshold
        else:
            ml_filter = False

        ema21_vs_50  = (
            (dataframe['ema_21'] - dataframe['ema_50']) / dataframe['ema_50']
        ).abs()
        not_extended = (
            (dataframe['atr_norm'] < 0.0041) &
            (ema21_vs_50 < 0.0037)
        )

        # ── Long entries ─────────────────────────────────────────────────────
        dataframe.loc[
            (
                (dataframe['htf_bias_1h'] == 1) &
                (dataframe['ema_9'] > dataframe['ema_21']) &
                (dataframe['rsi'] > 30) &
                (dataframe['rsi'] < 70) &
                (
                    (dataframe['close'] <= dataframe['ob_bull_top']) |
                    (dataframe['close'] <= dataframe['fvg_bull_top']) |
                    (dataframe['close'] <= dataframe['ema_21'] * 1.005)
                ) &
                (dataframe['volume_ratio'] > 0.8) &
                (dataframe['close'] > dataframe['open']) &
                (dataframe['volume'] > 0) &
                not_extended &
                ml_filter
            ),
            'enter_long'
        ] = 1

        # ── Short deshabilitado — solo long por ahora ─────────────────────
        # Razón: el modelo ML fue entrenado únicamente sobre setups largos.
        # Añadir shorts sin reentrenar introduciría sesgo. Habilitar después
        # de validar el sistema long en paper trading y reentrenar con labels
        # de short (exit_reason == "roi" en trades bajistas).
        dataframe['enter_short'] = 0

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[
            (
                (dataframe['bos_bearish'] == 1) &
                (dataframe['htf_bias_1h'] == -1)
            ),
            'exit_long'
        ] = 1

        dataframe['exit_short'] = 0
        return dataframe