← Tous les articles

N° 08

Une microseconde, c’est 200 mètres de fibre

Avant Nexow Studio, j’ai cofondé Nexow Systems, une société de trading à Barcelone. Ce que l’étude du trading à haute et à ultra-haute fréquence m’a appris sur la latence, et pourquoi son chemin critique s’écrit en C et, aujourd’hui, en Rust.

Alex Colls Outumuro

Cofondateur et CTO · · 7 min de lecture

Estampe 08, Plume. Des gouttes et des peignes sur un bain, tirée quand le site est construit.Pointez dessus pour faire tomber de l’encre.

Nexow Studio n’est pas la première entreprise à porter ce nom. De 2015 à 2020, j’ai cofondé et dirigé Nexow Systems, une société de trading à Barcelone. Son site la présentait comme une société d’investissement privée spécialisée dans le trading quantitatif multi-actifs, les algorithmes à faible latence et le market making. Quand nous l’avons fermée, j’ai gardé la marque et mes algorithmes. Voici ce que le côté rapide du marché m’a appris.

Vite, et plus vite

Le trading à haute fréquence est moins une stratégie qu’une propriété de certaines stratégies : leur avantage se périme avec le temps. Un prix faux à un endroit est corrigé quelques millisecondes plus tard, et celui qui agit le premier empoche la différence. Les teneurs de marché affichent les deux côtés du carnet et gagnent le spread ; les arbitragistes achètent à un endroit et revendent à un autre. Chaque transaction rapporte très peu : cela ne paie qu’en volume, et seulement pour ceux qui arrivent les premiers.

L’ultra-haute fréquence est le bout de cette échelle où la course se joue en microsecondes, mesurées de fil à fil : du moment où un paquet portant un prix atteint la carte réseau à celui où un ordre en repart. Là, un logiciel ordinaire est déjà trop lent. Les firmes installent leurs serveurs dans le centre de données de la bourse elle-même, contournent la pile réseau du système d’exploitation et confient la décision elle-même à des FPGA.

Notre propre portefeuille se tenait surtout du côté lent. Sur les 53 stratégies de notre fiche de 2020, le groupe le plus nombreux, 27, était à moyen terme : des positions tenues en moyenne moins de cinq jours. Huit étaient classées à ultra-court terme, moins d’une heure en moyenne, et sept d’entre elles n’ont jamais dépassé le stade de l’idée. Pour un desk UHFT, une heure, c’est une ère géologique.

La latence est une distance

La première leçon vient de la physique. Dans le vide, la lumière parcourt environ 300 mètres en une microseconde. Dans une fibre optique, dont le verre a un indice de réfraction d’environ 1,47, elle en parcourt environ 204. Aucun code ne va plus vite que ça : chaque microseconde passée en distance est une microseconde de moins pour le code.

t = d × n / c

1 km de fibre (n ≈ 1,47)            4,9 µs
Aurora → New Jersey, ≈ 1 180 km
  dans le vide (n = 1)              3,9 ms
  dans la fibre (n ≈ 1,47)          5,8 ms

Ce sont des lignes droites ; les vrais câbles sont plus longs. Et ces deux endroits sont le cas d’école. Le contrat à terme E-mini S&P 500, ES, se négocie au CME, dont le moteur d’appariement se trouve à Aurora, dans l’Illinois. SPY, un fonds qui suit le même indice, se négocie sur des bourses du New Jersey. Sur une minute ou une journée, les deux évoluent presque comme un seul. Sur quelques millisecondes, la corrélation s’effondre : dans The High-Frequency Trading Arms Race (2015), Budish, Cramton et Shim ont mesuré que l’opportunité d’arbitrage ES/SPY médiane durait 97 ms en 2005 et 7 ms en 2011, sans que sa rentabilité baisse. La fenêtre s’est réduite à la longueur du câble.

Une étude en C

Parmi les fichiers que j’ai gardés de ces années, il y a une expérience que Johann Christian Lotter a publiée en 2017 sur son blog, The Financial Hacker : un système d’arbitrage ES/SPY pour Zorro, la plateforme de backtest sur laquelle travaillaient nos quants, passé sur une journée de données tick, le 5 octobre 2016 : les cotations du CME pour ES et le flux NxCore de Nanex pour SPY. Les scripts Zorro s’écrivent en lite-C, un dialecte du C, et toute la décision tient dans une fonction. SPY vaut un dixième de l’indice : ses prix sont donc multipliés par dix avant d’y arriver.

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

Lisez-la comme du traitement du signal. ArbMean est une moyenne mobile exponentielle lente de l’écart entre les deux prix, avec α = 0,001 ; Deviation en est une rapide, α = 0,25, de la distance entre cet écart et sa moyenne. Quand elle dépasse le spread à payer plus un seuil, le système achète le côté bon marché et vend le côté cher. Rien de malin là-dedans. La leçon est dans le résultat publié : sur cette journée, avec 0,5 ms de latence, le backtest gagnait 793 $ ; avec 4 ms, 273 $ ; avec 6 ms, 205 $ ; avec 10 ms, il perdait 15 $.

Même code, même journée. Seule la distance avait changé.

Pourquoi le C, et pourquoi Rust

Une fois la distance payée, il reste votre propre code, et là, l’ennemi n’est pas la moyenne mais la queue de distribution. Un système qui répond d’habitude en microsecondes et de temps en temps en millisecondes se fait cueillir précisément à ces moments-là, et ils ne tombent pas au hasard : c’est une rafale de données de marché qui remplit les files et déclenche les pauses, et une rafale, c’est quand les prix bougent. Les ramasse-miettes font des pauses. Les appels système, les défauts de page et les verrous tenus par un autre thread aussi. Le C permet de tous les retirer : une mémoire disposée à la main et allouée avant l’ouverture, des threads épinglés à des cœurs qui tournent en boucle au lieu de dormir.

À cette échelle, le matériel est la structure de données. Une ligne de cache fait 64 octets sur x86, et deux threads qui écrivent des variables différentes sur la même ligne se la disputent. Les prix arrivent en entiers et doivent le rester, parce que la virgule flottante binaire ne sait même pas représenter 0,1 exactement. Les lignes du CME de cette expérience arrivent ainsi : 215800 avec un facteur d’affichage de 0,01, soit 2 158,00 points d’indice.

En Rust, une cotation peut porter tout cela dans son type. C’est une esquisse, pas du code d’un de nos produits :

#[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 garde ce contrôle. Pas de ramasse-miettes, rien n’est alloué sans qu’on le demande, et #[repr(C, align(64))] fixe la disposition : la structure ci-dessus contient 32 octets de données et occupe une ligne de cache à elle seule. Ce que Rust ajoute, c’est un compilateur qui refuse le bug le plus difficile à trouver en C : deux threads qui touchent la même mémoire sans le dire. En C, ça compile, ça tourne, et ça peut ressortir bien plus tard sous la forme d’un prix faux. En Rust, une valeur ne passe d’un thread à l’autre que si son type est Send, n’est partagée que s’il est Sync, et, en Rust sûr, une data race est une erreur de compilation.

Et maintenant

Rust est venu après Nexow Systems. J’ai ouvert le dépôt nexow en 2023 et j’y ai écrit un moteur de trading en 2025, en open source : le moteur tourne dans son propre thread et envoie barres, ordres et métriques par des canaux crossbeam à un serveur Axum, avec une interface Nuxt par-dessus. Aujourd’hui, c’est un MVP qui trade des barres synthétiques avec une forêt aléatoire. Ce n’est pas encore un système HFT, et je ne prétendrai pas le contraire : ses canaux sont non bornés, donc ils allouent en grandissant, et sur un chemin critique cela devient un tampon circulaire dimensionné une seule fois, avant l’ouverture.

Nexow Studio n’est pas une société de trading, mais les leçons sont restées avec le nom. Compter la distance avant le code. Mesurer la queue, pas la moyenne. Et garder les entiers entiers.

Alex