C++-паттерны для низколатентных приложений, включая высокочастотную торговлю

8/10

Пол Билокон, Бурак Гюндюз · Departments of Computing and Mathematics, Imperial College London · 11 сентября 2023 (arXiv: 8 сентября 2023)

Оригинал: Bilokon, P. and Gunduz, B. «C++ design patterns for low-latency applications including high-frequency trading», 2023 — arxiv.org/abs/2309.04259 (PDF).

Код: github.com/0burak/imperial_hft — репозиторий техник, бэктест пар и C++ Disruptor.

Оригинал на arXiv по лицензии arXiv nonexclusive-distrib 1.0 (не Creative Commons); перевод для личной коллекции, не для публикации.

Ключевые слова: C++; низкая латентность; HFT; кэш; Disruptor; парный арбитраж; Google Benchmark.

Аннотация

Работа закрывает пробел в оптимизации latency-critical кода, в первую очередь HFT. Три результата: репозиторий Low-Latency Programming со статистическими бенчмарками; оптимизация market-neutral парного статарбитража; реализация паттерна Disruptor на C++. Метрики — скорость, использование кэша, статистическая значимость. Сильнее всего латентность режут Cache Warming и constexpr. Дальше: расширять репозиторий, гонять алгоритм на живом рынке, склеить Disruptor с торговой логикой. Аудитория — и академия, и практика.

1. Введение

Цель — научить ускорять latency-critical код, в частности HFT. Индустрия (особенно бай-сайд публичных рынков) закрыта, новичку почти негде взять гайд. Авторы собрали репозиторий техник с бенчмарками, применили его к бэктесту пар и реализовали Disruptor как кандидат в OMS.

Литература по экономике HFT есть (Aldridge; Cartea et al.), по коду и кэшу — почти нет: конкурентное преимущество. Публичные доклады дают макрокартину без бенчмарков на торговой стратегии [34]. Книги по C++ объясняют язык и STL, но не ultra-low-latency HFT [22, 46, 50, 51]. Блоги дают средние наносекунды без разбора cache-access [11, 42, 44, 68, 69]. Здесь техники не только описывают, но внедряют и измеряют.

План: §2 — HFT, C++, паттерны, LMAX Disruptor, Google Benchmark, сеть; §3 — репозиторий (compile-time, оптимизация, данные, concurrency, system); §4 — пары GS/MS и CPU-оптимизации; §5 — C++ Disruptor vs очередь; §6 — оценка (отзывы, скорость, значимость, прибыль); §7 — выводы.

Вклад. (1) Практический репозиторий с бенчмарками. (2) Парный статарб: латентность бэктеста −87%, плюс доходность на истории. (3) Disruptor на C++ быстрее классической очереди — кандидат в OMS.

2. Фон

2.1. HFT

SEC Concept Release выделяет пять признаков: быстрые машины, колокация, короткие холды, пачки заявок с быстрыми отменами, почти нулевой overnight [18]. Пять блоков системы: data feed, OMS, стратегии, риск, исполнение/сеть. Здесь фокус — стратегия и OMS.

Четыре класса стратегий Aldridge [1]: арбитраж, направленная торговля на событиях, авто-MM, детекция ликвидности. Внутри — статарб, моментум, пары, illiquid detection, новости [34]. FPGA дают до 1000× против обычного CPU [37]; Donadio: программируемость, ёмкость, параллелизм, детерминизм [26]. Железо в работе не разбирают — только софт.

Aldridge и Krawciw (2017): в 2016 HFT держал 10–40% объёма акций и 10–15% FX/commodities [2]. Экономических текстов много, вычислительных мало. Cook разбирал variadic templates, loop unrolling, constexpr [46] — авторы это расширяют измерениями.

2.2. C++

Latency-critical код любит compile-time, не runtime: C++, Java, Rust; Python мешает интерпретатор. Ghosh: C++ компилируется, близок к железу, даёт контроль ресурсов [33]. «Zero-overhead»: нет GC, ручное владение памятью. Rust — ownership/borrowing без GC. Java живёт в latency, если объекты преаллоцировать (Disruptor LMAX) или выключить GC (Nasdaq OMX [1]); Carruth: при удачной VM Java иногда быстрее C++ [21].

Сдвиг работы на compile-time: шаблоны и CRTP вместо виртуального диспетча, inline, constexpr. На латентность ещё влияют компилятор, архитектура, флаги — смотреть ассемблер (Compiler Explorer, Godbolt). Rasovsky: clock_gettime на чужом бенчмарк-сайте казался в 85 раз медленнее TSC, на своём сервере — всего в 2 раза [23]. Контекст важнее «магического числа».

2.3. Паттерны

Список техник репозитория: cache warming; compile-time dispatch (специализация шаблонов / перегрузка); constexpr; loop unrolling; short-circuit; signed vs unsigned; не мешать float и double; branch prediction/reduction; slowpath removal; SIMD; prefetch; lock-free; inlining.

2.4. LMAX Disruptor

Фреймворк LMAX Exchange: продюсеры и консьюмеры без mutex. Блокировка = арбитраж + context switch. Disruptor: преаллоцированный ring buffer, sequence numbers, wait strategy. Рис. 1 оригинала — UML [63].

2.5–2.6. Бенчмарки и сеть

Google Benchmark удобен, но не моделирует NIC, колокацию и биржевой jitter. Сеть HFT: оптика, колокация, kernel bypass (OpenOnload, VMA, DPDK). Barbosa: DPDK даёт примерно 7× меньше латентности, чем обычный Linux-стек [7]. Регуляторика (Reg NMS, MiFID) задаёт рамку, но не заменяет микрооптимизацию горячего пути.

3. Репозиторий низколатентного кода

Пять полок: compile-time, оптимизация циклов/веток, работа с данными, concurrency, system (prefetch, kernel bypass — обзорно). Бенчмарки — Google Benchmark + perf по кэшу.

Cache warming. Cold: случайный обход большого массива — $267\,685\,006$ нс. Warm: последовательный pre-pass — $25\,635\,035$ нс, около $90\%$ ускорения. Miss rate почти тот же ($73.96\%$ vs $71.55\%$), но ссылок на кэш меньше ($146$ млн → $61$ млн). Табл. 1.

Compile-time dispatch. Runtime: $2.60$ / $2.15$ нс на два derived; compile-time: $1.92$ нс на оба. Экономия $0.68$ и $0.23$ нс — нет vtable.

Constexpr. Факториал 10: constexpr $0.245$ нс/итер., runtime $2.69$ нс ($\approx 90.88\%$). Это не «constexpr всегда быстрее runtime», а перенос вычисления на компиляцию; компилятор и так может свернуть обычный код.

Inlining (always_inline): $1.90$ vs $2.39$ нс ($\approx 20.5\%$). Риск — раздуть бинарь и убить I-cache.

Loop unrolling — меньше условных прыжков, больше работы на итерацию. В сводной табл. §3.6: $+72\%$.

Short-circuit, SIMD, типы, ветки, lock-free. Сводка рис. 11 / §3.6:

Kernel bypass в репозитории скорее обзор: DPDK и co. требуют NIC и аккуратного security model.

4. Парный трейдинг

Изолированные паттерны склеили в market-neutral пары (Tartaglia / Morgan Stanley, конец 1980-х). Спред mean-reverting; лонг дешёвого + шорт дорогого. Коинтеграция vs корреляция: нужна стационарная линейная комбинация.

Два I(1)-ряда $Y_t=\rho Y_{t-1}+\epsilon^Y_t$, $X_t=\beta X_{t-1}+\epsilon^X_t$ коинтегрированы, если $Z_t=Y_t-\gamma X_t$ стационарен. Тест Engle–Granger (statsmodels.tsa.stattools.coint) на дневных adj close Yahoo, 5 лет: GS и MS, $t=-3.7684$, $p\approx 0.0149$ — нуль «нет коинтеграции» отвергается на 5%. Это статарб, не чистый арбитраж; Johansen для $\gt 2$ рядов не брали.

Сигнал: окно $N$, среднее и $\sigma$ спреда, $z=(X-\mu)/\sigma$. $z\gt 1$ — шорт пары (продать GS, купить MS); $z\lt -1$ — лонг; $|z|\lt 0.8$ — закрыть. Старт 1 млн USD → 1 328 581 USD, Шарп $1.09$: \[ \mathrm{Sharpe}=\frac{R_p-R_f}{\sigma_p}. \] Оценка альфы не цель работы — цель латентность бэктеста. Рис. 12–17: спред, z, сделки, баланс, P&L.

Четыре оптимизации горячего пути: inlining mean/stddev; SIMD + unrolling; фиксированный массив спреда с кольцевым индексом (без аллокаций); все вместе. Без оптимизаций $519\,772$ нс. Табл. 3: inlining $406\,709$ ($21.41\%$); SIMD+unroll $355\,618$ ($31.28\%$); fixed array $266\,146$ ($48.58\%$); combined $65\,580$ ($87.33\%$).

Ограничения: цены уже в векторе — это не live feed; окно должно делиться на 4; AVX2 нужен на железе.

5. Disruptor на C++

Продюсер занимает слот ring buffer, пишет, публикует sequence. Буфер преаллоцирован, GC/new на горячем пути нет. Sequencer — точка синхронизации. Event processor читает по sequence, несколько консьюмеров без локов, можно граф зависимостей. Sequence barrier + wait strategy: busy-spin / yield / sleep — торг латентность vs CPU. Событие в демо — строка; в OMS это был бы ордер.

Бенчмарк: Disruptor vs std::queue + mutex + condition_variable, NUM_EVENTS в отдельном потоке. Табл. 4 (фрагмент): на 1000 событий очередь $881\,092$ нс vs Disruptor $451\,251$ ($\approx 48.8\%$); на $10^6$ — $884$ млн vs $543$ млн ($\approx 38.7\%$). Причины: нет lock contention, кольцо лучше сидит в кэше, предсказуемый reuse памяти, слабее связка producer/consumer.

6. Оценка

6.1. Репозиторий

17 студентов Imperial, UCL, KCL, Oxford (11 CS, 6 math/physics/eng). Полнота $8.3$ (мин $6.5$, макс $9.1$), ясность $8.7$ ($7.8$–$8.9$). Просили глубже data handling и concurrency; мало комментариев в примерах. Воспроизводимость: Google Benchmark + makefile на g++; железо/компилятор всё равно двигают цифры.

6.2. Торговый код

10 прогонов: mean $517\,559$ → $65\,588$ нс ($-87.38\%$), std $4233$ → $400$ нс — ровнее. Combined в изоляции совпадает с табл. 3. Violin: неоптимизированный код скошен вниз (аномалия). Инструкций: baseline $6.01$ млрд, combined $3.27$, buffer $8.27$, inlining $9.07$, SIMD $8.35$. Cache miss: baseline $16.0\%$, combined $33.9\%$ (меньше ссылок/времени важнее процента), buffer $19.1\%$, inlining $19.2\%$, SIMD $16.8\%$. Парный t-тест по 10 точкам: большие t, крошечные p — не шум; выборка маленькая.

Зачем резать латентность: Baron [9] — самые быстрые HFT зарабатывают больше (короткая информация + риск-менеджмент). Relative vs absolute speed. Пары здесь — абсолютная скорость бэктеста. LAO по Wah [66]: crossed market ($B_{M1}\gt A_{M2}$), NBBO, ненулевое окно. Ende et al. [8]: $+1\%$ латентности $\approx +0.9\%$ шанса попасть под неблагоприятное изменение книги. Авторы экстраполируют: $-87.32\%$ латентности $\approx -78.59\%$ такой экспозиции (табл. 10). Это линейная сказка с чужого регресса, не live P&L.

6.3. Disruptor

На малых $N$ разрыв узкий ($2464$ нс при 10 событиях), на $10^6$ — сотни миллионов нс. Cache-miss у Disruptor чуть хуже на 10–100 событиях и лучше с $10^3$. На 10 событиях на $\approx 586\,000$ инструкций меньше. 20 замеров при $N=1000$: очередь $931\,255\pm 453\,766$ нс, Disruptor $74\,908\pm 53\,600$; $t=22.596$, $p=1.243\times 10^{-23}$. Wait strategy в тесте — yield; busy-spin и sleep не сравнивали. Память и CPU load не мерили.

7. Заключение

Три артефакта: репозиторий (cache warming и constexpr $\approx 90\%$; unrolling / lock-free / short-circuit сильны; slowpath и signedness $\approx 12\%$; студенты 8.3/8.7); пары ($-87\%$ латентности, более плотный std); C++ Disruptor почти вдвое быстрее очереди, разрыв растёт с нагрузкой.

Дальше: variadic templates, kernel bypass и сеть в репозитории; live feed и cache warming на OMS; склеить Disruptor с ордерами и прогнать end-to-end.

Литература

Перевод: §§1–7, без листингов бенчмарков. Рисунки 1–28 и полные таблицы — в PDF оригинала и репозитории. · arXiv:2309.04259 · лицензия arXiv nonexclusive-distrib 1.0