# source: https://raw.githubusercontent.com/andythierry/freqtrade-strategies/e4701f998bc8077a3fc9d55cb0132cdb46c2416c/MultiStratAdaptative.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Github_andythierry_freqtrade_strategies__MultiStratAdaptative__20250901_063243 — stratégie compilée et adaptative pour Freqtrade

Combine Supertrend, RSI/EMA (v4 & v5) et un module simple de price-action.
Bascule automatiquement selon le régime de marché détecté en 1h (trend/range).
Utilise merge_informative_pair pour fiabiliser l'alignement 5m ⇄ 1h.
"""
from typing import Dict, List, Optional

import numpy as np
import pandas as pd
from pandas import DataFrame, Series

from freqtrade.strategy import (
    IStrategy,
    IntParameter,
    DecimalParameter,
    CategoricalParameter,
    merge_informative_pair,
)
import talib.abstract as ta


class Github_andythierry_freqtrade_strategies__MultiStratAdaptative__20250901_063243(IStrategy):
    timeframe = '5m'
    informative_timeframe = '1h'
    startup_candle_count: int = 240

    # ROI/SL — on gère réellement via custom_stoploss
    minimal_roi = {"0": 1000}
    stoploss = -0.20

    # Trailing doux (affiné dans custom_stoploss)
    trailing_stop = True
    trailing_only_offset_is_reached = True
    trailing_stop_positive = 0.01
    trailing_stop_positive_offset = 0.02

    # Hyperparams simples (hyperoptables)
    atr_mult_sl = DecimalParameter(1.0, 3.0, default=1.8, space='sell', decimals=1)
    adx_trend = IntParameter(15, 30, default=20, space='buy')
    bb_width_range = DecimalParameter(0.05, 0.12, default=0.08, space='buy', decimals=3)

    BAD_PAIRS: List[str] = [
        'AI16Z/USDT', 'FARTCOIN/USDT', 'XTZ/USDT', 'DOGE/USDT', 'TRUMP/USDT'
    ]

    MAX_CONSECUTIVE_LOSSES = 3
    DISABLE_FOR_HOURS = 6

    custom_info: Dict = {}
    use_custom_stoploss = True

    def informative_pairs(self):
        pairs = self.dp.current_whitelist()
        return [(p, self.informative_timeframe) for p in pairs]

    def populate_indicators(self, df: DataFrame, metadata: dict) -> DataFrame:
        pair = metadata['pair']

        # Flag paires à éviter
        df['bad_pair'] = pair in self.BAD_PAIRS

        # === Indicateurs 5m ===
        df['ema50'] = ta.EMA(df, timeperiod=50)
        df['ema200'] = ta.EMA(df, timeperiod=200)
        df['rsi'] = ta.RSI(df, timeperiod=14)
        df['atr'] = ta.ATR(df, timeperiod=14)
        df['adx'] = ta.ADX(df, timeperiod=14)

        # Supertrend (implémentation simple)
        atr10 = ta.ATR(df, timeperiod=10)
        factor = 3.0
        hl2 = (df['high'] + df['low']) / 2
        basic_ub = hl2 + factor * atr10
        basic_lb = hl2 - factor * atr10
        final_ub = basic_ub.copy()
        final_lb = basic_lb.copy()
        st = Series(index=df.index, dtype=float)
        for i in range(len(df)):
            if i == 0:
                st.iloc[i] = basic_lb.iloc[i]
                continue
            final_ub.iloc[i] = min(basic_ub.iloc[i], final_ub.iloc[i-1]) if df['close'].iloc[i-1] > final_ub.iloc[i-1] else basic_ub.iloc[i]
            final_lb.iloc[i] = max(basic_lb.iloc[i], final_lb.iloc[i-1]) if df['close'].iloc[i-1] < final_lb.iloc[i-1] else basic_lb.iloc[i]
            if st.iloc[i-1] == final_ub.iloc[i-1] and df['close'].iloc[i] <= final_ub.iloc[i]:
                st.iloc[i] = final_ub.iloc[i]
            elif st.iloc[i-1] == final_ub.iloc[i-1] and df['close'].iloc[i] > final_ub.iloc[i]:
                st.iloc[i] = final_lb.iloc[i]
            elif st.iloc[i-1] == final_lb.iloc[i-1] and df['close'].iloc[i] >= final_lb.iloc[i]:
                st.iloc[i] = final_lb.iloc[i]
            elif st.iloc[i-1] == final_lb.iloc[i-1] and df['close'].iloc[i] < final_lb.iloc[i]:
                st.iloc[i] = final_ub.iloc[i]
            else:
                st.iloc[i] = final_lb.iloc[i]
        df['supertrend'] = st
        df['supertrend_long'] = df['close'] > df['supertrend']

        # === Informative 1h (robuste avec merge_informative_pair) ===
        inf = self.dp.get_pair_dataframe(pair=pair, timeframe=self.informative_timeframe)
        if inf is None or inf.empty:
            for c in ['ema200_1h','ema50_1h','adx_1h','bb_width_1h','ema200_slope_1h']:
                df[c] = np.nan
        else:
            # TA-Lib renvoie des numpy.ndarray — éviter .replace sur ndarray
            inf['ema200'] = ta.EMA(inf, timeperiod=200)
            inf['ema50']  = ta.EMA(inf, timeperiod=50)
            inf['adx']    = ta.ADX(inf, timeperiod=14)
            up, mid, lo   = ta.BBANDS(inf['close'], timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
            mid_safe = np.where(mid == 0, np.nan, mid)
            # bb_width = (upper - lower) / middle
            inf['bb_width'] = (up - lo) / mid_safe
            inf['ema200_slope'] = inf['ema200'].diff()

            df = merge_informative_pair(
                df,
                inf[['date','ema200','ema50','adx','bb_width','ema200_slope']],
                self.timeframe,
                self.informative_timeframe,
                ffill=True,
            )

        # Régime de marché depuis les colonnes suffixées _1h
        df['is_trend'] = (df.get('adx_1h', np.nan) >= self.adx_trend.value) & (df.get('ema200_slope_1h', np.nan) > 0)
        df['is_range'] = (df.get('bb_width_1h', np.nan) <= self.bb_width_range.value)

        # Price-action basique
        df['bull_engulf'] = (
            (df['close'] > df['open']) & (df['open'].shift(1) > df['close'].shift(1)) &
            (df['close'] >= df['open'].shift(1)) & (df['open'] <= df['close'].shift(1))
        )
        body = (df['close'] - df['open']).abs()
        wick_up = df['high'] - df[['open', 'close']].max(axis=1)
        wick_down = df[['open', 'close']].min(axis=1) - df['low']
        df['bull_pin'] = (wick_down > body * 2) & (df['close'] > df['open'])

        # Momentum RSI/EMA (v4/v5)
        df['ema_fast'] = ta.EMA(df, timeperiod=12)
        df['ema_slow'] = ta.EMA(df, timeperiod=26)
        df['ema_cross_up'] = (df['ema_fast'] > df['ema_slow']) & (df['ema_fast'].shift(1) <= df['ema_slow'].shift(1))
        df['ema_trend_up'] = df['ema_fast'] > df['ema_slow']
        df['rsi_lt_50'] = df['rsi'] < 50
        df['rsi_lt_45'] = df['rsi'] < 45
        df['rsi_rebound'] = (df['rsi'].shift(1) < 30) & (df['rsi'] > df['rsi'].shift(1))

        return df

    def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
        disabled_tags = self._get_disabled_tags(metadata['pair'])

        # Supertrend (trend only)
        df.loc[
            (
                (df['is_trend']) &
                (~df['bad_pair']) &
                (df['supertrend_long']) &
                (df['close'] > df['ema50']) & (df['ema50'] > df['ema200']) &
                ('ST' not in disabled_tags)
            ),
            ['enter_long', 'enter_tag']
        ] = (1, 'ST')

        # RSI/EMA v5 (trend stricte)
        df.loc[
            (
                (df['is_trend']) &
                (~df['bad_pair']) &
                (df['ema_cross_up']) &
                (df['rsi'] > 50) &
                ('RSIEMA_V5' not in disabled_tags)
            ),
            ['enter_long', 'enter_tag']
        ] = (1, 'RSIEMA_V5')

        # RSI/EMA v4 (fallback range / hors-trend)
        df.loc[
            (
                (df['is_range'] | ~df['is_trend']) &
                (~df['bad_pair']) &
                (df['ema_trend_up']) &
                (df['rsi'] > 45) &
                ('RSIEMA_V4' not in disabled_tags)
            ),
            ['enter_long', 'enter_tag']
        ] = (1, 'RSIEMA_V4')

        # Patterns en range
        df.loc[
            (
                (df['is_range']) &
                (~df['bad_pair']) &
                (df['bull_engulf'] | df['bull_pin']) &
                ('PATTERN' not in disabled_tags)
            ),
            ['enter_long', 'enter_tag']
        ] = (1, 'PATTERN')

        return df

    def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
        df.loc[
            (
                (df['rsi'] > 70) |
                ((df['is_trend']) & (df['close'] < df['ema50'])) |
                ((df['is_range']) & (df['rsi'] < 40))
            ),
            'exit_long'
        ] = 1
        return df

    def custom_stoploss(self, pair: str, trade, current_time: pd.Timestamp, current_rate: float,
                        current_profit: float, **kwargs) -> float:
        df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if df is None or df.empty:
            return 1.0
        atr = df['atr'].iloc[-1] if 'atr' in df.columns else None
        if atr is None or np.isnan(atr):
            return 1.0
        sl_distance = float(self.atr_mult_sl.value) * float(atr)
        sl_pct = sl_distance / max(current_rate, 1e-9)
        if current_profit > 0.02:
            sl_pct = min(sl_pct, max(0.01, current_profit * 0.6))
        return float(max(0.01, min(0.20, sl_pct)))

    # ===== Kill-switch =====
    def _get_disabled_tags(self, pair: str) -> set:
        key = f"disabled_tags::{pair}"
        # Always work with tz-aware UTC timestamps
        now_utc = pd.Timestamp.now(tz='UTC')
        default_until = pd.Timestamp.min.tz_localize('UTC')
        info = self.custom_info.get(key, {"tags": set(), "until": default_until})
        # Reactivate tags if the disable window expired
        if now_utc > info.get('until', default_until):
            info['tags'] = set()
            info['until'] = default_until
        self.custom_info[key] = info
        return info['tags']

    def _disable_tag(self, pair: str, tag: str):
        key = f"disabled_tags::{pair}"
        now_utc = pd.Timestamp.now(tz='UTC')
        until = now_utc + pd.Timedelta(hours=self.DISABLE_FOR_HOURS)
        info = self.custom_info.get(key, {"tags": set(), "until": until})
        info['tags'].add(tag)
        info['until'] = until
        self.custom_info[key] = info
        self.logger.warning(f"[KILL] Désactivation temporaire du tag {tag} sur {pair} jusqu'au {until}")

    def on_trade_closed(self, trade, order, **kwargs) -> None:
        try:
            pair = trade.pair
            tag = (trade.enter_tag or '').upper()
            if not tag:
                return
            df_hist = self._get_trade_history(pair, tag, limit=10)
            if df_hist is None or df_hist.empty:
                return
            consec_losses = 0
            for p in df_hist['close_profit'].iloc[::-1]:
                if p <= 0:
                    consec_losses += 1
                else:
                    break
            if consec_losses >= self.MAX_CONSECUTIVE_LOSSES:
                self._disable_tag(pair, tag)
        except Exception as e:
            self.logger.warning(f"on_trade_closed error: {e}")

    def _get_trade_history(self, pair: str, tag: str, limit: int = 20) -> Optional[DataFrame]:
        try:
            trades = self.dp.get_trade_history(pair=pair, limit=limit)
            if not trades:
                return None
            rows = []
            for t in trades:
                if (t.enter_tag or '').upper() == tag.upper():
                    rows.append({'close_profit': t.close_profit, 'close_date': t.close_date})
            if not rows:
                return None
            return pd.DataFrame(rows).sort_values('close_date')
        except Exception:
            return None

    def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
                             time_in_force: str, enter_tag: str, **kwargs) -> bool:
        df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if df is not None and not df.empty and bool(df['bad_pair'].iloc[-1]):
            return False
        disabled = self._get_disabled_tags(pair)
        if enter_tag and enter_tag.upper() in disabled:
            return False
        return True

    # def custom_stake_amount(self, pair: str, current_price: float, proposed_stake: float, **kwargs) -> float:
    #     return proposed_stake
