L'argument d'un bot de trading sans code est simple : vous décrivez une stratégie dans un formulaire, et le logiciel l'exécute pour vous. La partie difficile n'est pas le formulaire. C'est que chaque simplification que vous offrez à l'utilisateur doit être payée quelque part dans le système, généralement à un endroit que vous n'aviez pas prévu.
Voici trois choses que j'ai mal comprises à ce sujet, et ce que chacune nous a réellement appris. Elles sont classées selon l'ampleur de mon erreur d'appréciation.
Points clés
- Les API des exchanges échouent en silence plus souvent qu'elles n'échouent bruyamment. Un HTTP 200 avec moins de données que celles demandées est le mode de défaillance contre lequel vous devez vous prémunir.
- Ne faites jamais confiance à un résultat de backtest tant que vous n'avez pas testé le simulateur lui-même. Une courbe de capital d'apparence propre peut provenir d'une exécution défaillante.
- L'arithmétique à virgule flottante sur les quantités d'ordres produit des erreurs d'un pas qui n'apparaissent que sur certaines paires.
- Décider de ne jamais détenir les fonds des utilisateurs élimine toute une catégorie de problèmes et en crée un plus petit mais plus aigu : votre système doit être correct sans jamais pouvoir annuler quoi que ce soit.
- Chaque commodité que vous offrez à l'utilisateur devient une contrainte quelque part dans le moteur. Ce compromis vaut généralement la peine, mais il n'est jamais gratuit.
1. J'ai supposé qu'une requête échouée ressemblerait à un échec
C'est celle qui a coûté le plus de temps, et c'est celle contre laquelle je mettrais en garde tout développeur en premier.
Nous récupérons les bougies historiques des exchanges pour exécuter des backtests. Le modèle mental naïf est qu'une requête réussit ou échoue : soit vous obtenez vos données, soit vous recevez un code différent de 200 et vous le gérez. Les vraies API des exchanges ne se comportent pas ainsi sous charge. Elles renvoient un HTTP 200 avec moins de bougies que celles demandées, sans aucune indication qu'un problème est survenu.
Voici un cas réel tiré de nos logs, et les chiffres sont exacts. Un backtest sur longue période a demandé environ 536,000 bougies. À 16:34, il en a reçu 58,000. Statut 200. Aucun champ d'erreur. Aucun avertissement. La même requête, inchangée, à 17:54 a reçu les 535,000 complètes.
Réfléchissez à ce que cela signifie si vous ne vérifiez pas. Le moteur simule tranquillement une stratégie sur onze pour cent de l'historique prévu, produit une courbe de capital d'apparence plausible, et rapporte un résultat. L'utilisateur lit un chiffre erroné d'une manière que rien dans le résultat ne révèle. C'est pire qu'un plantage. Un plantage est honnête.
La correction est conceptuellement ennuyeuse et il était facile de la négliger : après la récupération, vérifiez que les données reçues couvrent réellement la période demandée, et traitez un déficit comme un échec même si le transport indiquait que tout allait bien. La couverture est une propriété que vous devez affirmer, pas une que vous pouvez présumer.
Une leçon de second ordre se cachait derrière la première. Une fois que nous avons commencé à détecter la troncature, le réflexe évident était de faire échouer l'exécution. Mais les chiffres ci-dessus montrent que ces troncatures sont transitoires : la requête identique une heure plus tard renvoyait des données complètes. Échouer purement et simplement aurait consommé le quota de l'utilisateur pour ce qui n'est qu'un incident temporaire du côté de l'exchange. Le comportement correct consiste donc à réessayer une fois avant d'échouer, ce qui a nécessité de retravailler le chemin d'échec afin qu'une condition réessayable ne soit pas marquée définitivement comme un échec au passage.
Deux règles en sont ressorties, et je les emporterais toutes les deux dans tout projet qui communique avec une API tierce :
- Validez la forme d'une réponse réussie, pas seulement son code de statut. Le code de statut vous indique que la requête s'est terminée. Il ne vous dit pas que la réponse correspond à ce que vous avez demandé.
- Distinguez les échecs transitoires des échecs permanents avant de décider quoi faire. Ils semblent identiques au moment où ils surviennent et méritent des réponses opposées.
2. J'ai supposé qu'un résultat de backtest était un résultat
Lié, mais une erreur distincte, et qui va plus loin que le problème de troncature.
Quand vous construisez un simulateur, vous consacrez vos efforts de test aux stratégies. La logique DCA se comporte-t-elle correctement ? Les sorties se déclenchent-elles là où elles le devraient ? C'est la question intéressante, donc c'est là que l'attention se porte naturellement.
Le problème, c'est qu'un simulateur défaillant produit une sortie qui ressemble exactement à celle d'un simulateur qui fonctionne. Une exécution qui a silencieusement traité une fraction des données renvoie quand même un chiffre. Une exécution dont les conditions de sortie n'ont jamais été évaluées renvoie quand même un chiffre, et un chiffre étrangement plat. Nous avons vu des résultats revenir à exactement +0.00% avec zéro trade, ce qui est le signal le plus fort possible que quelque chose cloche dans la mécanique, et le plus facile à interpréter à tort comme « cette stratégie ne fait rien ».
Le changement qui a réglé cela a consisté à modifier ce que nous testons. Testez le simulateur, pas la stratégie. Concrètement, cela signifie faire des assertions sur des choses que la stratégie ne devrait pas pouvoir changer : que le nombre de bougies traitées correspond au nombre récupéré, qu'une exécution sur une période connue couvre réellement cette période, qu'une configuration garantie de se déclencher produit des trades. Quand ces invariants tiennent, un résultat d'apparence étrange est une véritable découverte sur la stratégie. Quand ils ne tiennent pas, le résultat n'est que du bruit déguisé.
La forme générale de cela, pour tout ce qui simule : un simulateur qui ne peut pas échouer bruyamment échouera en silence, et l'échec silencieux dans un système qui produit des chiffres est le pire mode de défaillance qui soit. Les gens agissent sur des chiffres. Ils n'agissent pas sur des traces de pile.
3. J'ai sous-estimé à quel point le dimensionnement des ordres est un travail de précision
Celle-ci n'est pas intellectuellement difficile. Elle est simplement bien plus vaste qu'elle n'y paraît, et elle se cache à un endroit où les erreurs coûtent cher.
Les exchanges n'acceptent pas des quantités arbitraires. Chaque paire de trading a un pas (step size) pour la quantité et un incrément (tick size) pour le prix, et les ordres qui ne s'alignent pas sont rejetés. Chaque quantité que le système calcule doit donc être arrondie vers le bas à un pas valide avant d'être envoyée. Cela ressemble à une simple fonction utilitaire.
Puis la virgule flottante entre en scène. Prenez l'arrondi vers le bas de 0.29 à un pas de 0.01. Multipliez pour le mettre à l'échelle et IEEE-754 vous donne 28.999999999999996, qui s'arrondit vers le bas à 28, ce qui redonne 0.28 une fois remis à l'échelle. La valeur était déjà alignée sur le pas. L'arithmétique l'a déplacée d'un pas entier vers le bas, et elle l'a fait en silence, dans le sens d'un ordre plus petit. Pour corriger cela, il faut un epsilon pendant l'étape de mise à l'échelle, assez petit pour qu'une valeur réellement inférieure comme 0.289999 s'arrondisse toujours correctement vers le bas, assez grand pour absorber le bruit de représentation.
C'est une correction d'une seule ligne qui a demandé un vrai débogage pour être trouvée, car le symptôme n'est pas une exception. C'est un ordre légèrement plus petit que prévu, sur certaines paires et pas d'autres, selon la position du pas par rapport à la représentation binaire.
Le point plus large, et la raison pour laquelle je classe cela parmi ce que j'ai sous-estimé : la précision dans un système de trading n'est pas une fonction utilitaire, c'est une catégorie de travail à part entière. Les pas et les incréments diffèrent selon la paire, selon l'exchange, et entre le spot et les futures. Les valeurs d'ordre minimales ne sont pas un chiffre unique que vous pouvez consulter et coder en dur. Elles proviennent de l'exchange pour chaque symbole, et sur certaines places elles dépendent du prix actuel. Chaque fois que vous vous surprenez à écrire une constante unique pour quelque chose que l'exchange définit paire par paire, vous écrivez un bug à retardement.
Nous exposons cela aux utilisateurs plutôt que de le cacher, parce que l'alternative est un ordre rejeté pour des raisons qu'ils ne peuvent pas voir.
4. La contrainte que je ne m'attendais pas à apprécier
La dernière n'est pas une erreur. C'est une décision dont j'ai sous-estimé les conséquences, dans le bon sens.
Freya ne détient jamais les fonds de trading des utilisateurs. Les bots tradent via des clés API d'exchange autorisées à lire et à trader, et jamais à retirer. L'argent reste dans le compte d'exchange de l'utilisateur d'un bout à l'autre.
Je m'attendais à ce que ce soit un argument marketing. Il s'est avéré être un argument architectural, et il a rendu le système plus petit. Il n'y a pas de registre de conservation, pas de solde interne à réconcilier avec un solde externe, pas de file d'attente de retrait, pas de politique de portefeuille froid pour le capital de trading. Toute une catégorie parmi les problèmes les plus difficiles du logiciel financier est tout simplement absente, parce que nous n'avons jamais pris en charge ce qui les crée.
Ce qui la remplace est plus étroit et plus aigu : votre système doit être correct en temps réel, parce qu'il ne peut rien annuler. Avec la conservation, vous pouvez, en principe, inverser un transfert interne. Passer un ordre sur le compte d'exchange de quelqu'un d'autre est définitif à l'instant où il est exécuté. Cela reporte tout le poids de l'ingénierie sur la correction avant l'envoi de la requête plutôt que sur la réconciliation après, un compromis que je referais.
Ce que je me dirais au début
- Partez du principe que toute API tierce réussit parfois de façon incorrecte, et concevez sa détection avant d'en avoir besoin.
- Écrivez des tests d'invariants pour votre simulateur avant d'écrire des assertions sur les stratégies. Les tests de stratégie ne valent rien tant que ceux du simulateur ne passent pas.
- Traitez la précision comme un domaine, pas comme un utilitaire.
- Choisissez vos contraintes tôt. Celles que vous acceptez d'emblée coûtent bien moins cher que celles que vous découvrez plus tard.
Aucun de ces problèmes n'était exotique. C'étaient des problèmes d'ingénierie ordinaires que j'avais classés comme « petits » lors de la planification, ce qui est précisément la raison pour laquelle ils sont devenus coûteux.
