Un backtest vectorisé : signaux, positions, décalage pour éviter le biais d'anticipation, coûts de transaction et courbe de capital
Construisez dans pandas un backtest vectorisé complet d'une règle de moyenne mobile, avec le décalage des positions, les coûts par trade, la courbe de capital et une comparaison avec un indice de référence.
Ce que vous allez apprendre
- La structure en cinq colonnes d'un backtest vectorisé : signal, position, rendement de l'actif, coût et rendement de la stratégie.
- Pourquoi
shift(1)sur la colonne des positions est la ligne qui sépare à elle seule un test honnête d'un fantasme fondé sur l'anticipation. - Comment imputer des coûts de transaction proportionnels aux changements de position et construire une courbe de capital en euros.
- Ce qu'un test vectorisé peut modéliser et ce qu'il ne peut pas modéliser, et quand le moteur événementiel de la leçon 8 devient nécessaire.
Explication
Un backtest vectorisé calcule tout l'historique d'un coup par des opérations sur colonnes, au lieu de boucler barre par barre. Le backtest sur tableur du cours Trading systématique avait cette structure ; pandas l'exécute sur des décennies en quelques millisecondes. Il convient aux règles long/neutre ou toujours investies qui agissent sur la clôture ; les stops intrabarre et les prix d'exécution spécifiques relèvent de la leçon 8.
Étape 1, le signal. signal = (close > sma(close, 50)).astype(int) vaut 1 lorsque la clôture est au-dessus de sa moyenne sur 50 jours, sinon 0. Il décrit ce que vous savez à la clôture de la barre t.
Étape 2, la position. Vous ne pouvez pas trader sur une clôture que vous n'avez pas encore vue ; le plus tôt où vous pouvez détenir le signal de la barre t est la barre t + 1, donc position = signal.shift(1).fillna(0). La ligne t de la colonne des positions signifie désormais « l'exposition détenue pendant la barre t », décidée à la fin de la barre t − 1. Omettre le décalage permet à la stratégie d'encaisser le rendement de la barre t en utilisant la clôture de la barre t, ce qui constitue un biais d'anticipation (look-ahead bias) : l'utilisation d'informations indisponibles au moment de la décision.
Étape 3, le rendement de l'actif. ret = close.pct_change().fillna(0) (leçon 5). Comme la position pendant la barre t a été fixée à la clôture de la barre t − 1, position * ret est le rendement brut de la stratégie pour la barre t. L'hypothèse implicite est une exécution à la clôture de la barre t − 1 ; la leçon 8 traite des exécutions à l'ouverture suivante.
Étape 4, les coûts. Commission, spread et slippage sont modélisés comme un taux proportionnel par unité de position modifiée : turnover = position.diff().abs().fillna(0) vaut 1 lors du passage de neutre à long ou inversement, et 2 lors d'un retournement. cost = turnover * c, où c est le coût par sens exprimé en fraction (0.001 pour 0.1%). Le coût s'inscrit dans la colonne des rendements sur la barre où la position change.
Étape 5, rendement de la stratégie et capital. strat = position * ret - cost, et equity = capital * (1 + strat).cumprod(). L'indice de référence achat-conservation (buy-and-hold) est capital * (1 + ret).cumprod(). Comparez toujours les deux avec les mêmes métriques (leçon 9) ; se contenter de diluer l'achat-conservation avec des liquidités réduit autant le drawdown que le rendement, ce qui n'est pas une compétence.
Énoncez les hypothèses : l'exposition est une fraction du capital réinvestie chaque jour, exécutions à la clôture, coûts proportionnels, aucun intérêt sur les liquidités, dividendes uniquement via des données ajustées, impôts ignorés. Le test répond alors à une seule question, étroite : la règle de timing a-t-elle apporté quelque chose par rapport à la simple détention de l'actif, après coûts ?
Vérifications avant de se fier au résultat. (1) Mettez le coût à zéro et le signal à une constante égale à 1 ; le capital doit être exactement égal à celui de l'achat-conservation. (2) Comptez les allers-retours avec turnover.sum() / 2 et vérifiez leur plausibilité. (3) Assurez-vous que les lignes NaN de la période de chauffe de l'indicateur donnent une position nulle, et non un trade parasite.
Exemple commenté
Exemple hypothétique : huit clôtures en euros, 100, 101, 103, 102, 104, 103, 101, 100 ; position longue lorsque la clôture est strictement au-dessus de sa SMA sur 3 jours, sinon neutre ; coût par sens de 0.1% ; capital de €10,000 ; ni intérêts, ni dividendes, ni impôts.
| Barre | Clôture | SMA3 | Signal | Position | Rendement | Rotation | Stratégie | Capital € |
|---|---|---|---|---|---|---|---|---|
| 0 | 100 | – | 0 | 0 | 0 | 0 | 0 | 10,000.00 |
| 1 | 101 | – | 0 | 0 | +1.000% | 0 | 0 | 10,000.00 |
| 2 | 103 | 101.33 | 1 | 0 | +1.980% | 0 | 0 | 10,000.00 |
| 3 | 102 | 102.00 | 0 | 1 | −0.971% | 1 | −1.071% | 9,892.91 |
| 4 | 104 | 103.00 | 1 | 0 | +1.961% | 1 | −0.100% | 9,883.02 |
| 5 | 103 | 103.00 | 0 | 1 | −0.962% | 1 | −1.062% | 9,778.11 |
| 6 | 101 | 102.67 | 0 | 0 | −1.942% | 1 | −0.100% | 9,768.33 |
| 7 | 100 | 101.33 | 0 | 0 | −0.990% | 0 | 0 | 9,768.33 |
Barre 3 : le signal de la barre 2 valait 1 (103 > 101.33), la position pendant la barre 3 est donc de 1. L'actif a baissé de 103 → 102 (−0.971%), une rotation de 1 coûte 0.1%, le rendement de la stratégie est de −1.071% et le capital de 10,000 × (1 − 0.01071) = €9,892.91. À la barre 3, la clôture est égale à la SMA ; la règle stricte donne donc 0 et la position est clôturée pendant la barre 4 (coût de 0.1%, aucune exposition). Capital final de €9,768.33 (−2.32%) contre exactement €10,000 pour l'achat-conservation. Les coûts ont absorbé €39.36 sur quatre transactions dans un seul sens.
Voyons maintenant la version avec anticipation, qui utilise signal au lieu de la position décalée : la stratégie « détient » l'actif pendant les barres 2 et 4, les deux jours de hausse qu'elle ne pouvait identifier qu'après leur clôture, et termine à €10,356.65 (+3.57%). Un seul shift(1) manquant a transformé −2.32% en +3.57%.
Erreur fréquente
position = signal.shift(1) ; les rendements sont un simple pct_change().Exercice pratique
Sur papier, avec les mêmes huit clôtures et la même règle : (a) recalculez le capital final avec un coût par sens de 0.2% ; (b) indiquez le nombre d'allers-retours ; (c) un collègue propose de remplir les premières valeurs NaN de la SMA avec la clôture afin que « le trading puisse commencer dès le premier jour » ; expliquez brièvement pourquoi c'est un problème.
Afficher la solution
(a) Seules les quatre barres avec rotation changent. Barre 3 : −1.171% → €9,882.91 ; barre 4 : −0.2% → €9,863.15 ; barre 5 : −1.162% → €9,748.58 ; barre 6 : −0.2% → €9,729.09. Perte finale de 2.71% contre 2.32% à 0.1% ; doubler le taux de coût a ajouté €39.24 de frein.
(b) La rotation totalise 4 changements dans un seul sens, soit 2 allers-retours (entrée pendant la barre 3, sortie à la barre 4 ; entrée à la barre 5, sortie à la barre 6).
(c) Remplir la SMA avec la clôture crée des trades à partir d'un indicateur qui n'existe pas encore, si bien que leur logique diffère de la règle testée ; gardez une position nulle pendant la période de chauffe et notez combien de barres ont été exclues.
Limites : huit barres prouvent la mécanique, pas la règle ; des coûts proportionnels et des exécutions à la clôture sous-estiment le slippage sur les instruments peu liquides.
Points clés à retenir
- Un backtest vectorisé se résume à cinq colonnes : signal, position (décalée), rendement de l'actif, coût sur la rotation, rendement de la stratégie ; le capital est le produit cumulé.
position = signal.shift(1)n'est pas négociable ; testez en le supprimant et regardez le résultat gonfler.- Imputez les coûts sur
position.diff().abs()et comparez toujours avec l'achat-conservation sous des hypothèses identiques. - Les tests vectorisés conviennent aux règles de clôture à clôture ; les stops, le dimensionnement et des exécutions réalistes exigent le moteur événementiel de la leçon 8.
Étape suivante
La leçon 8 construit un backtest événementiel : une boucle barre par barre avec des stops fondés sur l'ATR, un dimensionnement des positions et une liste de trades mesurée en R-multiples.
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.