Funktionen hette enter_pack. Det borde ha varit varningen. En research agent hade skrivit en BTC cash-and-carry: när perpetualen handlades mer än åtta baspunkter över spot, köp spot, shorta perpen, sitt stilla. Funding och basis skulle göra resten. Backtestet fyllde båda benen i samma millisekund, på mid, plus en tick av artighet. Equity-kurvan var en trappa. Ingen skulle kalla den spännande. Det var säljargumentet.
Vi lyfte den till paper på en måndag.
Måndag: paketet som alltid fylldes
Första signalen landade 11:17 UTC. Basis på skärmen var 9.4 bps, perp rich. I backtestet returnerade enter_pack två fills och en residual på noll. I paper returnerade den en spot-fill på 0.40 BTC på ask, och en vilande short på perpen som inte gick.
Spot hade betalat 4 bp i taker-avgift och ungefär 1 bp i spread. Perp-priset vi ville ha låg fortfarande i boken, sen gjorde det inte det. Tjugo sekunder senare var den noterade basisen 5 bps, för att vi precis hade lyft spot. Fyrtio sekunder senare fylldes perpen, efter rörelsen. Vi hade paketet. Vi hade paketet på en basis som inte längre betalade för att komma in och ut.
Backtestet skulle ha bokfört "+9.4 bps carry, residual 0." Blottern hade 4 bps i spot-takerkostnad, en 40-sekunders ohedgad long och ett entry-gap på ungefär 5. Jag hade ett sample på ett och, ändå, för mycket tilltro till trappan.
Tisdag: vi öppnade funktionen
enter_pack tog en lista med ben och en target mid. Den loopade, fyllde varje ben på mid * (1 ± slip), och skrev en portföljrad. En andra tidsstämpel fanns inte. Det fanns ingen gren där ben två inte fanns. Enhetstestet assertade residual == 0.
Det är ett test som får en att känna sig vuxen. Det kommer aldrig att faila.
Vi bröt paketet i två ordrar med eget state: inskickad, partiell, fylld, annullerad. Incomplete blev ett first-class-objekt. Den historiska replayen hade fortfarande ingen riktig bok, så vi fuskade. Fördröj den andra fillen med ett slumpdrag ur måndagens väntetid (medel 26 sekunder, en svans jag inte gillade), och drifta priset med vad spot än gjorde i det fönstret. Grovt, men trappan fick hack före lunch: paket som gick in på 3 bps, paket som aldrig blev paket.
Onsdag: tre sekunder, sen dumpa
Om det andra benet inte är på inom tre sekunder, flattena det första. Vi borde ha börjat där. Det låter konservativt tills man prissätter aborten. Att flattena betyder att du korsade spot för att komma in och du korsar den för att komma ut. Två taker-avgifter, två spreadar, ingen carry, ingen funding. I den omskrivna replayen var de aborterna inte sällsynta. De klustrade på de bars där basisen såg mest "verklig" ut, vilket oftast är de bars där något redan flödar genom spot.
Till onsdagskvällen hade vi sjutton papersignaler att bråka med.
| Backtest, måndag morgon | Paper, till onsdagen | |
|---|---|---|
| Ingång | Båda benen på mid, en tidsstämpel | Spot på ask; perp 5–40s senare, eller aldrig |
| Residual | Alltid 0 | Ohedgad på 10 av 17 signaler |
| Avgifter på en aktiv trade | Två takers | Två takers, eller fyra om vi avbryter |
| PnL-drivare | Basis + funding | Vad spot än gjorde medan vi väntade |
En kollega har fortfarande en notebook som behandlar paketet som en noterad produkt, BTC_CARRY, med en platt 2 bp "exekveringsskatt". Jag gillade den notebooken. Den producerade samma trappa som agenten. En skatt är ett medelvärde. Föräldralösa är en fördelning. Medelvärden blir inte överkörda i en tunn timme; 0.40 BTC överbliven spot kan.
Torsdag: överblivet, sen kaffe
Tre sekunder abortade nästan allt när vi dessutom väntade i en kö. Vi luckrade paper till femton. 02:11 UTC på torsdagen fylldes spot och perpen gjorde det inte. Jag var fortfarande i "den kommer vila, den kommer lyfta"-fasen av sorgearbetet när jag gick och kokade kaffe.
02:11:08 fylld BTCUSDT 0.40 köp
02:11:08 inskickad BTCUSDT-PERP 0.40 sälj, post-only
02:11:41 fortfarande working
02:26:00 residualmark −18.1 bps
BTC föll 18 bps i ett tyst fönster. Residualen var hela traden. Funding, när den printade, krediterade en perp vi inte hade. Spot bara satt där och var long. Det ursprungliga backtestets overnight-path innehöll aldrig en enbensposition, så det hade aldrig ställt frågan vi ställer till varje annan bok: vad är den värsta bar den här överblivna kan printa, och var är stoppen för en position vi inte menade att hålla?
De sju fulla paketen var inga hjältar heller. När det andra benet printade hade vår egen spot-lift redan tryckt på indexet, och entry-basis var 3–5 bps sämre än snapshotet som hade triggat signalen. Åtta-bps-tröskeln var ett tal om en skärm.
Vad vi skulle skippa nästa gång
Vi skulle inte shippa en multi-leg som en enda fill-funktion. Samma regel för en perp-spot-carry, en calendar, eller den call vertical en agent skickar in som två limitordrar och sen firar när båda "fylls" på mid.
- Orderstate per ben, inklusive partials. Behandla paketet som förälder till två tickets.
- En max-orphan-timer som faktiskt triggar, och en flatten-väg som betalar två spreadar på vägen ut.
- Mark-to-book på residualen, med en stop dimensionerad för det överblivna. Det packade nettot är fel notional.
- Ingen carry krediterad, ingen funding upplupen, förrän båda benen är på. Incomplete är incomplete.
- Vägra mid-på-mid som default för något som har två tickets. Om du måste gissa, gissa sämre på det tunna benet och senare på den andra fillen.
Och vi skulle inte släppa in test_pack_is_atomic i repot. Atomic är en egenskap hos en matching engine, inte en hjälpfunktion. BTC spot och BTC perpetual är två böcker som ibland rimmar. Paper är där de slutar rimma, vilket är den enda anledningen till att trappan någonsin var intressant.



