Un test de backtest utile n’a rien à voir avec la recherche d’un meilleur paramètre : arrêtez le processus à mi-parcours, restaurez-le et terminez le même replay. Avec des entrées identiques et un simulateur d’exécution contrôlé, ses décisions, ses ordres et sa valeur nette devraient correspondre à ceux d’une exécution ininterrompue.
Si ce n’est pas le cas, vous avez découvert un problème de gestion d’état. La stratégie dépend de quelque chose que vous n’avez pas sauvegardé ou que vous ne pouviez pas reconstruire. Cette dépendance compte chaque fois qu’une recherche est reprise, que des workers sont remplacés ou qu’un service de trading papier déploie du nouveau code.
J’aime ce test parce que la réponse attendue est particulièrement claire. Il n’y a pas de débat sur un éventuel changement de marché. Les deux exécutions reçoivent le même marché.
Voici trois mauvaises façons de redémarrer. Les chiffres sont donnés à titre d’illustration ; chacun de ces problèmes peut survenir dans un système par ailleurs déterministe.
1. Rechargez quelques barres et considérez les indicateurs comme initialisés
Supposons qu’une stratégie utilise une moyenne mobile exponentielle sur 100 périodes. Sa mise à jour est la suivante :
alpha = 2 / 101
ema_next = alpha * close + (1 - alpha) * ema_previous
Le processus ininterrompu conserve la valeur cumulée de l’EMA. Après son redémarrage, le processus récupère 100 barres, initialise l’EMA avec le premier cours de clôture et suppose qu’un indicateur sur 100 périodes nécessite 100 observations.
Cette hypothèse confond le paramètre de lissage de l’indicateur avec une fenêtre de mémoire finie. Une EMA conserve une contribution décroissante de son état initial. Si deux versions commencent avec un écart de 10 unités de prix entre leurs EMA, les prix suivants identiques réduisent cet écart comme suit :
| Mises à jour depuis l’initialisation | Écart restant | Part de l’erreur initiale |
|---|---|---|
| 100 | 1.353 | 13.53% |
| 250 | 0.0674 | 0.674% |
| 500 | 0.000454 | 0.00454% |
Le calcul est 10 * (99 / 101)^k. Récupérer 100 barres ne vous donne que 99 mises à jour si la première observation sert de valeur initiale.
Les dégâts apparaissent généralement près d’un seuil de décision. Une exécution voit le cours au-dessus de l’EMA ; l’autre le voit en dessous. Une petite différence numérique déclenche une transaction supplémentaire. Dès lors, les périodes de refroidissement, les liquidités disponibles et les décisions suivantes peuvent aussi diverger.
Sauvegardez l’état récursif de l’indicateur, son statut d’initialisation et le dernier événement traité. Vous pouvez aussi rejouer depuis un état initial connu. Un échauffement plus long peut donner une approximation acceptable, mais choisissez sa durée en fonction d’une tolérance d’erreur explicite et vérifiez si cette tolérance peut modifier les décisions. « Cinq fois la période » est une convention, pas une preuve.
Et les indicateurs ne sont pas les seuls à dépendre de l’historique. Un percentile glissant a besoin de sa fenêtre. Un modèle en ligne peut avoir besoin de l’état de son optimiseur. Une règle qui attend trois barres après une perte doit se souvenir de la perte et du compteur.
2. Sauvegardez les positions et oubliez les ordres en cours
Votre position cible est de 10 unités. Un ordre d’achat de 10 unités a été exécuté à hauteur de 4, il en reste donc 6 en attente. Vous enregistrez la position à 4, redémarrez et soumettez un nouvel ordre d’achat pour les 6 unités manquantes.
Si le reliquat de l’ordre initial et le nouvel ordre sont tous deux exécutés, vous détenez 16 unités.
En backtest, ce bug passe souvent inaperçu, car le redémarrage du moteur d’exécution efface silencieusement les ordres en cours. En trading papier, le simulateur ou le service externe peut les conserver. Le même code de reprise produit alors une exposition différente selon le composant qui a survécu.
| Au redémarrage | État réel | Ce que voit une reprise fondée uniquement sur les positions |
|---|---|---|
| Position cible | 10 | 10 |
| Position exécutée | 4 | 4 |
| Quantité d’achat en attente | 6 | 0 |
| Quantité supplémentaire nécessaire | 0 | 6 |
Les dégâts prennent la forme d’une salve inexpliquée d’ordres juste après la reprise. Parfois, l’exposition double. Parfois, une position est clôturée alors qu’un ordre de protection est toujours actif et peut ouvrir une nouvelle position plus tard.
Un checkpoint doit enregistrer l’identité et l’état du cycle de vie des ordres, en plus des positions. Avant de générer de nouvelles actions, la reprise doit rapprocher ces données de celles du système d’exécution. Un ordre dont l’issue est inconnue doit faire l’objet d’une investigation ; considérer que « aucun accusé de réception n’a été sauvegardé » signifie « l’ordre n’a jamais été soumis » est la recette des ordres en double.
Des identifiants d’ordre client stables vous aident à retrouver ce qui s’est passé. Ils empêchent les doublons uniquement si le système destinataire applique réellement les règles d’unicité ou d’idempotence requises. Conservez aussi les identifiants d’exécution déjà traités, afin qu’un fill rejoué n’augmente pas la position deux fois.
J’ai un faible pour le banal écran d’état des ordres. Le jour d’un redémarrage, ses petites lignes deviennent soudain l’interface la plus intéressante du bâtiment.
3. Restaurez la position et repartez avec un registre de P&L vierge
Prenons un exemple de spot sans levier et sans frais. Commencez avec $10,000 en espèces, achetez 10 unités à $100 et enregistrez un checkpoint lorsque le cours atteint $110.
L’état correct est de $9,000 en espèces plus une position valant $1,100 : soit une valeur nette de $10,100. Si la reprise restaure les 10 unités mais réinitialise les espèces à leur montant initial de $10,000, elle affiche $11,100. Vous avez créé $1,000 en redémarrant un processus.
D’autres cas sont moins spectaculaires. La reprise préserve la valeur nette, mais réinitialise le prix d’entrée à $110. La valeur nette totale peut rester correcte tandis que la répartition entre gains réalisés et latents change. Si un stop ou une condition de sortie dépend du prix d’entrée, ce raccourci comptable modifie alors le comportement de trading.
Ou bien le système oublie le précédent plus haut de la valeur nette. Supposons que celle-ci ait atteint $10,600 avant de retomber à $10,100. Son drawdown est d’environ 4.72%. Si le plus haut historique est réinitialisé lors de la reprise, la stratégie croit soudain que son drawdown est nul. Toute gestion du risque fondée sur le drawdown vient d’être réinitialisée sans autorisation.
Les dégâts peuvent donc se traduire par une discontinuité de la valeur nette, une amélioration suspecte du drawdown ou une règle de risque qui cesse de se déclencher après les déploiements. Préservez le registre et l’état de la stratégie qui dépend de la comptabilité : mouvements de trésorerie, positions, coût de revient applicable, frais cumulés et mémoire des contrôles du risque. Rapprochez la valeur nette restaurée avec celle du registre à la même date et heure de valorisation.
Un checkpoint doit correspondre à un état cohérent. Enregistrer les espèces après un fill et la quantité de la position avant ce fill produit un état qui n’a jamais existé. Enregistrez ensemble les états liés, ou consignez une séquence d’événements durable à partir de laquelle vous pourrez les reconstruire. Stockez le curseur des événements avec cet état pour que la reprise n’ignore pas le fill et ne l’applique pas deux fois.
Dans le banc de recherche, je garderais le test suivant : exécuter un replay de référence sans interruption, puis redémarrer une seconde exécution à des moments volontairement délicats, pendant l’initialisation d’un indicateur, après un fill partiel et pendant l’activation d’une limite de risque. Gardez le même ordre d’événements et préservez l’état aléatoire du simulateur. Comparez la première décision après la reprise, les enregistrements d’ordres et de fills, ainsi que l’évolution de la valeur nette. Un solde final identique à lui seul peut masquer des erreurs qui se compensent.
Pour un crash après la soumission mais avant l’accusé de réception, le banc de test doit aussi préserver l’état du service d’exécution indépendamment du processus de stratégie. Sinon, il efface précisément l’incertitude que vous cherchez à tester.
La spécification d’une stratégie inclut ce dont elle se souvient. Rendez cette mémoire assez explicite pour pouvoir arrêter le processus à mi-parcours d’un replay et montrer exactement comment il reprend son travail.
← Tous les articles


