# GridDcaHybrid.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 GridDcaHybrid(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