📝 Maîtriser les fichiers de stratégies ...

📝 Maîtriser les fichiers de stratégies Freqtrade : Le Guide de référence

Jul 25, 2025

Tu as configuré tes bots grâce au guide des fichiers de configuration, mais comment créer le moteur logique de tes robots ? Les fichiers de stratégies définissent la logique de trading : quand acheter, quand vendre, et comment gérer le risque.

Ce guide complet te servira de référence permanente. Garde-le dans tes favoris ! 📌

image

📌 Note fork : la structure d'une stratégie est la même sur le Freqtrade officiel et sur mon fork freqtrade-ultimate. Mais le fork t'apporte des outils côté stratégie : choix du sampler d'hyperopt + une loss profit/drawdown, un filtre de pairlist pour les shorts, et surtout la gestion automatique des fermetures externes sur Hyperliquid (j'y reviens). Présentation complète : mes forks Freqtrade et FreqUI.


🎯 Qu'est-ce qu'un fichier de stratégie ?

Les fichiers de stratégie (en .py) sont le moteur intelligent de tes bots. Ils contiennent :

  • 📊 Les indicateurs techniques à calculer

  • 🎯 Les conditions d'entrée en position

  • 🚪 Les conditions de sortie et prises de profit

  • 🛡️ La gestion du risque (stoploss, ROI, trailing)

  • ⚙️ Les paramètres optimisables pour l'hyperoptimisation

Point crucial : pour lancer n'importe quelle action, tu as TOUJOURS besoin de deux fichiers : un fichier de stratégie (.py) et un fichier de configuration (.json).


📚 Ressources essentielles


🏗️ Structure d'un fichier stratégie

Un fichier de stratégie suit toujours la même structure :

# 1. Imports nécessaires
from freqtrade.strategy import IStrategy
import pandas as pd
import talib.abstract as ta

# 2. Définition de la classe
class ma_strategie(IStrategy):

    # 3. Paramètres de risque
    minimal_roi = {"0": 0.1}
    stoploss = -0.1

    # 4. Configuration
    timeframe = '1h'

    # 5. Trois fonctions principales
    def populate_indicators()
    def populate_entry_trend()
    def populate_exit_trend()

🎓 Niveau 1 : Stratégie simple (basic_emas)

Commençons avec un exemple concret : la stratégie basic_emas.py, qui utilise le croisement de moyennes mobiles.

1️⃣ Les imports

from functools import reduce
from freqtrade.strategy import IStrategy
from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter
from pandas import DataFrame
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
  • IStrategy : la classe de base obligatoire

  • pandas : pour manipuler les données (DataFrame)

  • talib : bibliothèque d'indicateurs techniques (il existe aussi ta et d'autres)

  • qtpylib : utilitaires supplémentaires (comme crossed_above)

2️⃣ Définition de la classe

class basic_emas(IStrategy):
    # Le code de la stratégie sera ici

⚠️ Important : le nom de la classe (basic_emas) doit correspondre EXACTEMENT au champ "strategy" de ta config JSON, ou alors tu dois préciser --strategy basic_emas dans ta commande :

freqtrade trade --strategy basic_emas --config live_configs/...
freqtrade backtesting --strategy basic_emas --config backtest_configs/...

3️⃣ Paramètres de risque / take profit

minimal_roi = {
    "0": 1.0,      # 100% de gain → vendre immédiatement
    "10000": 0.40, # 40% après 10000 minutes → vendre
    "25000": 0.15, # 15% après 25000 minutes → vendre
    "50000": 0     # Break-even après 50000 minutes → vendre
}
stoploss = -0.35   # Stop à -35%

📊 Le ROI progressif permet de prendre des profits à différents niveaux selon le temps passé en position. Le dernier niveau ("50000": 0) est un break-even.

💡 Ces valeurs sont en minutes, peu importe la timeframe ! On en parle en détail dans Comment optimiser une stratégie ? (Guide complet + Vidéo).

⚠️ Piège du trailing stop natif : le trailing_stop intégré de Freqtrade peut, dans certaines configs, produire des « profits fantômes » et des sorties incohérentes. Par expérience, si tu as besoin d'un stop suiveur fiable, je te conseille de le coder toi-même dans custom_exit() plutôt que de te reposer sur le trailing natif.

4️⃣ Configuration de base

timeframe = '1d'  # Timeframe journalière

Timeframes courantes : 1m, 5m, 15m, 30m, 1h, 4h, 1d.

5️⃣ Les trois fonctions principales

A. Calcul des indicateurs

def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    dataframe['ema12'] = ta.EMA(dataframe, timeperiod=12)
    dataframe['ema80'] = ta.EMA(dataframe, timeperiod=80)
    return dataframe

Cette fonction calcule les indicateurs et les ajoute au DataFrame. Elle est appelée une fois par paire et par bougie.

B. Conditions d'entrée

def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    dataframe.loc[
        (
            qtpylib.crossed_above(dataframe['ema12'], dataframe['ema80']) &
            (dataframe['volume'] > 0)
        ),
        'enter_long'] = 1
    return dataframe

Signal d'achat quand l'EMA 12 croise l'EMA 80 à la hausse ET que le volume est positif (sécurité).

💡 Alternative sans qtpylib (fait la même chose) :

(dataframe['ema12'] > dataframe['ema80']) &
(dataframe['ema12'].shift(1) <= dataframe['ema80'].shift(1))

C. Conditions de sortie

def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -&gt; DataFrame:
    dataframe.loc[
        (
            qtpylib.crossed_above(dataframe['ema80'], dataframe['ema12']) &
            (dataframe['volume'] > 0)
        ),
        'exit_long'] = 1
    return dataframe

Signal de vente : le croisement inverse (baissier).

Le fichier complet forme une stratégie très basique mais parfaitement valide :

from functools import reduce
from freqtrade.strategy import IStrategy
from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter
from pandas import DataFrame

import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib


class basic_emas(IStrategy):
    INTERFACE_VERSION: int = 3
    minimal_roi = {
        "0": 1.0,
        "10000": 0.40,
        "25000": 0.15,
        "50000": 0
    }

    stoploss = -0.35

    timeframe = '1d'

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -&gt; DataFrame:
        dataframe['ema12'] = ta.EMA(dataframe, timeperiod=12)
        dataframe['ema80'] = ta.EMA(dataframe, timeperiod=80)
        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -&gt; DataFrame:
        dataframe.loc[
            (
                qtpylib.crossed_above(dataframe['ema12'], dataframe['ema80']) &
                (dataframe['volume'] > 0)
            ),
            'enter_long'] = 1
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -&gt; DataFrame:
        dataframe.loc[
            (
                qtpylib.crossed_above(dataframe['ema80'], dataframe['ema12']) &
                (dataframe['volume'] > 0)
            ),
            'exit_long'] = 1
        return dataframe

🔧 Customiser davantage la stratégie (les callbacks)

On peut customiser une stratégie en réécrivant les fonctions dites de « callback », par exemple :

  • custom_stake_amount() : ajuster la somme allouée à une position

  • custom_exit() : sorties personnalisées (crash de marché, objectif de portefeuille atteint, ATH du BTC…) — et c'est aussi là qu'on code un trailing stop fiable

  • custom_entry_price() / custom_exit_price() : prix d'entrée/sortie spécifique

  • adjust_trade_position() : changer la taille d'une position dans le temps (DCA)

📘 Toutes les fonctions de callback sont listées dans la documentation officielle.

💡 Pour que les exemples de callbacks ci-dessous tournent, ajoute ces imports en haut de ta stratégie : from datetime import datetime, from typing import Optional, from freqtrade.persistence import Trade.

Par exemple, pour ajouter à basic_emas une logique de DCA qui recharge la position quand un nouveau signal d'achat apparaît, on ajoute adjust_trade_position() :

    # Nombre de DCA maximum autorisé par trade
    max_dca = 3

    def adjust_trade_position(self, trade: Trade, current_time, current_rate: float,
                              current_profit: float, min_stake: float, max_stake: float,
                              **kwargs) -&gt; Optional[float]:
        """
        Recharge une position si elle est en perte et que le signal d'entrée est de nouveau valide.
        """
        # 1. En perte ?
        if current_profit >= 0:
            return None

        # 2. Trop de DCA ?
        filled_orders = trade.select_filled_orders(trade.entry_side)
        if len(filled_orders) > self.max_dca:
            return None

        # 3. Nouveau signal d'achat ?
        dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
        if dataframe is None or dataframe.empty:
            return None

        last_row = dataframe.iloc[-1]
        if last_row.get('enter_long', 0) != 1:
            return None

        # 4. Recalculer le montant du DCA (simple : x1.5)
        try:
            initial_stake = filled_orders[0].cost
            dca_stake = min(initial_stake * 1.5, max_stake)
            return dca_stake
        except Exception as e:
            logger.error(f"DCA error for {trade.pair}: {e}")
            return None

⚙️ Ordre d'exécution du bot (fonctions et callbacks)

Avant d'introduire des fonctions comme adjust_trade_position(), il faut comprendre ce qui se passe à chaque cycle. Freqtrade exécute chaque boucle d'analyse selon une séquence définie (le bot execution logic). Pour chaque paire de ta pairlist, dans l'ordre :

  • Récupération des paires tradables (+ les paires des positions ouvertes si elles ne sont plus tradables)

  • Téléchargement des données OHLCV (une fois par bougie)

  • Appel à bot_start()

  • Exécution (deux fois) de populate_indicators(), populate_entry_trend(), populate_exit_trend()

  • Synchronisation des ordres limites ouverts, puis check_entry_timeout(), check_exit_timeout() et adjust_entry_price() (voir le guide des ordres)

  • Évaluation des sorties : stoploss → ROI → signaux (exit_long/exit_short) → custom_exit() → custom_stoploss()

  • Confirmation des sorties via confirm_trade_exit() avant placement de l'ordre

  • Ajustement de la taille des positions via adjust_trade_position() (réduire ou recharger — le DCA)

  • Vérification des slots disponibles (max_open_trades) : si un signal d'entrée est valide, l'entrée est envoyée. Le prix vient de entry_pricing ou custom_entry_price(), la taille de custom_stake_amount() (sinon stake_amount), et confirm_trade_entry() doit retourner True

Ces étapes sont rejouées toutes les process_throttle_secs secondes (défini dans la config). Plus ta timeframe est haute, plus tu peux mettre une grande valeur. Détails dans la doc officielle.

🌊 Important si tu trades sur Hyperliquid : une position peut disparaître de l'exchange sans que ton bot l'ait demandé (ADL, liquidation, fermeture manuelle). Avec le Freqtrade officiel, le bot tourne alors en boucle sur un « trade fantôme ». freqtrade-ultimate détecte et ferme proprement ces fermetures externes automatiquement — rien à coder dans ta stratégie.

⚙️ Spécificité liée à l'hyperopt

Par défaut, lors d'un hyperopt : populate_indicators() est appelé une seule fois par paire, puis chaque jeu de paramètres testé (epoch) déclenche populate_entry_trend() et populate_exit_trend().

En ajoutant l'option --analyze-per-epoch à freqtrade hyperopt, tu demandes à recalculer les indicateurs à chaque epoch. Utile pour optimiser des indicateurs dont les périodes sont elles-mêmes des paramètres (EMA, VWAP configurables), mais nettement plus coûteux en calcul. Plus de détails : Comment optimiser une stratégie ?

⚠️ Le piège du fichier .json co-localisé : quand tu lances un hyperopt, Freqtrade peut écrire les meilleurs paramètres dans un MaStrategie.json posé à côté de ton MaStrategie.py. Ce fichier écrase silencieusement tes buy_params/sell_params définis dans le code Python. Autrement dit : si tes valeurs live ne correspondent pas à ce que tu lis dans le .py, vérifie toujours s'il existe un .json du même nom — c'est lui qui fait foi.

💡 Bonus fork : freqtrade-ultimate te laisse choisir l'algorithme d'optimisation via --sampler (TPE, CMA-ES, NSGA-II/III, GP, QMC) et fournit une loss profit/drawdown. Tout est expliqué dans le guide Hyperopt & Hyperopt-Loss et dans le post sur les forks.


🚀 Niveau 2 : Stratégie avancée (simple_vwap_v1)

Pour les traders plus expérimentés, examinons simple_vwap_v1, présentée en détail dans 🧩 Simple VWAP v1 — la stratégie du placement constant.

🎯 Philosophie : exposition maximale au marché — conditions d'entrée ultra-permissives et DCA intelligent.

📊 Indicateurs sophistiqués

def VWAPB(dataframe, window_size=20, num_of_std=1):
    """Bandes VWAP avec écart-type"""
    df = dataframe.copy()
    df['vwap'] = qtpylib.rolling_vwap(df, window=window_size)
    rolling_std = df['vwap'].rolling(window=window_size).std()
    df['vwap_low'] = df['vwap'] - (rolling_std * num_of_std)
    df['vwap_high'] = df['vwap'] + (rolling_std * num_of_std)
    return df['vwap_low'], df['vwap'], df['vwap_high']

Paramètres optimisables

# Stoploss personnalisé optimisable
my_custom_stoploss = DecimalParameter(
    -0.45, -0.2,         # Plage : -45% à -20%
    decimals=2,
    default=-0.37,       # Valeur par défaut : -37%
    space="sell",
    optimize=True        # Peut être optimisé
)

# Ratio du capital à utiliser
tradable_balance_ratio = CategoricalParameter(
    [0.25, 0.35, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
    default=0.35,
    space="buy",
    optimize=True
)

Ces paramètres peuvent être optimisés automatiquement avec la commande hyperopt (voir aussi DecimalParameter et CategoricalParameter).

Gestion avancée des positions

1. Taille dynamique selon la volatilité (voir aussi le post sur le position sizing) :

def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
                        proposed_stake: float, min_stake: float, max_stake: float,
                        **kwargs) -&gt; float:
    # Calcul de l'ATR pour mesurer la volatilité
    recent_atr_pct = (recent_high - recent_low).mean() / recent_close

    # Réduction si volatilité élevée
    if recent_atr_pct > 0.102:    # >10.2%
        volatility_adjustment = 0.5   # -50%
    elif recent_atr_pct > 0.061:  # >6.1%
        volatility_adjustment = 0.75  # -25%
    else:
        volatility_adjustment = 1.0   # Taille normale

2. Système DCA intelligent (rechargement progressif des positions perdantes) :

def adjust_trade_position(self, trade: Trade, current_time: datetime, current_rate: float,
                          current_profit: float, min_stake: float, max_stake: float,
                          **kwargs) -&gt; Optional[float]:
    """
    Système de DCA (Dollar Cost Averaging) intelligent.
    Rechargements déclenchés à des seuils de perte croissants, avec des montants croissants.

    Exemple avec les paramètres par défaut :
    - 1er DCA à -1.2% avec 1.8x le montant initial
    - 2e DCA à -3.6% avec 3.24x le montant initial
    - 3e DCA à -10.8% avec 5.83x le montant initial
    - 4e DCA à -32.4% avec 10.5x le montant initial
    """
    # Ne rien faire si on n'est pas en perte suffisante
    if current_profit > self.initial_safety_order_trigger.value:
        return None

    # Récupérer l'historique des ordres
    filled_buys = trade.select_filled_orders(trade.entry_side)
    count_of_buys = len(filled_buys)

    # Vérifier qu'on n'a pas atteint la limite de DCA
    if not (1 <= count_of_buys <= self.max_so_multiplier_orig.value):
        return None

    # Calcul du seuil de déclenchement pour ce DCA
    if self.safety_order_step_scale.value == 1:
        # Espacement linéaire
        safety_order_trigger = abs(self.initial_safety_order_trigger.value) * count_of_buys
    else:
        # Espacement exponentiel (recommandé)
        initial_trigger = abs(self.initial_safety_order_trigger.value)
        scale = self.safety_order_step_scale.value
        if scale > 1:
            numerator = scale * (pow(scale, count_of_buys - 1) - 1)
            denominator = scale - 1
            safety_order_trigger = initial_trigger + (initial_trigger * scale * numerator / denominator)
        else:
            numerator = 1 - pow(scale, count_of_buys - 1)
            denominator = 1 - scale
            safety_order_trigger = initial_trigger + (initial_trigger * scale * numerator / denominator)

    # Vérifier si on a atteint le seuil de DCA
    if current_profit > (-1 * abs(safety_order_trigger)):
        return None

    # Calcul du montant du DCA
    try:
        actual_initial_stake = filled_buys[0].cost
        stake_amount = actual_initial_stake * pow(
            self.safety_order_volume_scale.value,
            count_of_buys - 1
        )
        return stake_amount
    except Exception as e:
        logger.error(f'Erreur DCA pour {trade.pair}: {str(e)}')
        return None

⚠️ Le DCA agressif (rechargement des positions perdantes) peut transformer une petite perte en gros trou si le marché continue contre toi. Dimensionne toujours en imaginant le pire scénario — voir le post sur le position sizing.

📈 Configuration graphique

Affiche tes indicateurs dans FreqUI :

plot_config = {
    "main_plot": {
        "EMA": {"color": "#ffffff", "type": "line"},
        "VWAP_low": {"color": "#53c3eb", "type": "line"},
    },
    "subplots": {
        "CCI": {
            "CCI": {"color": "#ffffff", "type": "line"},
        }
    }
}

Résultat dans FreqUI :

image


🔧 Bonnes pratiques

✅ À faire

  • Nommage cohérent : nom de classe = nom dans la config, en minuscules avec underscores (ma_super_strategie)

  • Logs pour debug : un simple print(dataframe) dans populate_indicators() aide énormément à comprendre ce qui se passe

  • Commentaires détaillés : explique ta logique, documente les paramètres

❌ Erreurs courantes

  • Nom de classe invalide : class Ma-Super-Strategie ❌ (tirets interdits) → class ma_super_strategie ✅

  • Oubli du volume > 0 dans les conditions (évite de trader sur des bougies sans volume)

  • Stratégie en avance sur le futur (look-ahead bias) : ne te sers jamais d'une bougie non clôturée. Voir le piège du look-ahead bias


🎓 Progression recommandée

🥉 Débutant : commence avec basic_emas, modifie les périodes des EMA, ajoute un indicateur simple (RSI), teste en dry-run.

🥈 Intermédiaire : customise avec des callbacks, implémente des paramètres optimisables.

🥇 Avancé : cherche une config rentable avec ces paramètres et callbacks, implémente des sorties custom, optimise avec hyperopt, puis re-teste en dry-run.

Quand ton dry-run est validé et que tu es prêt à encaisser le pire scénario de ta stratégie → tu peux passer en live.

💡 Mon approche perso : je valide en live avec un petit capital plutôt que de sur-croire à un backtest. Un backtest ne vaut rien, ce qui vaut quelque chose c'est le live — mais commence toujours en dry-run, puis avec une mise minuscule.


💡 Ressources complémentaires

🎯 Conclusion

Les fichiers de stratégie sont l'âme de tes bots Freqtrade. Tu as maintenant les clés pour comprendre leur structure, créer tes conditions, optimiser les performances et gérer le risque.

Prochaine étape : valide tes stratégies en dry-run, puis passe en live avec une petite mise.


☕ Si ce guide t'a aidé, tu peux me soutenir ici : buymeacoffee.com/freqtrade_france.

💡 Si tu trades sur Hyperliquid, tu peux passer par mon lien d'affiliation (réduction sur les frais).

⚠️ Je ne suis pas conseiller financier. Ce contenu reflète mon expérience personnelle. Les performances passées ne préjugent pas des performances futures. Il existe d'autres méthodes tout aussi valables.

— Mouton 🐑

¿Te gusta esta publicación?

Comprar Freqtrade France un café

Más de Freqtrade France