Six semaines après avoir lancé un backtest, nous l’avons relancé avec le même code, la même configuration et la même période. Le capital final était différent. Pas de façon spectaculaire, mais suffisamment pour faire passer le ratio de Sharpe de 1.31 à 1.08 et changer le jeu de paramètres qui semblait le meilleur.
La stratégie n’avait pas changé. Les données historiques, si. Un fournisseur avait révisé plusieurs jours de transactions et de bougies, et notre première exécution n’avait pas conservé l’instantané exact des données d’entrée. Nous avons passé les jours suivants à déterminer si le nouveau résultat venait d’une correction des données, d’un bug du pipeline ou d’un processus de recherche impossible à reproduire.
Lundi : la relance donnait un résultat différent
Le premier indice était un petit écart sur une paire spot liquide. Notre bougie d’origine affichait un plus haut de 62,418. La relance indiquait 62,431. Cette différence semblait sans conséquence jusqu’à ce que nous remontions à l’entrée de la stratégie : son seuil se situait entre ces deux prix. Quelques bougies modifiées suffisaient à changer les signaux, et donc les positions plusieurs jours plus tard.
Nous avons comparé les volumes quotidiens de transactions et constaté que l’écart commençait à une date précise. Nous avons ensuite comparé les lignes brutes utilisées pour cette date. La réponse actuelle du fournisseur contenait quelques transactions corrigées et une bougie agrégée révisée. Aucun journal des modifications n’accompagnait la réponse que nous avions utilisée.
C’est le comportement normal des données. Les plateformes d’échange réparent leurs enregistrements ; les fournisseurs comblent les lacunes, corrigent les transactions erronées et ajustent leur logique d’agrégation. Une requête portant sur « les données de janvier » peut renvoyer un résultat différent en mars.
Mardi : nous avons trouvé le reçu manquant
Nous avions consigné le nom du fournisseur, le symbole et la période dans l’enregistrement de l’exécution. Nous n’avions pas conservé la réponse elle-même ni l’empreinte de contenu de chaque partition d’entrée. Nous pouvions donc prouver ce que nous avions demandé, mais pas quels octets le fournisseur avait renvoyés.
Nous avons récupéré les fichiers mis en cache les plus anciens que nous pouvions trouver et les avons comparés à la réponse actuelle. L’écart ne venait ni d’un ajustement lié à un split ni d’une conversion de fuseau horaire. Il venait d’un petit ensemble de transactions corrigées qui alimentaient notre générateur de bougies. Cela a éliminé une hypothèse, mais le cache était incomplet : nous ne pouvions toujours pas reconstituer exactement chaque bougie concernée.
| Ce que nous avions consigné | Ce que cela nous permettait de savoir | Ce qui manquait |
|---|---|---|
| Fournisseur, symbole, période | Quelle requête l’exécution avait envoyée | Le contenu de la réponse |
| Version du pipeline | Quel code avait généré les bougies | Les lignes d’entrée exactes |
| Horodatage de l’exécution | Quand le traitement avait été lancé | Quelle révision du fournisseur était alors disponible |
Nous avions assimilé « même requête » à « mêmes données ». Cette hypothèse donnait à l’expérience une apparence de reproductibilité supérieure à la réalité.
Mercredi : la correction a changé la donne
Nous avons rejoué les dates concernées avec les deux versions des données disponibles. Les bougies révisées ont supprimé deux entrées et décalé une sortie d’une bougie. Ces trois transactions expliquaient l’essentiel de la variation du Sharpe. La stratégie était sensible à quelques observations proches de son seuil de déclenchement, même si la série de prix globale semblait presque identique sur un graphique.
Cela ne signifiait pas que le résultat d’origine était frauduleux, ni que le résultat révisé était automatiquement meilleur. Cela signifiait qu’il fallait préciser la portée de l’affirmation : l’ancienne exécution décrivait l’instantané de données dont nous disposions à l’époque, tandis que la relance décrivait l’historique actuel du fournisseur. Nous ne pouvions pas honnêtement présenter l’une comme la reproduction de l’autre.
Jeudi : nous avons modifié le dossier de recherche
Nous avons ajouté des instantanés d’entrée immuables aux nouvelles exécutions de recherche. Chaque partition reçoit une empreinte de contenu, et le manifeste de l’exécution consigne la source, l’heure de récupération, la version du schéma, la politique d’ajustement et la version du code de transformation. Pour les grands jeux de données, nous conservons les fichiers dans un stockage adressé par contenu, afin de partager les partitions identiques sans écraser les versions précédentes.
Lorsque les règles de stockage ou de licence nous empêchent de conserver les données brutes, nous gardons la meilleure preuve autorisée par les conditions : les empreintes, les paramètres de requête, les identifiants de révision de la source et un petit échantillon de lignes analysées pour le diagnostic. Une empreinte ne permet pas de récupérer les données, mais elle nous indique si deux exécutions n’ont pas utilisé des octets identiques.
Un backtest reproductible exige une identité des données d’entrée, pas seulement une URL de données. Consignez ce qui est arrivé, quand c’est arrivé et comment ces données sont devenues les lignes consommées par votre stratégie.
Vendredi : ce que nous éviterions la prochaine fois
Nous éviterions de comparer uniquement les indicateurs principaux. Une variation du Sharpe vous indique qu’un élément a changé ; elle n’en localise pas la cause. Commencez par les nombres de lignes et les empreintes des partitions, puis repérez la première date d’entrée différente. Comparez ensuite, dans cet ordre, les lignes transformées, les signaux, les ordres et les exécutions.
Nous éviterions aussi de remplacer silencieusement les anciennes données par un historique corrigé. Les corrections sont utiles, mais les anciennes exécutions doivent rester liées à leurs données d’entrée d’origine. Un nouveau jeu de données doit donner lieu à une nouvelle exécution, avec un lien visible vers l’ancienne.
Notre règle pratique est désormais simple : si une relance doit reproduire une expérience, nous verrouillons l’instantané des données. Si elle doit tirer parti d’un historique corrigé, nous la désignons comme une nouvelle expérience. Il nous a fallu quelques jours pour comprendre cette distinction ; elle tient en une ligne du manifeste d’exécution.
← Tous les articles


