Une année civile de barres à 1 minute devrait compter 525,600 lignes. Notre extraction 2025 des contrats perpétuels BTCUSDT en a renvoyé 525,557 — il en manquait 43 minutes, soit un taux de complétude de 99.992%. Un contrat perpétuel sur un altcoin à capitalisation moyenne, sur la même plateforme, avec la même extraction et le même code, en a renvoyé 1,247 de moins : 99.76%. Et une série de minutes sur actions américaines pour la même année comptait environ 427,000 lignes de moins que celle des cryptos ; ce n’est pas une lacune, simplement un marché qui ferme.
Trois de ces chiffres sont sans intérêt. Celui qui vaut mille mots, c’est le 43.
Quatre phénomènes différents qui ressemblent tous à un trou dans votre dataframe
Avant d’aborder les 43 minutes, posons la typologie, car la plupart des traitements de lacunes se trompent déjà à ce niveau. Quand une ligne manque dans votre fichier parquet local, vous ne savez pas lequel de ces phénomènes l’a produite, et la bonne réponse varie selon le cas.
| Type | Ce qui s’est réellement passé | Comment cela apparaît | La bonne réaction |
|---|---|---|---|
| Aucune transaction | Le marché était ouvert ; personne n’a franchi le spread pendant cette minute | Barre à volume nul (OHLC toutes égales, trades=0) sur certaines API, ligne absente sur d’autres | Conservez-la. C’est une information réelle : personne ne voulait trader. |
| Plateforme hors service | Moteur de correspondance des ordres hors ligne, avec ou sans maintenance programmée | Ligne absente, parfois plusieurs à la suite | Marquez les données comme périmées. Ne tradez pas pendant cette période. |
| Suspension de l’instrument | Dépassement d’une bande LULD, actualité en attente, avis de radiation | Ligne absente, puis une transaction à la réouverture aux enchères | Marquez les données comme périmées et traitez la réouverture comme une discontinuité. |
| Une erreur de votre côté | Pagination limitée par le débit, nouvelle tentative ayant omis une page, erreur de fuseau horaire à la limite d’une boucle | Ligne absente, impossible à distinguer des cas précédents | Détectez le problème et relancez l’extraction. C’est le seul cas que vous pouvez corriger. |
Les plateformes ne traitent pas de la même façon le premier cas du tableau, et c’est là que le piège se referme. Le point d’accès aux chandeliers de Coinbase omet entièrement les tranches vides : l’historique d’une paire peu liquide est donc plein de trous littéraux. Les klines de Binance renvoient généralement une barre synthétique avec un volume de 0 et un open=high=low=close calé sur la transaction précédente. Même réalité sous-jacente, deux formes différentes ; et un chargeur qui réindexe sur une grille complète à la minute transforme l’une en l’autre sans vous le signaler. Vérifiez le comportement de votre plateforme avant d’écrire le combleur de lacunes, pas après.
Les 1,247 minutes manquantes sur l’alt relevaient presque toutes de la catégorie 1 : carnet peu fourni à 04:00 UTC un dimanche, personne ne tradant. C’est agaçant, facile à expliquer et largement sans conséquence, puisque la stratégie n’aurait de toute façon pas tradé à ce moment-là. C’est précisément pour cela que j’ai cessé de m’y attarder et que je suis revenu aux 43.
Les 43 minutes n’étaient pas dispersées
Si les absences étaient uniformément réparties, 43 minutes sur une année donneraient 43 minutes isolées, une tous les huit jours et demi, chacune représentant une erreur d’arrondi. Ce n’est pas ce que nous avons observé. Elles formaient six séquences : une de 19 minutes consécutives, une de 11, deux de 4 et deux paires. Six événements, pas 43 accidents.
Et ces événements dépendent de ce qui vous intéresse. Les plateformes tombent en panne quand elles sont surchargées, et elles le sont quand les prix bougent. J’ai réparti toutes les heures de l’année selon la volatilité réalisée, puis regardé où se trouvaient les minutes manquantes : 61% d’entre elles étaient dans le décile supérieur. La probabilité inconditionnelle qu’une minute donnée manque est de 0.008%. Si elle se trouve dans une heure du décile supérieur de volatilité, elle est d’environ 0.05% — six fois plus, avec en plus des absences regroupées.
Votre indicateur de complétude dans le tableau de bord qualité mesure donc la mauvaise chose. 99.992%, cela semble désigner un jeu de données auquel on peut cesser de penser. En réalité, cela décrit une série complète pendant les heures où votre stratégie ne fait rien, mais lacunaire pendant celles où elle fait tout. Un système de momentum qui se déclenche lors d’une accélération de la volatilité a bien plus de chances de rencontrer une lacune que ne le laisse entendre le chiffre global, et il la rencontre en pleine transaction.
Ce que le forward-fill provoque trois lignes plus loin
Voici le problème qui m’a poussé à écrire cet article. Prenons la séquence de 19 minutes. Nettoyage standard : réindexer sur la grille complète à la minute, propager les valeurs OHLC de la dernière clôture, mettre le volume à zéro. La série est maintenant continue et vos indicateurs s’exécutent sans le moindre NaN.
Ces 19 barres ont high == low == close. La true range est nulle pour chacune. Calculé sur cette période, un ATR(14), qui était d’environ 240 USDT avant la panne, descend vers 34 au retour de la plateforme — les cinq vraies barres restantes dans la période constituent toute la moyenne. Injectez ensuite cette valeur dans un dimensionnement de position ajusté à la volatilité, de type ordinaire size = risk_budget / ATR. La taille de position est multipliée par 7.
La barre réelle suivante est la transaction à la réouverture, et ce n’est pas une barre calme. Dans notre cas, elle a ouvert à 1.8% de la dernière clôture avant la panne. Le backtest a tranquillement pris une position 7x juste avant un écart de 1.8%, avec une exécution qui n’aurait pas pu exister, à un prix que personne ne cotait. Cette unique transaction synthétique valait plus d’un mois de P&L légitime sur la courbe de capital, dans le mauvais sens — et elle provenait entièrement d’une ligne de nettoyage des données ajoutée pour rendre le dataframe bien ordonné.
Supprimer les lignes au lieu de les remplir ne règle pas le problème : c’est le même bug sous un autre déguisement. Faites-les disparaître et vos fenêtres de calcul à index entier vous induisent en erreur : une « EMA sur 20 barres » couvre maintenant 39 minutes d’horloge pendant la panne, le rendement entre deux barres à la frontière est le saut complet de 1.8% traité comme un mouvement d’une minute, et toute estimation de volatilité par barre le considère comme un événement à 60-sigma. Rien ne vous alerte. L’index reste monotone.
Le rééchantillonnage rend le problème invisible
La plupart des recherches ne s’effectuent pas sur des barres à 1 minute, mais sur des données agrégées, et l’agrégation maquille le problème. Un rééchantillonnage sur 5 minutes transforme un trou de 19 minutes en quatre barres, dont la première et la dernière sont partielles. Pandas calcule alors un OHLC tout à fait plausible à partir de deux minutes survivantes et lui attribue le même libellé qu’à une barre issue de cinq minutes. Rien dans le résultat ne permet de les distinguer.
Le correctif le plus simple que je connaisse : conserver une colonne bars_in_window à chaque étape du rééchantillonnage et ne jamais la supprimer. Un entier par ligne suffit, et permet de répondre à toutes les questions en aval sur la fiabilité d’une barre. Nous conservons aussi seconds_since_last_real_print, qui contient la même information dans un format exploitable par la couche d’exécution.
Notre politique, pour ce qu’elle vaut
Voici ce que nos agents appliquent désormais, dans l’ordre :
- Ne réindexez jamais sans le signaler. Le chargeur produit un manifeste des lacunes — début, fin, durée et catégorie présumée parmi les quatre. Si une séquence de minutes manquantes dure moins de 3 barres et que le volume des barres environnantes est faible, il s’agit d’une minute sans transaction. Toute séquence plus longue pendant les heures actives est traitée comme une panne jusqu’à preuve du contraire.
- Relancez l’extraction avant d’interpréter les données. La moitié de nos premières lacunes venaient d’erreurs de pagination. Une seconde extraction depuis un autre point d’accès ou un autre fournisseur résout la catégorie 4 et réduit le problème avant même qu’il faille porter un jugement.
- Mettez en place un seuil de fraîcheur plutôt que de combler les lacunes. La stratégie reçoit une entrée
data_ageet une règle stricte : aucune nouvelle position si la dernière transaction réelle date de plus de N barres, et les positions ouvertes ne sont clôturées à la réouverture que par un ordre au marché, avec une décote explicite pour le risque d’écart. Des exécutions impossibles valent moins que des transactions qui n’ont pas eu lieu. - Les indicateurs voient NaN, pas des données inventées. Les prix propagés ne parviennent jamais à la couche des caractéristiques. Si l’ATR ne peut pas être calculé, il reste indéfini ; et indéfini signifie sans position. Un échec explicite vaut mieux qu’un multiplicateur silencieux de 7x.
- Présentez les performances conditionnelles aux lacunes. Chaque backtest que nous livrons montre le P&L avec les transactions proches d’une panne exclues, en parallèle du résultat global. Si ces transactions portent le résultat, celui-ci est un artefact des données.
Un audit rapide à lancer dès aujourd’hui : regroupez vos minutes manquantes en séquences consécutives, puis vérifiez quelle part des transactions de votre backtest s’ouvre ou se clôture dans les 30 minutes autour d’une frontière de séquence. En dessous de 1%, les lacunes ne déterminent probablement rien. À 5% ou plus, la courbe de capital raconte en partie l’histoire des pannes de la plateforme.
Le meilleur indice, d’après mon expérience, tient à la forme des absences plutôt qu’à leur nombre. Un jeu de données avec des milliers de trous dispersés pendant les heures creuses ne pose généralement pas de problème. Un jeu de données avec quelques grappes très concentrées vous indique que quelque chose casse sous forte charge ; et c’est précisément là que votre stratégie intervient.
← Tous les articles