← Todos los artículos

N.º 08

Un microsegundo son 200 metros de fibra

Antes de Nexow Studio, cofundé Nexow Systems, una firma de trading en Barcelona. Lo que estudiar el trading de alta y ultraalta frecuencia me enseñó sobre la latencia, y por qué su camino crítico se escribe en C y, ahora, en Rust.

Alex Colls Outumuro

Cofundador y CTO · · 6 min de lectura

Estampa 08, Pluma. Gotas y peines sobre un baño, sacada al construir el sitio.Pasa el puntero por encima para dejar caer tinta.

Nexow Studio no es la primera empresa que lleva este nombre. De 2015 a 2020 cofundé y dirigí Nexow Systems, una firma de trading en Barcelona. Su web la describía como una empresa de inversión privada especializada en trading cuantitativo multiactivo, algoritmos de baja latencia y market making. Cuando la cerramos, me quedé con la marca y con mis algoritmos. Esto es lo que me enseñó la parte rápida del mercado.

Rápido, y más rápido

El trading de alta frecuencia es menos una estrategia que una propiedad de algunas estrategias: su ventaja caduca con el tiempo. Un precio que está mal en un sitio se corrige unos milisegundos después, y quien actúa primero se queda la diferencia. Los creadores de mercado cotizan las dos puntas del libro y ganan el spread; los arbitrajistas compran en un sitio y venden en otro. Cada operación gana muy poco, así que solo compensa con volumen, y solo para quien llega primero.

La ultraalta frecuencia es el extremo de esa escala en el que la carrera se decide en microsegundos, medidos de cable a cable: desde que un paquete con un precio llega a la tarjeta de red hasta que una orden sale de ella. Ahí, el software corriente ya es demasiado lento. Las firmas colocan sus servidores en el propio centro de datos de la bolsa, se saltan la pila de red del sistema operativo y llevan la decisión misma a FPGA.

Nuestro libro estaba sobre todo en el lado lento. De las 53 estrategias de nuestra hoja de 2020, el grupo más numeroso, 27, era de medio plazo: posiciones de menos de cinco días de media. Ocho estaban clasificadas como de ultracorto plazo, menos de una hora de media, y siete de ellas nunca pasaron de idea. Para una mesa de UHFT, una hora es una era geológica.

La latencia es una distancia

La primera lección es de física. En el vacío, la luz recorre unos 300 metros en un microsegundo. En una fibra óptica, cuyo vidrio tiene un índice de refracción de alrededor de 1,47, recorre unos 204. Ningún código va más rápido que eso: cada microsegundo gastado en distancia es un microsegundo menos para el código.

t = d × n / c

1 km de fibra (n ≈ 1,47)            4,9 µs
Aurora → Nueva Jersey, ≈ 1.180 km
  en el vacío (n = 1)               3,9 ms
  en fibra (n ≈ 1,47)               5,8 ms

Son líneas rectas; los cables reales son más largos. Y esos dos lugares son el caso de manual. El futuro E-mini S&P 500, ES, cotiza en el CME, cuyo motor de casación está en Aurora, Illinois. SPY, un fondo que sigue el mismo índice, cotiza en bolsas de Nueva Jersey. En un minuto o en un día, los dos se mueven casi como uno solo. En milisegundos, la correlación se rompe: en The High-Frequency Trading Arms Race (2015), Budish, Cramton y Shim midieron que la oportunidad de arbitraje ES/SPY mediana duraba 97 ms en 2005 y 7 ms en 2011, sin que bajara su rentabilidad. La ventana se encogió hasta la longitud del cable.

Un estudio en C

Entre los archivos que guardé de aquellos años hay un experimento que Johann Christian Lotter publicó en 2017 en su blog, The Financial Hacker: un sistema de arbitraje ES/SPY para Zorro, la plataforma de backtesting en la que trabajaban nuestros quants, ejecutado sobre un día de datos tick, el 5 de octubre de 2016: las cotizaciones del CME para ES y el feed NxCore de Nanex para SPY. Los scripts de Zorro se escriben en lite-C, un dialecto de C, y toda la decisión cabe en una función. SPY vale una décima parte del índice, así que sus precios se multiplican por diez antes de llegar a ella.

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 {
    …

Léela como procesado de señal. ArbMean es una media móvil exponencial lenta de la diferencia entre los dos precios, con α = 0,001; Deviation es una rápida, α = 0,25, de cuánto se aleja esa diferencia de su media. Cuando se aleja más que el spread que habría que pagar más un umbral, el sistema compra el lado barato y vende el caro. No hay nada ingenioso. La lección está en el resultado publicado: en ese día, con 0,5 ms de latencia el backtest ganaba 793 $; con 4 ms, 273 $; con 6 ms, 205 $; con 10 ms perdía 15 $.

El mismo código, el mismo día. Solo había cambiado la distancia.

Por qué C, y por qué Rust

Una vez pagada la distancia, lo que queda es tu propio código, y ahí el enemigo no es la media sino la cola. Un sistema que suele responder en microsegundos y de vez en cuando en milisegundos pierde justo en esos momentos, y no caen al azar: es una ráfaga de datos de mercado lo que llena las colas y dispara las pausas, y una ráfaga es cuando se mueven los precios. Los recolectores de basura hacen pausas. Las llamadas al sistema, los fallos de página y los cerrojos que tiene otro hilo, también. C permite quitarlos todos: memoria dispuesta a mano y reservada antes de la apertura, hilos fijados a núcleos que giran en bucle en lugar de dormir.

A esa escala, el hardware es la estructura de datos. Una línea de caché ocupa 64 bytes en x86, y dos hilos que escriben variables distintas en la misma línea se la pelean. Los precios llegan como enteros y deben seguir siéndolo, porque la coma flotante binaria no puede representar ni siquiera 0,1 de forma exacta. Las líneas del CME de aquel experimento llegan así: 215800 con un factor de visualización de 0,01, es decir, 2.158,00 puntos de índice.

En Rust, una cotización puede llevar todo eso en su tipo. Es un esbozo, no código de uno de nuestros productos:

#[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 ese control. No hay recolector de basura, no se reserva nada que no pidas, y #[repr(C, align(64))] fija la disposición: la estructura de arriba contiene 32 bytes de datos y ocupa una línea de caché para ella sola. Lo que añade Rust es un compilador que rechaza el error más difícil de encontrar en C: dos hilos que tocan la misma memoria sin decirlo. En C, eso compila, funciona y puede aparecer mucho después como un precio equivocado. En Rust, un valor solo pasa de un hilo a otro si su tipo es Send, solo se comparte si es Sync, y, en Rust seguro, una carrera de datos es un error de compilación.

Lo que viene

Rust llegó después de Nexow Systems. Abrí el repositorio nexow en 2023 y escribí en él un motor de trading en 2025, en código abierto: el motor corre en su propio hilo y envía barras, órdenes y métricas por canales de crossbeam a un servidor Axum, con una interfaz en Nuxt encima. Hoy es un MVP que opera barras sintéticas con un random forest. Todavía no es un sistema de HFT, y no voy a fingir lo contrario: sus canales no tienen límite, así que reservan memoria a medida que crecen, y en un camino crítico eso pasa a ser un búfer circular dimensionado una sola vez, antes de la apertura.

Nexow Studio no es una firma de trading, pero las lecciones se quedaron con el nombre. Contar la distancia antes que el código. Medir la cola, no la media. Y que los enteros sigan siendo enteros.

Alex