Un backtest événementiel : un moteur en boucle avec stops ATR, dimensionnement des positions et liste de trades en R-multiples
Construisez en Python une boucle de backtest barre par barre qui dimensionne les positions selon le risque mesuré par l'ATR, respecte les stops, gaps compris, et enregistre chaque trade en R-multiples.
Ce que vous allez apprendre
- Pourquoi un backtest vectorisé ne peut pas modéliser les stops, les exécutions partielles ou le dimensionnement fondé sur le risque, et quand un moteur en boucle devient nécessaire.
- L'état qu'un moteur événementiel doit transporter d'une barre à l'autre : liquidités, position, prix d'entrée, niveau du stop et trade ouvert.
- Comment dimensionner chaque position à partir du risque du compte et de l'ATR, et comment enregistrer le résultat sous forme de R-multiple.
- Comment traiter honnêtement en code les gaps qui franchissent un stop.
Explication
Le backtest vectorisé de la leçon 7 calcule la position de chaque barre à partir de toute la série d'un coup. Il est rapide, mais il ne peut pas exprimer des règles qui dépendent du chemin parcouru : un stop placé 2 ATR sous l'entrée dépend de l'endroit où vous êtes entré, et la taille dépend de la valeur du compte à ce moment-là. Un backtest événementiel parcourt les barres une à une, n'utilise que les informations disponibles jusqu'à cette barre et met à jour son état exactement comme le ferait un trader.
L'état et l'ordre des événements
Le moteur conserve un petit ensemble de variables : cash, position (nombre d'actions, 0 en l'absence de position), entry, stop, risk_per_share et une liste trades. Sur chaque barre, l'ordre des vérifications compte ; il doit être écrit une fois pour toutes et respecté :
- Les sorties d'abord. Si une position est ouverte et que le plus bas du jour est inférieur ou égal au stop, sortez. Le prix d'exécution est le stop, sauf si l'ouverture est déjà sous le stop (un gap), auquel cas l'exécution se fait à l'ouverture. C'est là qu'une perte prévue de 1R peut devenir 1.4R ou plus.
- Mettez à jour les stops suiveurs à l'aide de la clôture et de l'ATR du jour.
- Les entrées en dernier, et uniquement à l'ouverture de la barre suivante, car un signal calculé à partir de la clôture du jour ne peut pas être exécuté à cette même clôture. En code, cela signifie : un signal sur la barre t active un indicateur, et l'exécution a lieu à
open[t+1].
Le dimensionnement des positions dans la boucle
Le dimensionnement fondé sur le risque a été enseigné dans Gestion du risque et des trades (leçon 2) et dans Trading systématique (leçon 9). En code, il tient en une ligne : size = floor(equity * risk_pct / (entry - stop)), où le dénominateur est le risque par action (ici 2 * ATR). Ajoutez un plafond tel que size * entry <= 0.25 * equity afin qu'un ATR minuscule ne puisse pas créer une position que le compte ne peut pas porter.
Enregistrer les trades en R
Chaque trade clôturé est ajouté sous forme de dictionnaire avec les dates, les prix, la taille, le profit en euros et le R-multiple = profit / (taille × risque par action). pd.DataFrame(trades) transforme la liste en une table de trades destinée aux fonctions de métriques de la leçon 9 et au calcul de l'espérance vu dans Gestion du risque et des trades (leçon 1).
Un moteur minimal
def run(df, risk_pct=0.01, atr_mult=2.0, start_cash=20_000):cash, pos, entry, stop, rps = start_cash, 0, None, None, Nonetrades, equity, pending = [], [], Falsefor i in range(1, len(df)):o, h, l, cl, atr = df.open[i], df.high[i], df.low[i], df.close[i], df.atr[i]if pos > 0 and l <= stop: # sortie : stop ou gapfill = min(o, stop)pnl = pos * (fill - entry)trades.append(dict(exit=df.index[i], pnl=pnl, R=pnl / (pos * rps)))cash += pos * fill; pos = 0if pending and pos == 0: # entrée à l'ouverture de cette barreentry, rps = o, atr_mult * df.atr[i-1]pos = int(cash * risk_pct // rps)stop = entry - rps; cash -= pos * entry; pending = Falseif pos == 0 and df.signal[i]:pending = True # trade à la prochaine ouvertureequity.append(cash + pos * cl)return pd.DataFrame(trades), pd.Series(equity, index=df.index[1:])
Deux détails : l'ATR utilisé pour le dimensionnement est df.atr[i-1], la valeur connue la veille au soir de l'entrée, et le stop est comparé au plus bas, et non à la clôture. Un stop suiveur ne demande qu'une ligne de plus, stop = max(stop, cl - rps), après le bloc de sortie. Les coûts peuvent être soustraits de pnl comme dans la leçon 7 ; ils sont omis ici par souci de clarté.
Exemple commenté
Exemple hypothétique : capital initial de €20,000, risque de 1 % par trade (€200), stop à 2 × ATR. Les prix et l'ATR sont des chiffres ronds inventés ; frais, spread et impôts sont exclus.
- Trade 1. Signal le lundi ; ouverture du mardi à €50.00, ATR précédent de €2.50. Risque par action = 2 × 2.50 = €5.00 ; stop = €45.00. Taille = floor(200 / 5) = 40 actions (€2,000 d'exposition, 10 % du capital, sous un plafond de 25 %). Sortie ultérieure sur signal à €58.00 : profit = 40 × 8 = €320 = 320 / 200 = +1.6R.
- Trade 2. Même configuration, entrée à €50.00, stop à €45.00, 40 actions. Nouvelle pendant la nuit : l'action ouvre à €43.00. Le moteur exécute à min(ouverture, stop) = €43.00. Perte = 40 × 7 = €280 = −1.4R, et non le −1.0R prévu. Un test vectorisé aurait affiché −1.0R, voire rien du tout.
- Trade 3. Entrée à €50.00, stop suiveur ; le stop est monté à €47.50 lorsque le plus bas l'atteint. Perte = 40 × 2.50 = €100 = −0.5R.
- Liste des trades. R-multiples de +1.6, −1.4, −0.5 : somme de −0.3R, moyenne de −0.1R. Trois trades ne prouvent rien, mais la table a la bonne forme pour en accueillir des centaines.
Un second instrument à €120 avec un ATR de €6 donne une taille de floor(200 / 12) = 16 actions : même risque de €200, moins d'actions parce que la volatilité est plus élevée.
Erreur fréquente
if l <= stop: fill = stop suppose silencieusement que le marché a coté à votre stop. En cas de gap, ce n'est pas le cas. Utilisez toujours min(open, stop) pour les sorties de positions longues (et max(open, stop) pour les positions courtes). La même logique s'applique aux entrées par ordre stop : un buy-stop à €52 est exécuté à l'ouverture si celle-ci est à €54.Exercice pratique
Sur papier, déroulez le moteur pour un trade : capital de €15,000, risque de 1.5 %, ATR de la veille de l'entrée de €1.80, multiplicateur de stop de 2.5, entrée à l'ouverture suivante à €36.00. (a) Calculez le risque par action, le niveau du stop et la taille de la position (arrondie vers le bas). (b) Le jour de la sortie, l'action ouvre à €37.00, descend jusqu'à un plus bas de €31.00 et clôture à €32.00. Quels sont le prix d'exécution, le profit en euros et le R-multiple ? (c) Le résultat serait-il différent si vous compariez le stop à la clôture plutôt qu'au plus bas ?
Afficher la solution
(a) Risque par trade = 0.015 × 15,000 = €225. Risque par action = 2.5 × 1.80 = €4.50. Stop = 36.00 − 4.50 = €31.50. Taille = floor(225 / 4.50) = 50 actions.
(b) L'ouverture à €37.00 est au-dessus du stop, il n'y a donc pas de gap ; le plus bas à €31.00 est inférieur ou égal à €31.50, l'exécution se fait donc à min(37.00, 31.50) = €31.50. Perte = 50 × (31.50 − 36.00) = −€225 = −1.0R.
(c) Une vérification sur la clôture (€32.00) déclencherait aussi la sortie ici, mais à €32.00 au lieu de €31.50 ; la perte serait alors de €200 (−0.89R) et paraîtrait meilleure que la réalité. Sur d'autres barres, le plus bas peut toucher le stop alors que la clôture repasse au-dessus ; une vérification sur la clôture manquerait alors complètement une sortie réelle.
Limites : l'ordre intraday du plus haut et du plus bas est inconnu avec des barres journalières ; lorsque le stop et un objectif se trouvent tous deux dans une même barre, supposez l'issue la plus défavorable.
Points clés à retenir
- Utilisez un moteur en boucle lorsque les règles dépendent du chemin parcouru : stops, stops suiveurs, dimensionnement fondé sur le risque et sorties partielles.
- Fixez l'ordre des événements par barre : sorties, mise à jour des stops, puis entrées à l'ouverture suivante.
- Dimensionnez à partir du capital × risque en % divisé par le risque par action, avec un plafond d'exposition ; enregistrez chaque trade avec son R-multiple.
- Exécutez les sorties sur gap à l'ouverture, et non au stop ; c'est de là que viennent les pertes supérieures à 1R.
Étape suivante
La leçon 9 transforme la courbe de capital et la liste de trades que vous produisez désormais en fonctions de performance réutilisables et testées : CAGR, drawdown, Sharpe, Sortino et profit factor.
Auto-évaluation
Répondez aux questions, puis vérifiez vos réponses. Vous pouvez recommencer autant de fois que vous le souhaitez.
Auto-évaluation automatique pour vos propres révisions. Vos réponses sont vérifiées dans votre navigateur ; nous ne les voyons pas et ne les évaluons pas. Ce n'est ni un examen ni une qualification.
Votre progression
« Terminée » signifie seulement que vous avez marqué la leçon comme lue. Ce n'est pas un test de votre compréhension. La progression et les notes sont enregistrées uniquement dans ce navigateur, sur cet appareil — pas dans un compte.