Patru numere, toate măsurate pe propriul nostru stack de paper trading, o strategie, semnale de 1 minut pe perpetualul BTCUSDT, 31 de zile și 1,418 ordine. Timpul median de la închiderea barei până la confirmarea ordinului de către exchange: 195 ms. Percentila 99: 2,970 ms. Mediana pe 1% din minutele cu cea mai mare volatilitate realizată: 840 ms. Iar constanta de latență care a stat în config-ul de backtest aproape tot anul precedent: 0.
Trei dintre aceste numere sunt plictisitoare. Toată lumea știe că zero e greșit, toată lumea îl înlocuiește până la urmă cu ceva, iar acel ceva e aproape întotdeauna o constantă pe care cineva a ales-o după ureche. 100 ms, 250 ms, „hai să fim conservatori, 500.” Numărul care contează cu adevărat e 840, pentru că spune că latența ta e o funcție de starea pieței, iar stările de piață în care explodează sunt aceleași stări de piață în care semnalul tău crede că a găsit ceva.
Unde se duc cele 195 de milisecunde
Iată bugetul complet, măsurat etapă cu etapă, cu timestamp-uri de ceas monotonic scrise la fiecare hop. Box-ul e o instanță AWS în ap-northeast-1, aceeași regiune cu endpoint-urile venue-ului, cam ce e mai bun la care poți spera cu un setup de tip retail, fără colocare.
| Etapă | p50 | p95 | p99 |
|---|---|---|---|
| Închidere bară → eveniment WS kline-closed primit | 128 ms | 402 ms | 1,940 ms |
| Parsare + actualizare stare features | 3 ms | 9 ms | 28 ms |
| Decizie: evaluare model + verificări de risc | 11 ms | 22 ms | 60 ms |
| Trimitere ordin → confirmare exchange | 47 ms | 180 ms | 1,100 ms |
| End-to-end (măsurat, nu însumat) | 195 ms | 690 ms | 2,970 ms |
Două lucruri de reținut înainte de partea interesantă. Primul: medianele nu se adună: 128 + 3 + 11 + 47 = 189, iar mediana end-to-end măsurată e 195. Percentilele nu sunt aditive, și dacă îți construiești estimarea de latență însumând p99-urile pe etape, obții 3,128 ms când p99-ul real e 2,970. Suficient de aproape aici, grav greșit când etapele sunt corelate în cealaltă direcție. Măsoară tot traseul.
Al doilea: uită-te la primul rând. Două treimi din bugetul median se duc așteptând ca exchange-ul să-ți spună că bara s-a închis. Nu rețeaua, nu codul tău, nu rutarea ordinelor. Venue-ul agregă tranzacțiile într-un kline, decide că minutul s-a încheiat și face push. Push-ul ăla are o coadă în față, iar coada e comună cu toți ceilalți subscriberi de pe stream.
Cele 840
Grupează în bucket-uri fiecare dintre cele 1,418 ordine după volatilitatea realizată a minutului care a produs semnalul, și distribuția se destramă.
| Bucket de volatilitate a barei-semnal | Ordine | p50 end-to-end | p99 end-to-end |
|---|---|---|---|
| Cuartila inferioară | 201 | 141 ms | 488 ms |
| Cele două cuartile din mijloc | 796 | 183 ms | 1,210 ms |
| Cuartila superioară | 407 | 312 ms | 2,880 ms |
| Top 1% din toate minutele | 14 | 840 ms | 4,100 ms |
Toate etapele se degradează împreună, de aceea efectul se compune. Coada de agregare și de broadcast a venue-ului e alimentată de încărcarea matching-engine-ului, deci un minut cu 11,000 de tranzacții e împins mai târziu decât un minut cu 600. Propriul tău proces gestionează mai multe actualizări de depth și mai multe print-uri de tranzacții în aceeași fereastră, deci actualizarea de features care de obicei durează 3 ms durează 28. Calea REST a ordinului concurează cu panica tuturor celorlalți, și începi să vezi câte un 503-and-retry care te costă o secundă întreagă de unul singur. Coeficientul de corelație dintre numărul de tranzacții din bara-semnal și latența end-to-end în eșantionul nostru a fost 0.41. Nu e enorm. Destul cât să strice o constantă.
Iar strategiile care supraviețuiesc unui filtru de primă trecere sunt, covârșitor, cele care tranzacționează breakout-uri, reversal-uri, dislocări, cascade de lichidare. Se declanșează exact în bucket-ul unde latența e cea mai proastă. Un semnal de momentum care se activează doar când minutul s-a mișcat 40 bps e un semnal care s-a aranjat, structural, să fie servit întotdeauna de cea mai lentă versiune a stack-ului tău.
Am petrecut odată o zi urmărind un outlier de 3.6 secunde care apărea cam la aceeași oră în fiecare noapte. Era logrotate, care făcea copy-truncate pe un fișier de 2 GB pe același volum EBS pe care scria procesul, blocând event loop-ul. Piața era nevinovată. Măsoară întâi, apoi dă vina pe microstructură.
Ce face asta unui fill
Prețul derivează cât aștepți. Într-un minut oarecare, mișcarea adversă medie pe mediana noastră de 195 ms a fost de circa 1.1 bps împotriva direcției semnalului. Pe minutele din cuartila superioară, la 312 ms, 5.8 bps. Pe minutele din top-1%, la 840 ms, aproximativ 14 bps de derivă adversă înainte ca ordinul să ajungă măcar în book.
Pe perpetualele BTC spread-ul abia participă: un tick pe o cotație de 84,000 e 0.012 bps, deci dauna e derivă adversă, nu cost de crossing. Mută aceeași strategie pe un perp mid-cap și al doilea termen se trezește — spread-ul top-of-book în eșantionul nostru a trecut de la 1.4 bps în minutele calme la 6.2 bps în cuartila superioară, iar depth-ul la acel touch s-a subțiat cu mai mult de jumătate. Plătești deriva și spread-ul mai larg și depth-ul mai slab, toate deodată, toate pentru că au aceeași cauză.
Când am reluat aceleași 31 de zile cu latență condiționată per bară în loc de un 200 ms plat, prețul mediu de intrare din backtest s-a mutat cu 3.1 bps mai rău, concentrat aproape în întregime în 9% din tranzacții. Divergența de preț de intrare backtest-versus-paper a scăzut de la 4.2 bps la 0.9 bps. Sharpe-ul in-sample pe strategia aia a trecut de la 1.7 la 1.1, care e numărul onest și a fost dintotdeauna.
Deci ce pui, de fapt, în config
Nu un scalar. Ce rulăm acum:
- Eșantionează, nu seta. Extrage latența pentru fiecare ordin simulat dintr-o distribuție empirică condiționată de activitatea propriu-zisă a acelei bare — numărul de tranzacții și randamentul absolut sunt suficiente, și ambele sunt deja într-un kline standard. Estimeaz-o o dată per venue per tip de ordin, reestimează lunar.
- Păstrează coada. Nu estima o lognormală și gata; p99-ul trăiește în furtuni de retry-uri și pauze de GC pe care nicio distribuție lină nu le reproduce. Folosim quantilele empirice cu o coadă reeșantionată deasupra p95.
- Latența se aplică și la cancel-uri. Un stop-replacement sau un cancel de hedge e supus aceleiași întârzieri, iar în piețe rapide cancel-ul tău concurează cu un fill. Dacă simulatorul tău anulează instant, modelul tău de risc e ficțiune.
- Adaugă o gardă de staleness și simuleaz-o. A noastră aruncă orice semnal a cărui bară e mai veche de 1.5 secunde la sosire. E un comportament real, cu cost real — elimină tranzacții în tăcere, și trebuie să le elimine pe aceleași în backtest.
- Recalibrează față de paper. Paper trading-ul e locul de unde iei ground truth-ul. Loghează cele patru timestamp-uri de hop pe fiecare ordin și compară distribuțiile cu ce a presupus simulatorul, săptămânal.
Alte clase de active, aceeași boală. Acțiuni SUA: banda consolidată își adaugă propria întârziere de agregare peste cea a brokerului tău, și se lărgește sub presiunea ratei de mesaje din același motiv structural. Piețe de predicție: introducere de ordine doar pe REST pe majoritatea venue-urilor, și am măsurat mediane sub-100 ms care sar la câteva secunde când lovește un eveniment de rezoluție și toată lumea sosește simultan. Opțiuni crypto: cotația de pe care prețuiești poate fi veche de două secunde exact în minutul în care underlying-ul s-a mișcat.
Forma generală a greșelii e să tratezi un parametru de execuție ca o constantă independentă când e o funcție a aceleiași variabile de stare care îți conduce semnalul. Comisioanele se comportă; funding-ul se comportă; latența nu. Dacă o să ții minte un singur număr din toate astea, ține raportul: p99-ul nostru e de cincisprezece ori mediana, iar semnalul nostru sosește disproporționat aproape de capătul p99.
Hai, instrumentează cele patru hop-uri. E o după-amiază de muncă și o să-ți spună mai multe despre strategia ta decât încă o săptămână de căutare de parametri.
← Toate articolele


