Voici une bougie. Contrat perpétuel Binance USDⓈ-M, BTCUSDT, pour la minute qui commence à 13:46:00 UTC. Elle provient de l’endpoint klines sous la forme d’un simple tableau de 12 valeurs. C’est l’unité d’entrée la plus courante dans la recherche quantitative amateur et assimilée. Presque toutes les stratégies que nous générons touchent à quelque chose qui ressemble à ça.
Alors décortiquons-la, index par index, et voyons tout ce qu’un backtest interprète de travers.
| Index | Valeur | Nom |
|---|---|---|
| 0 | 1773495960000 | heure d’ouverture (ms) |
| 1 | "84120.50" | ouverture |
| 2 | "84177.90" | plus haut |
| 3 | "84098.10" | plus bas |
| 4 | "84163.40" | clôture |
| 5 | "38.417" | volume de base (BTC) |
| 6 | 1773496019999 | heure de clôture (ms) |
| 7 | "3232108.94" | volume en contre-valeur (USDT) |
| 8 | 1204 | nombre de transactions |
| 9 | "21.883" | volume de base acheté au taker |
| 10 | "1841203.55" | volume en contre-valeur acheté au taker |
| 11 | "0" | ignoré |
[0] et [6] : de quelle minute s’agit-il, et quelle horloge fait foi ?
Heure d’ouverture : 1773495960000 ; heure de clôture : 1773496019999. Regardez bien la deuxième : elle se termine par 19999, soit 1 milliseconde avant l’ouverture de la bougie suivante. L’intervalle est semi-ouvert, et l’exchange vous l’indique explicitement. La moitié des bogues de données que j’ai traqués venait de quelqu’un qui traitait les deux bornes comme inclusives et comptait deux fois une transaction à la limite, ou qui alignait un rééchantillonnage de sorte que chaque bougie de 5 minutes emprunte 1 milliseconde à sa voisine.
Les horodatages correspondent à l’heure du moteur d’appariement de l’exchange. Pas à celle de votre horloge, ni à celle d’ingestion de votre fournisseur, ni à la même horloge que celle de l’instantané de funding. Quand vous joignez une série de klines à une série de taux de funding ou d’open interest extraite d’un autre endpoint, vous combinez des sources qui s’accordent à environ 1 seconde près, la plupart du temps. Pour une stratégie à 1 minute, cela représente une erreur de synchronisation de 1,7 % sur chaque ligne jointe. Pour toute stratégie sous la minute, c’est fatal.
Et l’horodatage de cette bougie correspond à son ouverture. Les informations qu’elle contient ne sont connues qu’à 13:46:59.999. Toutes les questions de convention d’indexation — décaler d’une unité ou non, utiliser l’horodatage de clôture ou d’ouverture comme heure de l’événement — reviennent à la même question de fuite d’information, sous un autre déguisement.
[1] « 84120.50 » : le cours d’ouverture auquel vous ne pouvez pas traiter
L’ouverture est le prix de la première transaction exécutée dans l’intervalle. C’est une transaction conclue entre deux autres personnes. Quand la bougie apparaît sous forme de ligne dans votre dataframe, ce prix date déjà de 60 secondes.
La bougie précédente a clôturé à 84109.80 : il y a donc 10.70 $ de mouvement de prix dans l’intervalle entre deux bougies adjacentes d’1 minute. Cela représente 1,3 point de base, invisible sur un graphique, et environ un quart des frais taker. Rien de grave. Mais regardez la même série sur un perp d’altcoin pendant une publication de l’IPC américain : ces écarts entre bougies atteignent 15 à 40 points de base. Un backtest qui génère un signal à la clôture de la bougie et exécute à l’ouverture de la suivante suppose discrètement que l’écart est nul — et il l’est précisément quand ça n’a aucune importance.
[2] et [3] : plus haut à « 84177.90 », plus bas à « 84098.10 »
C’est là que la plupart des moteurs d’exécution rendent l’âme, alors cette section est la plus longue.
Le plus haut et le plus bas sont des prix touchés. Ils n’indiquent ni quantité ni durée. D’après le flux brut des transactions de cette même minute, tout ce qui s’est échangé à 84100.00 ou moins représentait 0,62 BTC, sur 9 transactions en 1,4 seconde. Un backtest considère donc qu’un stop à 84100 est « exécuté » ; et si votre position est de 3 BTC, vous avez absorbé tout le carnet visible dans la baisse, puis le reste de votre quantité s’est exécuté quelque part pendant le retour de 84105 à 84130. La bougie indique un plus bas à 84098.10. Elle ne dit pas que seulement 52 000 $ de valeur notionnelle y ont été échangés.
Dans l’autre sens, c’est pire, parce que ça vous avantage. Supposons que vous ayez un ordre limite de vente au repos à 84175. Le plus haut de la bougie est à 84177.90 : un moteur naïf vous exécute donc à 84175 et comptabilise un rabais maker plutôt que des frais taker. La réalité de cette exécution dépend de votre position dans la file d’attente à ce niveau de prix, information que la bougie ne peut pas fournir et que vous n’avez probablement jamais enregistrée. Un prix touché ne signifie pas que l’ordre a été exécuté.
La règle que nous avons retenue dans notre moteur d’exécution : un ordre limite au repos n’est exécuté que si la bougie franchit le niveau, pas si elle l’atteint simplement. Pour les exécutions au tick extrême exact, il faut une preuve de volume à ce prix dans le flux des transactions ; sinon, l’exécution est rejetée. Cette règle a éliminé environ 6 % des transactions d’un portefeuille de retour à la moyenne typique et fait passer le Sharpe backtesté d’un candidat de 1,9 à 1,1. Ce candidat n’a jamais été réel ; la règle d’exécution a simplement été la première assez honnête pour le dire.
Autre piège : le parcours intrabougie. Si l’amplitude d’une bougie englobe à la fois votre stop et votre take-profit, l’OHLCV ne permet pas de savoir lequel a été atteint en premier. Chaque moteur doit choisir une convention. Le nôtre suppose toujours que le stop est atteint en premier : c’est pessimiste, parfois faux, mais cela ne fabrique jamais de gain. Si votre moteur suppose que le take-profit est atteint en premier, les bougies de grande amplitude produiront des profits imaginaires à partir de cette ambiguïté. Et ce sont précisément ces bougies qui pèsent le plus dans la distribution de vos PnL.
[4] « 84163.40 » : la valeur la moins robuste de la bougie
La clôture est la dernière transaction de l’intervalle. C’est tout. Il peut s’agir d’un lot atypique de 0.002 BTC, vendu par un bot qui solde une position à 13:46:59.8. C’est ce tick unique, arbitraire par construction, que la plupart des pipelines de recherche utilisent pour calculer chaque signal, évaluer chaque position et mesurer chaque sortie.
Sur les perps BTC, cela compte à peine ; sur un perp d’altcoin peu liquide à 04:00 UTC, cela compte énormément, et l’écart entre les cours de clôture de 2 plateformes pour la même minute peut dépasser votre avantage total par transaction. Quand le PnL d’une stratégie dépend spécifiquement de la clôture, nous la réexécutons en évaluant les positions au mark price de l’exchange, calculé à partir de l’indice et beaucoup plus difficile à manipuler. Si les résultats divergent, la stratégie exploitait cet artefact.
[5] et [7] : « 38.417 » et « 3232108.94 », dans quelle unité le volume est-il exprimé ?
Le volume de base est en BTC ; le volume en contre-valeur est en USDT. Les deux sont fournis ici, une attention que toutes les plateformes n’ont pas. C’est important pour agréger les données de plusieurs plateformes. Les contrats à marge en coin sont cotés en contrats de 100 $ de valeur notionnelle. Certains flux d’actions indiquent des lots standards. Les plateformes de marchés prédictifs indiquent le nombre de parts, chaque part étant une créance binaire libellée en dollars. Additionner le « volume » de tout un univers hétérogène sans le normaliser dans une unité notionnelle unique donne un classement de liquidité complètement absurde, et cet absurdité aura l’air suffisamment cohérente pour passer les vérifications.
Normalisez tout en valeur notionnelle de contre-valeur dès l’ingestion. Conservez aussi le champ brut, mais ne le laissez jamais parvenir jusqu’à une stratégie.
[8] 1204 : le champ qui devrait définir votre modèle d’impact
Diviser le volume de base par le nombre de transactions donne une taille moyenne de 0.032 BTC, soit environ 2 700 $. Si votre stratégie candidate veut entrer une position de 250 000 $ d’un coup, elle demande une quantité environ 92 fois supérieure à celle d’une transaction typique de cette minute. C’est ce chiffre, et non une constante générique de slippage de 5 points de base, qui devrait alimenter votre terme d’impact en racine carrée. Nous calculons le taux de participation par bougie dans une colonne à part entière et rejetons les stratégies dont l’entrée médiane dépasse quelques pourcents de la valeur notionnelle de la bougie, car tout ce qui suit relève de la fiction.
Le nombre de transactions permet aussi de repérer facilement les minutes atypiques. Volume normal, nombre de transactions tombé à 11 ? Quelqu’un a exécuté un bloc. Volume normal, nombre de transactions à 9 000 ? Une cascade de liquidations est absorbée en petites transactions.
[9] et [10] : « 21.883 », le champ que tout le monde écarte
Le volume de base acheté au taker. Sur les 38.417 BTC de cette bougie, 21.883 ont été initiés par des acheteurs. Le delta de volume signé est donc de +5.349 BTC, et la répartition des agresseurs est de 57/43 en faveur des acheteurs. L’exchange vous fournit gratuitement un déséquilibre des flux d’ordres dans un champ que la plupart des gens ne lisent jamais, parce que pandas ne lui a pas donné de nom.
Je ne prétends pas que ce champ prédit les rendements à lui seul ; les stratégies naïves fondées sur le delta comptent parmi les moyens les plus fiables de renflouer le compte des frais. Mais il mesure quelque chose de réellement différent du prix, est disponible dans la requête que vous envoyiez déjà et permet de distinguer un rallye porté par les achats d’un rallye dû au retrait des vendeurs. Ces deux cas se ressemblent en OHLC et se comportent différemment 10 minutes plus tard. Notre agent de recherche considère qu’une hypothèse qui ignore la répartition taker sur une plateforme qui la publie passe à côté d’éléments disponibles.
[11] « 0 » : champ ignoré — et tout ce qui ne figure pas ici
L’index 11 est un champ obsolète, toujours à zéro. Plus intéressante est la liste des données absentes de cette bougie : ni bid, ni ask, ni spread, ni profondeur de carnet, ni taux de funding, ni open interest, ni liquidations, ni mark price, ni prix d’indice. Et surtout, rien ne permet de savoir si votre ordre aurait été le vôtre en maker ou en taker — la différence entre payer 0,045 % et gagner 0,01 % sur cette plateforme.
Tout modèle de frais construit à partir des seules klines est donc une hypothèse déguisée en chiffre. Nous réglons le problème en obligeant chaque stratégie à déclarer à l’avance son style d’exécution, puis en appliquant les frais taker à tout ce qui ne peut pas prouver le contraire.
La bougie qui n’est jamais apparue
Dernier point, et celui qui fait le plus mal en dehors des actifs majeurs. Une minute sans aucune transaction ne produit aucune kline. Les fournisseurs et les bibliothèques recopient souvent la valeur précédente : ouverture = plus haut = plus bas = clôture = clôture précédente, volume 0. Votre indicateur calcule tranquillement. Votre stratégie voit une ligne valide et peut générer un signal pour une minute où personne au monde n’a échangé cet instrument.
Sur un perp de capitalisation moyenne que nous avons ingéré, 4,1 % des bougies d’1 minute sur une période de 12 mois n’avaient enregistré aucune transaction. Une stratégie de retour à la moyenne candidate sur ce symbole plaçait 38 % de ses entrées sur des bougies synthétiques, car les prix synthétiques plats sont irrésistibles pour tout ce qui mesure l’écart à une moyenne mobile. Le backtest était magnifique. La stratégie exploitait les lacunes des données.
C’est pourquoi la couche d’ingestion ajoute désormais un indicateur syntheticbooléen à chaque bougie, propagé à chaque rééchantillonnage, et la batterie de vérifications échoue si les transactions d’une stratégie se concentrent sur ces bougies. Une colonne peu coûteuse, qui a éliminé plus de stratégies candidates que n’importe quel indicateur que nous ayons jamais écrit.
12 valeurs. 4 régulièrement mal interprétées, 2 régulièrement écartées, et toute une catégorie absente de la ligne, mais imaginée par le moteur. Avant votre prochain backtest, récupérez une bougie brute de votre propre stockage et lisez chaque champ à voix haute en vérifiant ce que votre logique d’exécution suppose à son sujet. Cela prend 20 minutes, et je n’ai jamais vu quelqu’un le faire sans rien découvrir.
← Tous les articles


