← Tots els articles

Núm. 08

Un microsegon són 200 metres de fibra

Abans de Nexow Studio, vaig cofundar Nexow Systems, una firma de trading a Barcelona. El que estudiar el trading d’alta i d’ultraalta freqüència em va ensenyar sobre la latència, i per què el seu camí crític s’escriu en C i, ara, en Rust.

Alex Colls Outumuro

Cofundador i CTO · · 7 min de lectura

Estampa 08, Ploma. Gotes i pintes sobre un bany, treta en construir el web.Passa-hi el punter per deixar caure tinta.

Nexow Studio no és la primera empresa que porta aquest nom. Del 2015 al 2020 vaig cofundar i dirigir Nexow Systems, una firma de trading a Barcelona. El seu web la descrivia com una empresa d’inversió privada especialitzada en trading quantitatiu multiactiu, algorismes de baixa latència i market making. Quan la vam tancar, em vaig quedar la marca i els meus algorismes. Això és el que em va ensenyar la part ràpida del mercat.

Ràpid, i més ràpid

El trading d’alta freqüència és menys una estratègia que una propietat d’algunes estratègies: el seu avantatge caduca amb el temps. Un preu que és erroni en un lloc es corregeix uns quants mil·lisegons després, i qui actua primer es queda la diferència. Els creadors de mercat coticen les dues puntes del llibre i guanyen l’spread; els arbitratgistes compren en un lloc i venen en un altre. Cada operació guanya molt poc, així que només surt a compte amb volum, i només per a qui arriba primer.

L’ultraalta freqüència és l’extrem d’aquesta escala en què la cursa es decideix en microsegons, mesurats de cable a cable: des que un paquet amb un preu arriba a la targeta de xarxa fins que una ordre en surt. Allà, el programari corrent ja és massa lent. Les firmes posen els servidors dins el mateix centre de dades de la borsa, se salten la pila de xarxa del sistema operatiu i porten la decisió mateixa a FPGA.

La nostra cartera era sobretot al costat lent. De les 53 estratègies del nostre full del 2020, el grup més nombrós, 27, era de mitjà termini: posicions de menys de cinc dies de mitjana. Vuit estaven classificades d’ultracurt termini, menys d’una hora de mitjana, i set d’elles no van passar mai de la idea. Per a una taula d’UHFT, una hora és una era geològica.

La latència és una distància

La primera lliçó és física. Al buit, la llum recorre uns 300 metres en un microsegon. En una fibra òptica, el vidre de la qual té un índex de refracció d’uns 1,47, en recorre uns 204. Cap codi no va més de pressa que això: cada microsegon gastat en distància és un microsegon menys per al codi.

t = d × n / c

1 km de fibra (n ≈ 1,47)            4,9 µs
Aurora → Nova Jersey, ≈ 1.180 km
  al buit (n = 1)                   3,9 ms
  en fibra (n ≈ 1,47)               5,8 ms

Són línies rectes; els cables reals són més llargs. I aquests dos llocs són el cas de manual. El futur E-mini S&P 500, ES, cotitza al CME, que té el motor de casament a Aurora, Illinois. SPY, un fons que segueix el mateix índex, cotitza en borses de Nova Jersey. En un minut o en un dia, tots dos es mouen gairebé com un de sol. En mil·lisegons, la correlació es trenca: a The High-Frequency Trading Arms Race (2015), Budish, Cramton i Shim van mesurar que l’oportunitat d’arbitratge ES/SPY mediana durava 97 ms el 2005 i 7 ms el 2011, sense que baixés la seva rendibilitat. La finestra es va encongir fins a la llargada del cable.

Un estudi en C

Entre els fitxers que vaig guardar d’aquells anys hi ha un experiment que Johann Christian Lotter va publicar el 2017 al seu blog, The Financial Hacker: un sistema d’arbitratge ES/SPY per a Zorro, la plataforma de backtesting on treballaven els nostres quants, executat sobre un dia de dades tick, el 5 d’octubre de 2016: les cotitzacions del CME per a ES i el feed NxCore de Nanex per a SPY. Els scripts de Zorro s’escriuen en lite-C, un dialecte del C, i tota la decisió cap en una funció. SPY val una desena part de l’índex, així que els seus preus es multipliquen per deu abans d’arribar-hi.

int tradeHFT(double AskSPY,double BidSPY,double AskES,double BidES)
{
  double SpreadSPY = AskSPY-BidSPY, SpreadES = AskES-BidES;
  double Arbitrage = (AskSPY+BidSPY-AskES-BidES)/2;
  static double ArbMean = Arbitrage;
  ArbMean = 0.999*ArbMean + 0.001*Arbitrage;
  static double Deviation = 0;
  Deviation = 0.75*Deviation + 0.25*(Arbitrage - ArbMean);
  static int Position = 0;
  if(Position == 0) {
    if(Deviation > SpreadSPY+THRESHOLD) return Position = 1;
    if(-Deviation > SpreadES+THRESHOLD) return Position = 2;
  } else {
    …

Llegeix-la com a processament de senyal. ArbMean és una mitjana mòbil exponencial lenta de la diferència entre els dos preus, amb α = 0,001; Deviation n’és una de ràpida, α = 0,25, de quant s’allunya aquesta diferència de la seva mitjana. Quan s’allunya més que l’spread que caldria pagar més un llindar, el sistema compra el costat barat i ven el car. No hi ha res d’enginyós. La lliçó és al resultat publicat: aquell dia, amb 0,5 ms de latència el backtest guanyava 793 $; amb 4 ms, 273 $; amb 6 ms, 205 $; amb 10 ms en perdia 15.

El mateix codi, el mateix dia. Només havia canviat la distància.

Per què C, i per què Rust

Un cop pagada la distància, el que queda és el teu propi codi, i allà l’enemic no és la mitjana sinó la cua. Un sistema que sol respondre en microsegons i de tant en tant en mil·lisegons perd just en aquests moments, i no cauen a l’atzar: és una ràfega de dades de mercat el que omple les cues i dispara les pauses, i una ràfega és quan es mouen els preus. Els recol·lectors de brossa fan pauses. Les crides al sistema, les fallades de pàgina i els bloquejos que té un altre fil, també. El C permet treure’ls tots: memòria disposada a mà i reservada abans de l’obertura, fils fixats a nuclis que giren en bucle en lloc de dormir.

A aquesta escala, el maquinari és l’estructura de dades. Una línia de memòria cau ocupa 64 bytes en x86, i dos fils que escriuen variables diferents a la mateixa línia se la disputen. Els preus arriben com a enters i n’han de continuar sent, perquè la coma flotant binària no pot representar ni tan sols 0,1 de manera exacta. Les línies del CME d’aquell experiment arriben així: 215800 amb un factor de visualització de 0,01, és a dir, 2.158,00 punts d’índex.

En Rust, una cotització pot dur tot això en el seu tipus. És un esbós, no codi d’un dels nostres productes:

#[repr(C, align(64))]
pub struct Quote {
    pub ts_ns: u64,
    pub bid: i64,
    pub ask: i64,
    pub bid_qty: u32,
    pub ask_qty: u32,
}

Rust conserva aquest control. No hi ha recol·lector de brossa, no es reserva res que no demanis, i #[repr(C, align(64))] fixa la disposició: l’estructura de dalt conté 32 bytes de dades i ocupa una línia de memòria cau per a ella sola. El que hi afegeix Rust és un compilador que rebutja l’error més difícil de trobar en C: dos fils que toquen la mateixa memòria sense dir-ho. En C, això compila, funciona i pot aparèixer molt més tard com un preu equivocat. En Rust, un valor només passa d’un fil a un altre si el seu tipus és Send, només es comparteix si és Sync, i, en Rust segur, una data race és un error de compilació.

El que ve

Rust va arribar després de Nexow Systems. Vaig obrir el repositori nexow el 2023 i hi vaig escriure un motor de trading el 2025, en codi obert: el motor s’executa en el seu propi fil i envia barres, ordres i mètriques per canals de crossbeam a un servidor Axum, amb una interfície en Nuxt al damunt. Avui és un MVP que opera barres sintètiques amb un random forest. Encara no és un sistema d’HFT, i no faré veure el contrari: els seus canals no tenen límit, així que reserven memòria a mesura que creixen, i en un camí crític això passa a ser un búfer circular dimensionat un sol cop, abans de l’obertura.

Nexow Studio no és una firma de trading, però les lliçons es van quedar amb el nom. Comptar la distància abans que el codi. Mesurar la cua, no la mitjana. I que els enters continuïn sent enters.

Alex