Aller au contenu principal
ingénieriebacktestingalgorithmes
Pourquoi les backtests mentent : simuler des sorties basées sur des indicateurs est plus difficile qu'il n'y paraît

Pourquoi les backtests mentent : simuler des sorties basées sur des indicateurs est plus difficile qu'il n'y paraît

Un backtest produit un chiffre, et les chiffres sont convaincants. C'est là tout le problème. Quand une simulation est subtilement fausse, elle ne plante pas et elle ne vous prévient pas. Elle vous tend un résultat légèrement meilleur que la réalité et vous laisse agir dessus.

Simuler un take-profit fixe est facile : soit le prix a atteint votre niveau, soit il ne l'a pas atteint. Simuler une entrée ou une sortie basée sur un indicateur est là où ça se complique, car la simulation doit maintenant répondre à une question bien plus délicate : que savait réellement le bot à cet instant ?

Voici trois façons dont nous avons mal répondu à cette question, et ce que chacune nous a appris.

Points clés

  • Le bug classique consiste à lire un indicateur en unité de temps supérieure avant la clôture de sa bougie. Un RSI en 4 heures consulté à l'ouverture de la bougie contient quatre heures de mouvement de prix qui ne s'étaient pas encore produites.
  • Le correctif est arithmétique, pas heuristique : une valeur issue de l'unité de temps T n'est légitimement utilisable qu'une fois T écoulée, il faut donc décaler la lecture en arrière de (unité de temps du déclencheur moins unité de temps de la simulation).
  • Les moteurs en temps réel ont le même problème sous un autre déguisement. Un indicateur calculé sur une bougie encore en formation repeint, ce qui veut dire qu'une condition peut être vraie puis fausse sur la même bougie.
  • Un simulateur qui ignore en silence ce qu'il ne peut pas rejouer est plus permissif que le bot en temps réel, et l'écart favorise toujours le backtest.
  • L'attitude honnête consiste à faire apparaître cet écart dans le résultat plutôt qu'à le cacher. Un backtest qui vous dit où il ment vaut plus qu'un backtest qui a l'air propre.

1. Le bug de look-ahead qui se cache dans les stratégies multi-unités de temps

C'est celui qui est le plus facile à écrire et le plus difficile à remarquer.

Supposons que la simulation avance une bougie de 5 minutes à la fois, et que la stratégie a une condition sur un RSI en 4 heures. À chaque pas de 5 minutes, le moteur a besoin d'une valeur pour ce RSI. L'implémentation naïve va chercher la bougie de 4 heures contenant l'instant présent et lit sa valeur.

C'est une machine à voyager dans le temps.

Une bougie de 4 heures qui s'est ouverte à 12:00 n'a pas de valeur RSI définitive avant sa clôture à 16:00. Si la simulation se trouve à 12:05 et lit cette bougie, elle lit un chiffre calculé à partir de quatre heures de mouvement de prix qui, du point de vue de 12:05, ne se sont pas encore produites. La stratégie semble entrer juste avant des mouvements qu'elle ne pouvait pas voir venir, et la courbe de capital paraît excellente.

Le correctif n'est pas une heuristique. Il découle du fait d'écrire noir sur blanc à quel moment une valeur devient réellement connaissable. Une bougie qui s'ouvre à t sur une unité de temps de triggerMs n'est connaissable qu'à t + triggerMs. La simulation prend sa décision à la clôture de la bougie courante, candleOpenTime + simulationMs. La valeur est donc légitimement utilisable exactement quand :

t + triggerMs <= candleOpenTime + simulationMs
t             <= candleOpenTime - (triggerMs - simulationMs)

Ce qui signifie que la lecture correcte est celle que vous aviez déjà, exécutée contre un horodatage décalé en arrière de triggerMs - simulationMs.

Deux propriétés de cette expression méritent d'être soulignées, car ce sont elles qui la rendent sûre à appliquer partout :

  • Les conditions dans la même unité de temps reçoivent un décalage nul. Un indicateur en 5 minutes lu à la clôture d'une bougie de 5 minutes est sa propre valeur basée sur la clôture, qui a toujours été correcte. Seules les unités de temps réellement supérieures se décalent.
  • Le décalage est dérivé, pas configuré. Il provient de la bougie elle-même plutôt que d'un réglage que quelqu'un doit penser à définir.

La leçon générale, je l'appliquerais à n'importe quelle simulation, pas seulement au trading : quand vous n'êtes pas sûr d'avoir un biais de look-ahead, écrivez l'horodatage auquel chaque donnée d'entrée devient connaissable et comparez-le à l'horodatage auquel vous l'utilisez. Le biais, c'est ce qui reste quand vous sautez cette étape et vous fiez à l'intuition à la place, et l'intuition est mauvaise à cet exercice parce que lire « la bougie de 4 heures actuelle » semble tout à fait raisonnable.

2. Le même bug dans le code en temps réel porte un autre déguisement

On pourrait croire que ce problème appartient au backtesting. Ce n'est pas le cas. Le moteur en temps réel y est confronté lui aussi, et là on l'appelle le repainting.

À tout instant, la bougie la plus récente est encore en train de se former. Un indicateur calculé sur une fenêtre se terminant sur cette bougie va changer à mesure que la bougie change. Une condition peut être vraie à 12:01, fausse à 12:03, puis de nouveau vraie à 12:04, le tout à l'intérieur de la même bougie, sans que le marché fasse quoi que ce soit d'inhabituel. Si le bot agit sur la première d'entre elles, il a agi sur une valeur qui n'existe plus.

Par défaut, nous écartons la bougie encore en formation et nous calculons uniquement sur les bougies confirmées, de sorte qu'un signal ne repeint jamais. C'est aussi ce qui fait concorder le temps réel et le backtest, puisque la simulation travaille par construction avec des bougies clôturées.

Mais « ne jamais repeindre » n'est pas gratuit. Attendre la confirmation signifie agir plus tard, et pour certaines stratégies ce retard coûte plus cher que le bruit qu'il évite. C'est donc exposé comme un choix par condition : évaluer sur la bougie confirmée, ou évaluer en temps réel et accepter que la valeur puisse bouger sous vos pieds.

Le principe de conception, c'est la partie que je garderais : quand un compromis est réellement un compromis, ne l'enterrez pas dans un réglage par défaut en le faisant passer pour un fait. Faites de l'option sûre le choix par défaut, puis laissez l'utilisateur opter pour l'autre en connaissance de cause. Ce qu'il ne faut pas faire, c'est laisser les deux modes diverger entre le temps réel et le backtest, car l'utilisateur compare alors deux bots différents.

3. Ce que le simulateur ne peut pas rejouer est plus dangereux que ce qu'il calcule mal

Les deux premiers sont des bugs d'exactitude. Celui-ci est un problème de conception, et c'est celui que je crois le plus sous-estimé.

Certaines conditions ne peuvent pas être rejouées. Un signal de webhook externe en est le cas le plus clair : il n'existe aucune trace historique de ce qu'un système extérieur aurait envoyé à 14:20 tel jour du mois de mars dernier. La simulation ne peut tout simplement pas le savoir.

Que devrait-elle faire alors ? La réponse tentante est d'ignorer cette condition et d'évaluer le reste. C'est ce qui donne au résultat une apparence complète. C'est aussi là que se produisent les dégâts, et le mécanisme mérite d'être détaillé :

Si une stratégie exige A ET B, et que B ne peut pas être simulé, ignorer B laisse A décider seul. Le bot simulé entre sur des conditions où le bot en temps réel serait resté à l'écart. La simulation est systématiquement plus permissive que la réalité.

Remarquez la direction. L'erreur n'est pas aléatoire. Écarter une condition sous une logique ET ne peut jamais produire que plus de trades que la réalité, jamais moins, de sorte que le biais pointe vers un résultat plus flatteur à chaque fois. C'est cette asymétrie qui le rend dangereux : le bruit aléatoire se moyenne sur de nombreuses exécutions, mais un biais unidirectionnel non.

Nous avons choisi de continuer à faire tourner la simulation plutôt que de refuser, car un résultat partiel avec un écart connu reste utile. Mais alors, cet écart doit être visible dans le résultat, pas enterré dans un journal. Le résultat porte un avertissement explicite, dans la langue de l'utilisateur, avec un nom qui dit ce qui s'est passé : « Certaines conditions d'entrée non simulées ». Il en existe un équivalent pour les sorties.

Cette décision est la partie de ce système que je défendrais le plus ardemment. Un simulateur qui ne peut pas échouer bruyamment échouera silencieusement, et l'échec silencieux dans quelque chose qui produit des chiffres est le pire mode de défaillance qui soit, parce que les gens agissent sur des chiffres et n'agissent pas sur des traces de pile.

Ce qu'il faut en retenir

Si vous construisez quoi que ce soit qui rejoue l'historique pour produire un chiffre :

  • Chaque donnée d'entrée a besoin d'un horodatage « connaissable à ». Si vous ne pouvez pas l'énoncer, vous ne pouvez pas exclure le biais de look-ahead.
  • Vérifiez la direction de vos erreurs, pas seulement leur ampleur. Une erreur qui pointe toujours dans le même sens se compose ; une erreur qui pointe au hasard se moyenne. L'optimisme unidirectionnel est la signature du biais de look-ahead.
  • Faites partager au simulateur et au système en temps réel la même logique de décision partout où vous le pouvez. Deux implémentations de « cette condition est-elle vraie » finiront par diverger, et vous l'apprendrez d'un utilisateur plutôt que d'un test.
  • Rapportez ce que vous n'avez pas pu faire. La crédibilité d'un résultat vient de ses limites annoncées, pas de sa propreté.

Le résumé inconfortable, c'est qu'un backtest est une affirmation sur un contrefactuel, et les contrefactuels sont faciles à fausser en silence. L'objectif d'ingénierie n'est pas un simulateur qui ne ment jamais. C'est un simulateur qui vous dit où il ment.


À lire également

Créer un compte

Mettez vos connaissances en pratique et ouvrez un compte Freya dès aujourd'hui.

S'inscrire maintenant
Ce contenu est uniquement à des fins éducatives et ne constitue pas un conseil financier. Le trading de cryptomonnaies comporte des risques significatifs. N'investissez jamais plus que ce que vous pouvez vous permettre de perdre.