C++-паттерны для низколатентных приложений, включая высокочастотную торговлю
Пол Билокон, Бурак Гюндюз · 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:
- Cache warming $\approx 90\%$, constexpr $\approx 90\%$, unrolling $72\%$, lock-free $63\%$, short-circuit $50\%$, SIMD $49\%$, не мешать float/double $52\%$;
- compile-time dispatch $26\%$, inlining $20.5\%$, prefetch $23.5\%$, branch reduction $36\%$;
- slowpath removal $12\%$, signed vs unsigned $12.15\%$.
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.
Литература
- Aldridge, I. High-Frequency Trading. Wiley, 2013; Aldridge & Krawciw, Real-Time Risk, 2017. ↑
- Baron, M. et al. Risk and return in high-frequency trading. 2019. ↑
- Barbosa. DPDK vs Linux networking stack.
- Cartea, Á., Jaimungal, S., Penalva, J. Algorithmic and High-Frequency Trading. CUP, 2015.
- Carruth, C. talks on C++ performance / GC.
- Cook, C. C++ optimisation for HFT (variadic templates, unrolling, constexpr).
- Donadio. FPGA characteristics for trading. ↑
- Ende, B. et al. Latency and adverse order-book changes. 2011. ↑
- Engle, R. and Granger, C. Co-integration and error correction. 1987. ↑
- Ghosh. Why C++ for low-latency.
- Godbolt, M. Compiler Explorer.
- LMAX. Disruptor technical paper / Thompson et al. [63].
- Rasovsky. clock_gettime vs TSC in HFT benchmarking.
- Sharpe, W. The Sharpe ratio.
- SEC Concept Release on Equity Market Structure.
- Stroustrup, B. The C++ Programming Language; The Design and Evolution of C++.
- Tartaglia / Gatev, Goetzmann, Rouwenhorst. Pairs trading.
- Van Ness et al. NASDAQ HFT concentration. 2005. ↑
- Wah, E. Latency arbitrage / crossed markets.
- Yahoo! Finance. Adjusted close GS, MS.
Перевод: §§1–7, без листингов бенчмарков. Рисунки 1–28 и полные таблицы — в PDF оригинала и репозитории. · arXiv:2309.04259 · лицензия arXiv nonexclusive-distrib 1.0