L'arrondi ressemble à un problème résolu. Vous avez un nombre, vous avez un pas, vous arrondissez à la valeur valide la plus proche. Une fonction utilitaire, vingt lignes, réglé pour toujours.
Ce n'est pas une seule fonction. Dans un système qui envoie des ordres à plusieurs exchanges, la précision est un domaine à part entière, et les bugs qu'elle produit partagent une propriété désagréable : ils ne lèvent pas d'exception. Ils produisent un ordre légèrement erroné, ou un ordre rejeté pour une raison que l'utilisateur ne peut pas voir.
Voici à quoi ressemble vraiment ce travail, à travers trois problèmes réels tirés de notre chaîne d'exécution des ordres.
Points clés
- L'IEEE-754 rend l'alignement sur le pas peu fiable : multiplier 0,29 par 100 donne 28.999999999999996, qui, une fois tronqué, tombe sur une valeur inférieure d'un pas entier.
- Trois exchanges appliquent trois règles d'ordre minimum incompatibles. L'un vérifie la valeur en USDT, un autre le nombre de contrats, un autre les deux.
- Notre pire bug dans ce domaine venait de l'invention d'une règle qu'un exchange n'a pas, puis du rejet d'ordres valides au nom de cette règle.
- Ne synthétisez jamais une limite que l'API ne vous a pas donnée. Si une plateforme n'a pas de minimum en valeur notionnelle, en calculer un à partir d'autres champs relève de l'invention, pas de la prudence.
- Une métadonnée d'exchange manquante devrait lever une erreur, pas retomber sur une valeur par défaut. Une mauvaise valeur par défaut produit silencieusement de mauvais ordres.
1. Le problème arithmétique : l'arrondi déplace votre nombre
Commençons par le cas le plus simple, parce que c'est celui que l'on croit sûr.
Un exchange accepte les quantités en multiples d'un pas. Vous avez 0.29 et un pas de 0.01. Cette valeur est déjà valide, donc la tronquer devrait ne rien changer. L'implémentation standard multiplie, tronque, puis redivise :
0.29 / 0.01 = 28.999999999999996 ← IEEE-754, pas 29
floor(...) = 28
28 × 0.01 = 0.28
Une valeur qui était déjà alignée a bougé d'un pas entier vers le bas, silencieusement, dans le sens d'un ordre plus petit. Aucune exception, aucun avertissement, et cela n'arrive que pour certaines combinaisons de valeur et de pas, ce qui est précisément ce qui rend le problème difficile à repérer.
La correction consiste en un epsilon appliqué pendant la multiplication, assez grand pour absorber le bruit de représentation mais assez petit pour qu'une valeur réellement inférieure comme 0.289999 soit encore tronquée correctement. J'ai raconté la découverte de ce problème dans ce que j'ai sous-estimé en construisant cette plateforme ; ce qui compte ici, c'est la catégorie à laquelle il appartient.
Parce qu'une fois que vous acceptez que l'arithmétique sur les quantités n'est pas fiable, vous commencez à examiner tout le reste qui touche à une quantité. C'est là que se trouvent les vraies surprises.
2. Trois exchanges, trois minimums incompatibles
Chaque plateforme applique un ordre minimum. On s'attendrait à ce que la règle diffère dans sa valeur, pas dans sa forme. Elle diffère dans sa forme :
| Exchange | Règle |
|---|---|
| Binance | qty × price >= minNotional (une vérification de valeur en USDT) |
| Bybit | qty >= minOrderQty et notional >= minNotional (deux vérifications) |
| Perpétuels OKX | qty >= minSz (nombre de contrats, aucune vérification de valeur notionnelle) |
Lisez ce tableau comme trois questions différentes posées à propos du même ordre. Binance demande combien il vaut. OKX demande combien il représente de contrats. Bybit demande les deux, et un ordre peut satisfaire l'une des conditions à lui seul tout en étant rejeté.
Il y a une subtilité supplémentaire sur OKX : les quantités sont exprimées en contrats, pas en coins, donc une taille de contrat doit convertir entre les deux avant que tout cela n'ait un sens. Une quantité qui signifie « 0,5 BTC » sur une plateforme signifie « 50 contrats » sur une autre.
La conséquence architecturale que nous avons retenue : chaque exchange valide ses propres ordres. Le moteur de stratégie ne contient pas une seule condition sur la plateforme à laquelle il s'adresse. Il transmet l'ordre à un adaptateur et l'adaptateur applique ses propres règles. Chaque fois que nous avons essayé d'unifier ces règles derrière une seule abstraction, l'abstraction devenait un mensonge dès qu'un quatrième cas apparaissait.
3. Le bug coûteux : inventer une règle qui n'existe pas
C'est le bug que je voudrais le plus qu'un autre ingénieur évite, parce que l'erreur ressemble à du sérieux.
Les perpétuels OKX n'ont pas de minimum en valeur notionnelle. Nous ne le savions pas. Raisonnant par analogie à partir des autres plateformes, nous en avons calculé un : nombre de contrats minimum, multiplié par la taille du contrat, multipliée par le prix. Cela semblait raisonnable. Cela produisait un chiffre en USDT plausible. Et c'était une fiction.
L'endroit où cela cassait est précis et mérite qu'on s'y attarde. Une stratégie DCA place ses ordres de sécurité en dessous du prix d'entrée. Notre minimum synthétique était calculé à un prix donné, puis comparé à des ordres au prix plus bas, si bien que la même quantité qui passait en haut de l'échelle échouait plus bas.
Le résultat : des ordres d'exactement 0.1 contrat, ce qui est le minimum de l'exchange et aurait été accepté, rejetés par notre propre validateur avant même de quitter notre système. L'exchange ne les a jamais vus. Du côté de l'utilisateur, la stratégie ne faisait simplement pas ce pour quoi elle était configurée.
La leçon se généralise bien au-delà du trading :
Ne synthétisez pas une contrainte que l'API ne vous a pas donnée. Si une plateforme expose un minimum en contrats, le minimum est en contrats. En dériver un équivalent basé sur la valeur, ce n'est pas être prudent, c'est inventer une règle puis l'imposer à l'utilisateur.
L'indice, avec le recul, était que notre valeur calculée dépendait du prix alors que la vraie règle de l'exchange non. Chaque fois que votre version d'une contrainte comporte une entrée que la vraie n'a pas, vous avez construit quelque chose de différent de ce que vous vouliez modéliser.
4. Une métadonnée manquante devrait être une erreur, pas une valeur par défaut
La dernière pièce est une politique plutôt qu'un bug, et c'est ce qui empêche les trois premiers de réapparaître discrètement.
Chaque exchange nous fournit des métadonnées par symbole : pas, échelon de prix (tick size), quantité minimum, taille du contrat. N'importe laquelle peut être absente, et la tentation est de prévoir un repli. Le pas est manquant ? On suppose 0.001. La quantité minimum est manquante ? On suppose zéro et on laisse l'exchange décider.
Nous faisons l'inverse : un champ requis qui est manquant lève une erreur, immédiatement, avant qu'aucun ordre ne soit construit. Rien en aval n'a la moindre chance de bâtir sur une supposition.
Le raisonnement tient à l'asymétrie des conséquences. Une erreur levée est un échec bruyant qui apparaît tout de suite, sous les yeux de quelqu'un qui peut le corriger. Une valeur par défaut est une mauvaise réponse silencieuse qui expédie un ordre à la mauvaise taille et se révèle plus tard, en argent. Entre une erreur que vous ne pouvez pas ignorer et un nombre que vous ne pouvez pas vérifier, l'erreur revient moins cher à chaque fois.
Cela signifie aussi que la couche de métadonnées est honnête sur ce qu'elle sait. Le code qui lit un champ requis obtient une vraie valeur ou une exception. Il n'obtient jamais un substitut déguisé en vraie donnée.
Ce qu'il faut retenir
- Traitez l'alignement sur le pas et l'échelon de prix comme de l'arithmétique qui peut déplacer votre valeur, pas comme du formatage.
- N'unifiez pas des règles qui sont réellement différentes. La validation par plateforme reste correcte à mesure que des plateformes s'ajoutent ; une abstraction partagée par-dessus des règles incompatibles, non.
- Ne dérivez jamais une limite que l'API n'a pas énoncée. Si votre version d'une contrainte prend une entrée que la vraie n'a pas, c'est une contrainte différente.
- Échouez bruyamment sur une métadonnée manquante. Les valeurs par défaut transforment une erreur visible en un ordre mal dimensionné invisible.
Le travail sur la précision n'a rien de glorieux et n'apparaît jamais sur une liste de fonctionnalités. Mais chacun de ces bugs a produit un ordre qui était erroné ou un ordre qui n'a jamais existé, et dans un système qui trade pour le compte de quelqu'un, ce sont là les mêmes catégories d'échec que de lui faire perdre son argent purement et simplement.
