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

Objectif
--------
Combiner plusieurs approches (Supertrend, momentum RSI/EMA, pattern price-action)
et activer automatiquement l'approche la plus pertinente selon le régime de marché
(détecté sur une timeframe informative 1h : tendance / range / volatilité).

Points clés
-----------
- Multi-stratégies dans **un seul fichier** via des tags d'entrée :
  - "ST"           → Supertrend (trend following)
  - "RSIEMA_V4"    → Momentum (ancienne MagicStratScalp_v4)
  - "RSIEMA_V5"    → Momentum (variante v5, plus stricte)
  - "PATTERN"      → Price-action (engloutissant haussier, pin bar)
- Sélection par **régime de marché** (1h) :
  - Trend si ADX(14) ≥ 20 et pente EMA200 positive ⇒ favorise Supertrend + RSIEMA_V5
  - Range si BandWidth Bollinger ≤ 8% ⇒ favorise PATTERN + RSIEMA_V4
  - Sinon ⇒ fallback momentum léger (RSIEMA_V4)
- **Kill-switch par sous-stratégie** : si > N pertes consécutives (rolling) pour un tag,
  on désactive temporairement ce tag pendant M heures.
- **Protections** (exposables via `protections` + garde-fous locaux) :
  - Cooldown après perte
  - Stoploss dynamique (ATR + trailing léger)
- **Nettoyage paires** : possibilité d'exclure les paires destructrices via `BAD_PAIRS`.

Compatibilité
-------------
- Testé avec Freqtrade ≥ 2024.3
- Timeframe par défaut : 5m ; informative : 1h

Conseils d'utilisation
----------------------
- Dans le `config.json` : `"log_trade_details": true`, `"verbosity": 3`
- Lancer normal : `freqtrade trade --strategy Github_andythierry_freqtrade_strategies__MultiStratAdaptative_local__20250901_063243`

"""
from functools import lru_cache
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
from freqtrade.exchange import timeframe_to_minutes

import talib.abstract as ta
from technical.indicators import RMI
from technical.util import resample_to_interval, heikinashi


class Github_andythierry_freqtrade_strategies__MultiStratAdaptative_local__20250901_063243(IStrategy):
    # --- Config de base ---
    timeframe = '5m'
    informative_timeframe = '1h'
    startup_candle_count: int = 240  # pour indicateurs 1h

    # ROI/Stoploss par défaut (le SL réel est géré par custom_stoploss)
    minimal_roi = {"0": 1000}
    stoploss = -0.20

    # Trailing (soft) — sera renforcé en custom_stoploss
    trailing_stop = True
    trailing_only_offset_is_reached = True
    trailing_stop_positive = 0.01
    trailing_stop_positive_offset = 0.02

    # Hyperparams simples (ajustables via Hyperopt)
    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)

    # Exclusion de paires destructrices (customisable)
    BAD_PAIRS: List[str] = [
        'AI16Z/USDT', 'FARTCOIN/USDT', 'XTZ/USDT', 'DOGE/USDT', 'TRUMP/USDT'
    ]

    # Kill-switch par sous-stratégie
    MAX_CONSECUTIVE_LOSSES = 3
    DISABLE_FOR_HOURS = 6

    # Internals (stocke état des sous-stratégies)
    custom_info: Dict = {}

    # Protections globales (facultatif; sinon utiliser protections dans config)
    use_custom_stoploss = True

    def informative_pairs(self):
        # On veut l'informative 1h pour chaque paire active
        return [(self.dp.current_whitelist(), self.informative_timeframe)]

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

        # Filtre paires mauvaises
        df['bad_pair'] = pair in self.BAD_PAIRS

        # Base 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)
        df['tr'] = ta.TRANGE(df)
        atr = ta.ATR(df, timeperiod=10)
        factor = 3.0
        hl2 = (df['high'] + df['low']) / 2
        basic_ub = hl2 + factor * atr
        basic_lb = hl2 - factor * atr
        final_ub = basic_ub.copy()
        final_lb = basic_lb.copy()
        st = Series(index=df.index, dtype=float)
        dir_long = True
        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
        inf = self.dp.get_pair_dataframe(pair=pair, timeframe=self.informative_timeframe)
        inf['ema200_1h'] = ta.EMA(inf, timeperiod=200)
        inf['ema50_1h'] = ta.EMA(inf, timeperiod=50)
        inf['adx_1h'] = ta.ADX(inf, timeperiod=14)
        # BB width en % (1h)
        upper, middle, lower = ta.BBANDS(inf['close'], timeperiod=20, nbdevup=2, nbdevdn=2)
        inf['bb_width'] = (upper - lower) / middle.replace(0, np.nan)
        # pente EMA200 (1h)
        inf['ema200_slope'] = inf['ema200_1h'].diff()

        df = df.merge(
            inf[['date', 'ema200_1h', 'ema50_1h', 'adx_1h', 'bb_width', 'ema200_slope']].rename(columns={'date': 'date_inf'}),
            left_on='date', right_on='date_inf', how='left'
        )
        df.drop(columns=['date_inf'], inplace=True)

        # Détection de régime
        df['is_trend'] = (df['adx_1h'] >= self.adx_trend.value) & (df['ema200_slope'] > 0)
        df['is_range'] = (df['bb_width'] <= self.bb_width_range.value)

        # Price-action basique (patterns)
        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

    # --- Entrées ---
    def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
        # Kill-switch: récupérer l'état des sous-strategies désactivées
        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 a priori, 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 momentum / range permissif)
        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')

        # Pattern price-action (range only)
        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

    # --- Sorties ---
    def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
        # Exits simples, le SL/Trailing fait le gros du travail
        df.loc[
            (
                (df['rsi'] > 70) |  # take profit simple
                ((df['is_trend']) & (df['close'] < df['ema50'])) |  # perte de momentum
                ((df['is_range']) & (df['rsi'] < 40))
            ),
            'exit_long'
        ] = 1
        return df

    # --- Stoploss dynamique ---
    def custom_stoploss(self, pair: str, trade, current_time: pd.Timestamp, current_rate: float,
                        current_profit: float, **kwargs) -> float:
        # ATR sur last candle 5m
        df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if df is None or df.empty:
            return 1.0  # fallback
        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 = self.atr_mult_sl.value * atr
        # Converti en pourcentage de prix
        sl_pct = sl_distance / max(current_rate, 1e-9)
        # Trailing supplémentaire si en gain
        if current_profit > 0.02:
            sl_pct = min(sl_pct, max(0.01, current_profit * 0.6))
        return max(0.01, min(0.20, sl_pct))

    # --- Hooks pour Kill-switch ---
    def check_exit_timeout(self, pair: str, trade, order) -> bool:
        # non utilisé ici
        return False

    def _get_disabled_tags(self, pair: str) -> set:
        key = f"disabled_tags::{pair}"
        info = self.custom_info.get(key, {"tags": set(), "until": pd.Timestamp.min})
        # Réactive si délai passé
        if pd.Timestamp.utcnow() > info.get('until', pd.Timestamp.min):
            info['tags'] = set()
        self.custom_info[key] = info
        return info['tags']

    def _disable_tag(self, pair: str, tag: str):
        key = f"disabled_tags::{pair}"
        until = pd.Timestamp.utcnow() + 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

    def on_trade_closed(self, trade, order, **kwargs) -> None:
        """Kill-switch : désactive un tag s'il enchaîne trop de pertes."""
        try:
            pair = trade.pair
            tag = (trade.enter_tag or '').upper()
            profit = trade.close_profit or 0.0
            if not tag:
                return
            # Historique des X derniers trades pour ce tag/paire
            df_hist = self._get_trade_history(pair, tag, limit=10)
            if df_hist is None or df_hist.empty:
                return
            # Compte pertes consécutives
            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)
                self.log(f"[KILL] Désactivation temporaire du tag {tag} sur {pair} ({consec_losses} pertes consécutives)")
        except Exception as e:
            self.log(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

    # --- Protections via hooks (en plus des protections config) ---
    def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
                             time_in_force: str, enter_tag: str, **kwargs) -> bool:
        # Bloque si paire marquée "bad_pair"
        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
        # Kill-switch local
        disabled = self._get_disabled_tags(pair)
        if enter_tag and enter_tag.upper() in disabled:
            return False
        return True

    # (Optionnel) position sizing custom : ici on laisse la stake du config.json
    # def custom_stake_amount(self, pair: str, current_price: float, proposed_stake: float, **kwargs) -> float:
    #     return proposed_stake
