La funzione si chiamava enter_pack. Quello avrebbe dovuto essere il campanello d'allarme. Un agente di ricerca aveva scritto un cash-and-carry su BTC: quando il perpetual scambiava più di otto basis point sopra lo spot, compra spot, shorta il perp, resta fermo. Funding e basis avrebbero fatto il resto. Il backtest eseguiva entrambe le gambe nello stesso millisecondo, a mid, più un tick di cortesia. La curva di equity era una scalinata. Nessuno l'avrebbe definita emozionante. Era proprio quello il punto di forza.
Lunedì l'abbiamo promosso a paper.
Lunedì: il pack che si eseguiva sempre
Il primo segnale è arrivato alle 11:17 UTC. Il basis a schermo era 9.4 bps, perp ricco. Nel backtest, enter_pack restituiva due fill e un residuo pari a zero. In paper ha restituito un fill spot di 0.40 BTC all'ask, e uno short a riposo sul perp che non ha scambiato.
Lo spot aveva pagato una commissione taker di 4 bp e circa 1 bp di spread. Il prezzo del perp che volevamo era ancora sul book, poi non lo era più. Venti secondi dopo il basis quotato era 5 bps, perché avevamo appena liftato lo spot. Quaranta secondi dopo il perp è stato eseguito, dopo il movimento. Eravamo packed. Eravamo packed a un basis che non pagava più l'ingresso e l'uscita.
Il backtest avrebbe registrato "+9.4 bps di carry, residuo 0." Il blotter aveva 4 bps di costo taker sullo spot, un long scoperto di 40 secondi e un gap di ingresso di circa 5. Avevo un campione di uno e, nonostante tutto, troppa fiducia nella scalinata.
Martedì: abbiamo aperto la funzione
enter_pack prendeva una lista di gambe e un mid obiettivo. Ciclava, eseguiva ogni gamba a mid * (1 ± slip) e scriveva una riga di portafoglio. Non c'era un secondo timestamp. Non c'era un ramo in cui la gamba due non esistesse. Lo unit test asseriva residual == 0.
È un test che ti fa sentire un adulto. Non fallirà mai.
Abbiamo spezzato il pack in due ordini con stato proprio: submitted, partial, filled, canceled. Incomplete è diventato un oggetto di prima classe. Il replay storico non aveva ancora un book reale, quindi abbiamo barato. Ritardare il secondo fill di un'estrazione dall'attesa di lunedì (media 26 secondi, una coda che non mi piaceva) e far driftare il prezzo di quanto aveva fatto lo spot in quella finestra. Grezzo, ma la scalinata ha preso intaccature prima di pranzo: pack che entravano a 3 bps, pack che non diventavano mai pack.
Mercoledì: tre secondi, poi scaricalo
Se la seconda gamba non è in posizione entro tre secondi, flatten della prima. Avremmo dovuto partire da lì. Sembra conservativo finché non metti un prezzo all'abort. Flatten significa che hai attraversato lo spot per entrare e lo attraversi per uscire. Due commissioni taker, due spread, niente carry, niente funding. Nel replay riscritto quegli abort non erano rari. Si concentravano sulle barre in cui il basis sembrava più "reale", che di solito sono le barre in cui qualcosa sta già fluendo attraverso lo spot.
Mercoledì sera avevamo diciassette segnali paper con cui discutere.
| Backtest, lunedì mattina | Paper, entro mercoledì | |
|---|---|---|
| Ingresso | Entrambe le gambe a mid, un solo timestamp | Spot all'ask; perp 5–40s dopo, o mai |
| Residuo | Sempre 0 | Scoperto su 10 di 17 segnali |
| Commissioni su un trade in essere | Due taker | Due taker, o quattro se abortiamo |
| Driver di PnL | Basis + funding | Quel che faceva lo spot mentre aspettavamo |
Un collega ha ancora un notebook che tratta il pack come un prodotto quotato, BTC_CARRY, con una "tassa di esecuzione" fissa di 2 bp. Quel notebook mi piaceva. Produceva la stessa scalinata dell'agente. Una tassa è una media. Gli orfani sono una distribuzione. Le medie non vengono travolte in un'ora illiquida; 0.40 BTC di spot residuo sì.
Giovedì: il residuo, poi il caffè
Tre secondi abortivano quasi tutto una volta che aspettavamo anche in coda. Abbiamo allentato il paper a quindici. Giovedì alle 02:11 UTC, lo spot è stato eseguito e il perp no. Ero ancora nella fase di lutto del "resterà, verrà liftato" quando sono andato a fare il caffè.
02:11:08 eseguito BTCUSDT 0.40 buy
02:11:08 inviato BTCUSDT-PERP 0.40 sell, post-only
02:11:41 ancora in lavorazione
02:26:00 mark del residuo −18.1 bps
BTC è sceso di 18 bps in una finestra quieta. Il residuo era l'intero trade. Il funding, quando è stato stampato, ha accreditato un perp che non avevamo. Lo spot se ne stava lì, long. Il path overnight del backtest originale non conteneva mai una posizione a una gamba, quindi non si era mai posto la domanda che rivolgiamo a ogni altro book: qual è la barra peggiore che questo residuo può stampare, e dov'è lo stop per una posizione che non intendevamo tenere?
Nemmeno i sette pack completi erano eroi. Quando la seconda gamba è stata stampata, il nostro stesso lift sullo spot aveva già premuto sull'indice, e il basis di ingresso era 3–5 bps peggiore dello snapshot che aveva sparato il segnale. La soglia di otto bps era un numero che riguardava uno schermo.
Cosa eviteremmo la prossima volta
Non rilasceremmo un multi-leg come una singola funzione di fill. Stessa regola per un carry perp-spot, un calendar, o la call vertical che un agente invia come due limit e poi festeggia quando entrambi "fillano" a mid.
- Stato dell'ordine per gamba, inclusi i parziali. Tratta il pack come parent di due ticket.
- Un timer max-orphan che scatta davvero, e un percorso di flatten che paga due spread in uscita.
- Mark-to-book sul residuo, con uno stop dimensionato per il residuo. Il net packed è il nozionale sbagliato.
- Nessun carry accreditato, nessun funding maturato, finché entrambe le gambe non sono in posizione. Incomplete è incomplete.
- Rifiuta mid-on-mid come default per qualsiasi cosa abbia due ticket. Se devi indovinare, indovina peggio sulla gamba sottile e più tardi sul secondo fill.
E non lasceremmo test_pack_is_atomic entrare nel repo. Atomic è una proprietà di un matching engine, non di una funzione helper. BTC spot e BTC perpetual sono due book che a volte fanno rima. Il paper è dove smettono di far rima, ed è l'unica ragione per cui la scalinata è mai stata interessante.



