Oluwa.io
Tous les écrits
·8 min de lectureingénieriecryptofiabilitétrading

Exécution fiable des ordres crypto : la mécanique qui inspire confiance

La fiabilité de l'exécution des ordres crypto naît des machines à états, des clés d'idempotence et d'une réconciliation obstinée. Retours d'expérience sur MadaiOps.

Par Vitor Lima

La plupart des produits de trading crypto vendent de la vitesse et un graphique. Le plus difficile n'est pas de dessiner les chandeliers. C'est d'être capable de répondre, à n'importe quelle milliseconde, à une question trompeusement simple : quel est le véritable état de mon ordre en ce moment précis ? Quand nous avons commencé à construire MadaiOps, notre application interne de gestion des ordres et des opérations de trading, cette question a tout structuré. La vitesse est une fonctionnalité. La vérité est le produit. La fiabilité de l'exécution des ordres crypto, c'est ce que vous obtenez quand l'application peut répondre honnêtement à cette question même pendant que la plateforme se dégrade. Et une application qui affiche une exécution qui n'a pas eu lieu — ou qui en cache une qui a bel et bien eu lieu — détruit la confiance d'une façon qu'aucun gain de latence ne pourra jamais racheter.

Cet article parle de l'ingénierie qui rend une application d'ordres digne de confiance : la machine à états, l'idempotence, la réconciliation via WebSockets, et les modes de défaillance qui n'apparaissent que sous charge. Nous avons un avis tranché sur tout cela, parce que ce sont justement les choix ennuyeux qui vous gardent solvable.

Modéliser l'ordre comme une machine à états explicite

La meilleure décision que nous ayons prise, et de loin, a été d'arrêter de traiter un ordre comme une ligne que l'on UPDATE pour commencer à le traiter comme une machine à états finis, avec des transitions nommées et énumérées. Un ordre vit dans des états comme PENDING_NEW, WORKING, PARTIALLY_FILLED, FILLED, CANCEL_REQUESTED, CANCELED, REJECTED et EXPIRED. Chaque transition est une fonction de l'état courant combiné à un événement entrant, et les transitions illégales sont rejetées bruyamment plutôt qu'absorbées en silence.

Le bénéfice : l'ambiguïté devient un problème de validation plutôt qu'un incident à 2 heures du matin. Un ordre FILLED ne peut jamais revenir à WORKING. Un événement d'exécution qui arrive pour un ordre CANCELED n'est pas « impossible » — c'est une vraie course critique, et la machine à états vous force à décider de ce que cela signifie. En général, cela veut dire que l'annulation a perdu la course, que l'ordre a effectivement été exécuté, et que votre CANCELED local était faux. Modélisez les ordres comme des lignes mutables et cet événement se contente d'écraser quelque chose ; vous ne remarquez rien jusqu'à ce que la réconciliation — ou un utilisateur — le rattrape.

Ce qu'il faut retenir

Énumérez vos états avant d'écrire le moindre appel d'API. Si vous ne savez pas dessiner le diagramme sur un tableau blanc, c'est que vous ne comprenez pas encore votre domaine, et l'exchange vous l'apprendra à la dure. Stockez les transitions, pas seulement l'état courant. Un journal d'événements en ajout seul de ce qui s'est passé vaut mieux qu'une unique colonne de statut mutable, parce que le journal est rejouable et que la colonne, elle, n'est qu'une supposition.

L'idempotence n'est pas optionnelle

Les réseaux réessaient. Votre propre logique de retry réessaie. Un utilisateur tape deux fois sur « Acheter ». N'importe laquelle de ces situations peut envoyer le même ordre deux fois, et sur un exchange, un ordre en double, c'est de l'argent réel. La défense, c'est une clé d'idempotence générée côté client — la plupart des exchanges l'appellent clientOrderId ou newClientOrderId — attachée à chaque ordre au moment même où l'utilisateur valide, avant que la requête ne quitte le device ou le serveur.

La règle que nous imposons : la clé est générée une seule fois, persistée localement avant l'appel réseau, et réutilisée mot pour mot à chaque nouvelle tentative. Si la première tentative a expiré sans réponse, le retry porte la même clé, et l'exchange soit crée l'ordre une seule fois, soit renvoie celui qui existe déjà. Dans les deux cas, vous convergez vers un ordre unique. Générez la clé après un échec et vous avez construit une machine à ordres en double.

Cela s'étend aux annulations et aux modifications. Une demande d'annulation devrait elle aussi être idempotente : annuler un ordre déjà annulé est une opération sans effet qui renvoie un succès, pas une erreur qui déclenche une nouvelle boucle de retry. Nous traitons partout « l'état final souhaité est déjà atteint » comme un succès. Cela paraît trivial. Cela élimine toute une catégorie de tempêtes de retry.

Ce qu'il faut retenir

Persistez la clé d'idempotence avant d'agir, pas après avoir réussi. L'intervalle entre « je l'ai envoyé » et « je sais que ça a marché » est exactement l'endroit où l'argent disparaît, et la clé est la seule chose qui le referme.

Réconciliez sans relâche : le WebSocket est un indice, pas la vérité

Les exchanges poussent les mises à jour d'ordres via WebSockets, et c'est merveilleux quand ça fonctionne : exécutions en moins d'une seconde, quantités d'exécution partielle en direct, confirmations d'annulation. Mais un WebSocket, c'est un flux que vous pouvez manquer. Vous serez déconnecté. Les messages arrivent dans le désordre, sont perdus pendant une reconnexion, ou n'arrivent tout simplement jamais parce que la plateforme est dégradée. Si l'état de votre ordre est uniquement ce que le socket vous a dit en dernier, votre application se trompe avec assurance dès que le socket a un hoquet.

Alors nous faisons tourner deux boucles. La boucle rapide consomme le WebSocket et applique les événements de façon optimiste. La boucle lente interroge l'API REST — GET /order et les endpoints des ordres ouverts — à intervalle régulier et après chaque reconnexion, puis réconcilie ce snapshot avec l'état local. Le REST est la source de vérité ; le socket est un indice à faible latence. Quand les deux ne sont pas d'accord, le REST l'emporte et nous journalisons la divergence, car une divergence, c'est un rapport de bug que le système a écrit pour vous.

Le séquencement compte. La plupart des exchanges estampillent les mises à jour avec un numéro de séquence monotone ou un updateTime. Nous écartons tout événement plus ancien que le dernier appliqué pour cet ordre, ce qui rend inoffensive une livraison dans le désordre. À la reconnexion, la bonne manœuvre est : se réabonner, tirer immédiatement un snapshot REST complet pour combler le trou, puis refaire confiance au flux en direct. Sautez le snapshot et vous manquez l'exécution qui a eu lieu pendant les deux secondes où vous étiez hors ligne.

Ce qu'il faut retenir

Ne laissez jamais un flux temps réel être le seul auteur de votre état. Le socket vous rend rapide ; la boucle de réconciliation vous rend correct. Vous avez besoin des deux, et quand ils entrent en conflit, correct l'emporte sur rapide à tous les coups.

Exécutions partielles et déconnexions : les deux qui vous mettent à nu

Les exécutions partielles, c'est là où les applications naïves mentent en silence. Un ordre de 1,0 BTC qui exécute 0,3, puis 0,4, puis se fige n'est ni « ouvert » ni « terminé ». Il est PARTIALLY_FILLED avec 0,7 exécuté et 0,3 restant, et chacun de ces chiffres doit être suivi à partir des événements d'exécution individuels, pas déduit d'un unique champ de statut. Nous accumulons la quantité exécutée et le prix moyen pondéré par le volume à partir des exécutions discrètes, de sorte que la position soit toujours reconstructible à partir de primitives. Un utilisateur qui voit « exécuté » a besoin de savoir exécuté à quelle hauteur, à quel prix moyen. Tout ce qui est en deçà n'est qu'une demi-vérité.

Les déconnexions aggravent tout cela. Tombez hors ligne en plein milieu d'une exécution et le WebSocket ne vous aura jamais parlé du 0,4 ; votre état local est donc périmé. C'est précisément pour cela qu'existe la discipline « reconnexion puis snapshot » : le snapshot REST renvoie la quantité cumulée exécutée, et la boucle de réconciliation comble le trou sans que l'utilisateur ne voie jamais un chiffre faux. Le mode de défaillance contre lequel nous concevons n'est pas « l'application a planté » — les plantages sont honnêtes. C'est « l'application a continué de tourner et a affiché quelque chose de faux ».

Ce qu'il faut retenir

Suivez la quantité cumulée exécutée comme le fait primordial et dérivez tout le reste à partir d'elle. Ne stockez jamais le « restant » comme un champ indépendant que vous mutez. Dérivez-le (commandé − exécuté) pour qu'il ne puisse pas dériver hors de la réalité.

Honnête sous pression vaut mieux que malin

Il y a une tentation persistante d'être malin : prédire les exécutions avant confirmation, masquer les erreurs « transitoires », lisser une reconnexion pour que l'interface ne scintille jamais. Nous avons appris à y résister, pour l'essentiel. Un opérateur de trading ne veut pas d'un mensonge tout lisse ; il veut savoir, honnêtement, que la connexion vient de tomber et que l'état est en cours de réconciliation. Un état visible « reconnexion, vérification des ordres » vaut plus qu'une interface sans accroc qui, au bout du compte, affiche la vérité d'hier.

Concrètement, nous exposons l'incertitude au lieu de la camoufler. Quand l'état local et l'exchange ne se sont pas réconciliés récemment, l'application le dit. Quand un ordre est CANCEL_REQUESTED mais pas encore confirmé annulé, elle affiche exactement cela, pas un CANCELED prématuré. L'interface optimiste convient très bien à une application de listes de tâches. Pour de l'argent, l'optimisme est une dette que vous payez en tickets de support et en confiance perdue.

Ce qu'il faut retenir

Dans le doute, montrez le doute. La chose la plus rassurante qu'une application financière puisse faire sous stress, c'est de refuser de deviner.

Réflexion finale

Rien de tout cela n'est glamour. Il n'existe aucune démo où « nos clés d'idempotence sont persistées avant l'appel réseau » déclenche des applaudissements. Mais la fiabilité n'est pas une fonctionnalité que l'on ajoute plus tard. C'est l'ensemble des décisions que vous prenez tôt et que vous refusez de compromettre, puis que vous défendez chaque fois qu'un raccourci se met à paraître tentant. Construisez le système qui dit la vérité quand tout est en feu, et les parties rapides et jolies s'occuperont d'elles-mêmes.