La fonction s'appelait enter_pack. Ça aurait dû nous servir d'avertissement. Un agent de recherche avait écrit un cash-and-carry BTC : quand le perpétuel cotait plus de 8 points de base au-dessus du spot, acheter le spot, shorter le perp, ne plus bouger. Le funding et le basis feraient le reste. Le backtest exécutait les deux jambes dans la même milliseconde, au mid, plus un tick de politesse. La courbe d'equity était un escalier. Personne n'aurait dit ça palpitant. C'était précisément l'argument de vente.
Nous l'avons promu en paper un lundi.
Lundi : le pack qui s'exécutait toujours
Le premier signal est arrivé à 11:17 UTC. Le basis à l'écran était de 9.4 bps, perp cher. Dans le backtest, enter_pack renvoyait 2 fills et un résiduel de 0. En paper, il a renvoyé un fill spot de 0.40 BTC à l'ask, et un short au repos sur le perp qui n'a pas été exécuté.
Le spot avait payé 4 bp de frais taker et environ 1 bp de spread. Le prix perp que nous voulions était encore au carnet, puis plus. 20 secondes plus tard, le basis coté était de 5 bps, parce que nous venions de lifter le spot. 40 secondes plus tard, le perp s'est exécuté, après le mouvement. Nous étions packés. Nous étions packés à un basis qui ne payait plus l'entrée et la sortie.
Le backtest aurait comptabilisé « +9.4 bps de carry, résiduel 0. » Le blotter avait 4 bps de coût taker spot, un long non couvert de 40 secondes, et un gap d'entrée d'environ 5. J'avais un échantillon de 1 et, malgré tout, trop confiance dans l'escalier.
Mardi : nous avons ouvert la fonction
enter_pack prenait une liste de jambes et un mid cible. Il bouclait, exécutait chaque jambe à mid * (1 ± slip), et écrivait une ligne de portefeuille. Il n'y avait pas de second timestamp. Il n'y avait pas de branche dans laquelle la jambe 2 n'existait pas. Le test unitaire assertait residual == 0.
C'est le genre de test qui vous donne l'impression d'être un adulte. Il n'échouera jamais.
Nous avons cassé le pack en deux ordres avec chacun son état : soumis, partiel, exécuté, annulé. Incomplet est devenu un objet de première classe. Le replay historique n'avait toujours pas de vrai carnet, alors on a triché. Retarder le second fill d'un tirage dans l'attente du lundi (moyenne 26 secondes, une queue que je n'aimais pas), et dériver son prix de ce que le spot avait fait dans cette fenêtre. Rudimentaire, mais l'escalier a gagné des encoches avant le déjeuner : des packs entrés à 3 bps, des packs qui n'étaient jamais devenus des packs.
Mercredi : 3 secondes, puis on jette
Si la seconde jambe n'est pas en place dans les 3 secondes, dénouer la première. Nous aurions dû commencer par là. Ça sonne conservateur jusqu'à ce qu'on chiffre l'abort. Dénouer, c'est qu'on a croisé le spot pour entrer et qu'on le recroise pour sortir. 2 frais taker, 2 spreads, pas de carry, pas de funding. Dans le replay réécrit, ces aborts n'étaient pas rares. Ils se regroupaient sur les barres où le basis avait l'air le plus « réel », en général les barres où quelque chose traverse déjà le spot.
Mercredi soir, nous avions 17 signaux paper avec lesquels nous disputer.
| Backtest, lundi matin | Paper, mercredi | |
|---|---|---|
| Entrée | Les deux jambes au mid, un seul timestamp | Spot à l'ask ; perp 5–40s plus tard, ou jamais |
| Résiduel | Toujours 0 | Non couvert sur 10 des 17 signaux |
| Frais sur un trade en cours | 2 takers | 2 takers, ou 4 si on abort |
| Moteur de PnL | Basis + funding | Ce que le spot a fait pendant qu'on attendait |
Un collègue a encore un notebook qui traite le pack comme un produit listé, BTC_CARRY, avec une « taxe d'exécution » forfaitaire de 2 bp. J'aimais ce notebook. Il produisait le même escalier que l'agent. Une taxe, c'est une moyenne. Les orphelins, c'est une distribution. Les moyennes ne se font pas écraser dans une heure peu liquide ; 0.40 BTC de spot orphelin, si.
Jeudi : l'orphelin, puis le café
3 secondes abortaient presque tout dès qu'on attendait aussi dans une file. Nous avons assoupli le paper à 15. À 02:11 UTC jeudi, le spot s'est exécuté et le perp non. J'étais encore dans la phase de deuil « il va rester, il va se faire lifter » quand je suis allé faire le café.
02:11:08 exécuté BTCUSDT 0.40 achat
02:11:08 soumis BTCUSDT-PERP 0.40 vente, post-only
02:11:41 toujours en cours
02:26:00 mark du résiduel −18.1 bps
Le BTC a lâché 18 bps dans une fenêtre calme. Le résiduel était tout le trade. Le funding, quand il a imprimé, a crédité un perp que nous n'avions pas. Le spot est resté là, à être long. La trajectoire overnight du backtest d'origine ne contenait jamais de position à une jambe, donc il n'avait jamais posé la question qu'on pose à tous les autres books : quelle est la pire barre que cet orphelin peut imprimer, et où est le stop pour une position qu'on n'avait pas l'intention de porter ?
Les 7 packs complets n'étaient pas des héros non plus. Le temps que la seconde jambe s'imprime, notre propre lift du spot avait déjà appuyé sur l'index, et le basis d'entrée était 3–5 bps plus mauvais que le snapshot qui avait déclenché le signal. Le seuil de 8 bps était un chiffre à propos d'un écran.
Ce qu'on sauterait la prochaine fois
Nous ne livrerions pas un multi-leg comme une seule fonction de fill. Même règle pour un carry perp-spot, un calendar, ou le call vertical qu'un agent soumet en deux limites et célèbre ensuite quand les deux « fill » au mid.
- État d'ordre par jambe, y compris les partiels. Traiter le pack comme parent de deux tickets.
- Un timer max-orphelin qui se déclenche vraiment, et un chemin de flatten qui paie 2 spreads à la sortie.
- Mark-to-book sur le résiduel, avec un stop dimensionné pour l'orphelin. Le net packé est le mauvais notionnel.
- Pas de carry crédité, pas de funding accumulé, tant que les deux jambes ne sont pas en place. Incomplet est incomplet.
- Refuser le mid-on-mid par défaut pour tout ce qui a deux tickets. S'il faut deviner, deviner plus défavorable sur la jambe mince et plus tard sur le second fill.
Et nous ne laisserions pas test_pack_is_atomic entrer dans le repo. L'atomicité est une propriété d'un matching engine, pas d'une fonction helper. Le spot BTC et le perpétuel BTC sont deux carnets qui riment parfois. Le paper, c'est là qu'ils cessent de rimer, et c'est la seule raison pour laquelle l'escalier a un jour été intéressant.



