# source: https://raw.githubusercontent.com/NGUEND-ARTHUR/smartbot_freqtrade/93aeed0f6e8dcd2c323d9fd2e1ef41630c379e14/user_data/strategies/GridDcaHybrid.py
# directory_url: https://github.com/NGUEND-ARTHUR/smartbot_freqtrade/blob/main/user_data/strategies/
# User: NGUEND-ARTHUR
# Repository: smartbot_freqtrade
# --------------------# Github_NGUEND_ARTHUR_smartbot_freqtrade__GridDcaHybrid__20260813_103416.py
# Stratégie "grid-like" pour Freqtrade : combine Bollinger Bands + RSI pour détecter
# une fourchette de marché (range), et utilise le DCA (position adjustment) de Freqtrade
# pour rentrer progressivement, comme le ferait un grid bot classique.
#
# Logique :
#   - On achète une première tranche quand le prix touche la bande basse de Bollinger
#     ET que le RSI indique une survente (marché probablement en range, pas en forte tendance baissière).
#   - Si le prix continue de baisser après l'achat initial, on ajoute des tranches
#     supplémentaires à intervalles de prix définis (comme des niveaux de grille) --
#     c'est le rôle d'adjust_trade_position.
#   - On vend par paliers de profit (comme un grid bot le ferait à chaque niveau supérieur).
#
# ATTENTION : dry_run=true est activé dans config.json. NE JAMAIS passer en argent réel
# avant d'avoir backtesté sur plusieurs mois ET fait tourner en dry-run pendant plusieurs semaines.

from freqtrade.strategy import IStrategy, DecimalParameter, IntParameter
from pandas import DataFrame
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib


class Github_NGUEND_ARTHUR_smartbot_freqtrade__GridDcaHybrid__20260813_103416(IStrategy):
    INTERFACE_VERSION = 3

    # Timeframe des bougies analysées. 15m = bon compromis réactivité/bruit pour un range.
    timeframe = "15m"

    # Stoploss "catastrophe" au niveau du trade (filet de sécurité global, pas le stoploss principal
    # qui est ici géré par la logique de sortie par paliers ci-dessous)
    stoploss = -0.15

    # On désactive le trailing stop classique : la stratégie gère ses propres paliers de sortie
    trailing_stop = False

    # CORRECTIF #4 : exit_profit_only=True a de nouveau produit un résultat catastrophique
    # (-22.21%, drawdown 71.60%) -- même avec dca_multiplier=1.0, laisser une position
    # perdante courir jusqu'au stoploss (-15%) plutôt que de sortir tôt via le signal reste
    # structurellement dangereux. On revient définitivement à False. On garde en revanche
    # la confirmation RSI > 50 ajoutée sur le signal de sortie (jamais testée isolément).
    exit_profit_only = False

    # ROI (Return on Investment) par palier : plus le trade est ouvert longtemps, plus on accepte
    # un profit minimal pour sortir. Simule des ventes "de grille" à différents seuils de gain.
    minimal_roi = {
        "0": 0.015,   # sortir dès 1.5% de gain si ça arrive tout de suite
        "60": 0.01,   # après 60 min, se contenter de 1%
        "180": 0.005, # après 3h, se contenter de 0.5%
        "720": 0      # après 12h, sortir dès le retour à l'équilibre
    }

    # --- Paramètres ajustables (optimisables via freqtrade hyperopt) ---
    bb_period = IntParameter(15, 30, default=20, space="buy")
    rsi_period = IntParameter(10, 21, default=14, space="buy")
    rsi_buy_threshold = IntParameter(25, 40, default=32, space="buy")

    # Nombre max de tranches DCA (= nombre de "niveaux de grille" à la baisse)
    max_dca_entries = 3
    # Distance en % entre chaque niveau de grille
    dca_price_step = 0.025  # 2.5%
    # CORRECTIF #2 : multiplicateur réduit de 1.3 à 1.0 -- chaque tranche DCA a maintenant
    # la même taille que la précédente, au lieu de grossir à chaque niveau. Ça évite que le
    # bot investisse de plus en plus dans une position qui continue de baisser.
    dca_multiplier = 1.0

    # Autorise Freqtrade à ajuster la position (nécessaire pour le DCA / grid-like)
    position_adjustment_enable = True
    max_entry_position_adjustment = max_dca_entries

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # Bollinger Bands : définissent la fourchette (range) dans laquelle on va "griller"
        bollinger = qtpylib.bollinger_bands(
            qtpylib.typical_price(dataframe), window=self.bb_period.value, stds=2
        )
        dataframe["bb_lowerband"] = bollinger["lower"]
        dataframe["bb_middleband"] = bollinger["mid"]
        dataframe["bb_upperband"] = bollinger["upper"]

        # RSI : confirme qu'on est en survente, pas en tendance baissière forte
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=self.rsi_period.value)

        # ADX : mesure la force de la tendance. Un ADX faible = marché en range,
        # condition idéale pour une logique de type grid.
        dataframe["adx"] = ta.ADX(dataframe)

        # CORRECTIF #2 : moyenne mobile longue (200 périodes) pour détecter la tendance
        # de fond. On n'achète que si le prix est au-dessus -- ça évite d'accumuler du DCA
        # sur un actif en tendance baissière soutenue (le problème observé sur ETH/USDT).
        dataframe["sma200"] = ta.SMA(dataframe, timeperiod=200)

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[
            (
                (dataframe["close"] <= dataframe["bb_lowerband"])
                & (dataframe["rsi"] < self.rsi_buy_threshold.value)
                & (dataframe["adx"] < 25)  # on évite d'entrer si une tendance forte est en cours
                & (dataframe["close"] > dataframe["sma200"])  # CORRECTIF #2 : tendance de fond haussière/neutre uniquement
                & (dataframe["volume"] > 0)
            ),
            "enter_long",
        ] = 1
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[
            (
                (dataframe["close"] >= dataframe["bb_middleband"])
                # CORRECTIF #3 : confirmation RSI > 50 -- exige que la dynamique haussière
                # soit vraiment établie, pas juste un rebond ponctuel qui touche la bande
                # moyenne avant de repartir à la baisse.
                & (dataframe["rsi"] > 50)
                & (dataframe["volume"] > 0)
            ),
            "exit_long",
        ] = 1
        return dataframe

    def adjust_trade_position(self, trade, current_time, current_rate,
                               current_profit, min_stake, max_stake,
                               current_entry_rate, current_exit_rate,
                               current_entry_profit, current_exit_profit,
                               **kwargs):
        """
        C'est ici que se joue le comportement "grid" : si le prix continue de baisser
        après l'achat initial, on ajoute des tranches à intervalles réguliers,
        exactement comme des niveaux de grille qui s'activent un par un.
        """
        if current_profit > -self.dca_price_step:
            # Le prix n'a pas encore assez baissé pour déclencher le niveau suivant
            return None

        count_of_entries = trade.nr_of_successful_entries
        if count_of_entries >= self.max_dca_entries + 1:
            # On a déjà utilisé tous les niveaux de grille disponibles
            return None

        try:
            # Chaque niveau ajoute une tranche de même taille que la mise initiale
            # (dca_multiplier = 1.0, voir CORRECTIF #2 ci-dessus)
            stake_amount = trade.stake_amount * self.dca_multiplier
            return stake_amount
        except Exception:
            return None