Спотовый матчинг-движок — маленький конечный автомат в кольце жёстких ограничений: приоритет цены и времени, частичные исполнения, хвост задержки, который не уезжает под всплеском нагрузки. Вот как устроены стакан, цикл сведения, журнал и стенд реплея — и где язык перестаёт помогать.
У матчинг-движка спотовой биржи узкая работа. Он берёт упорядоченный поток команд — новая заявка, отмена, замена — применяет их к стакану по приоритету цены и времени и выдаёт упорядоченный поток событий: сделки, обновления стакана, отказы, подтверждения.
Вся сложность идёт от трёх ограничений, надстроенных над этой работой: результат должен быть одинаковым при каждом реплее того же входа, остаток частично исполненной заявки должен сохранять своё место в очереди, а хвост задержки не должен уезжать при всплеске.
Стакан — это две стороны, каждая из которых упорядоченная по цене коллекция уровней. Уровень — не число: это очередь стоящих заявок по данной цене в порядке поступления. Сведение постоянно трогает лучший уровень и редко — глубокие, поэтому структура выбирается под этот шаблон доступа, а не ради изящества.
Следствие интрузивных очередей и индексных handle'ов в том, что стоящая заявка за свою жизнь никогда не переезжает в памяти. Её место в очереди — свойство её ссылок, а не того, где она лежит, и именно это делает частичные исполнения дешёвыми дальше.
Входящая агрессивная заявка идёт по противоположной стороне от лучшей цены вглубь. На каждом уровне она обходит FIFO-очередь с начала. Она останавливается, когда цена уровня перестаёт устраивать входящую заявку или когда её количество доходит до нуля.
Частично исполненная стоящая заявка сохраняет своё место. Остаток остаётся в начале своей очереди с исходным номером поступления, потому что исполнение меняет количество и больше ничего. Частично исполненная агрессивная заявка, если это обычный лимит, становится стоящей в конце своего ценового уровня с новым номером поступления — она пришла сейчас, а не раньше.
Семантика типов заявок — решения, принимаемые на границе этого цикла, а не внутри него. Immediate-or-cancel отбрасывает остаток вместо того, чтобы поставить его в стакан. Fill-or-kill сначала делает сухой прогон и либо исполняется целиком, либо отклоняется. Post-only отклоняется, если заявка пересеклась бы при поступлении. Держать это вне цикла — значит оставить цикл единственным местом, где меняется состояние стакана.
Защита от самосделок, минимальные количества, проверка тика и лота тоже относятся к тому, что идёт до цикла. Заявка, дошедшая до сведения, уже доказано корректна, поэтому в цикле нет ветвей ошибок, которые его замедляли бы или вызывали споры.
Средняя задержка редко бывает проблемой. Проблема — худшее наблюдение во время всплеска, то есть тогда, когда движок важнее всего и когда пауза stop-the-world вероятнее всего и случится. Под управляемой средой со сборщиком мусора эту паузу планирует сборщик, а не вы, и приходится она на середину того всплеска, который мусор и породил.
Ручное выделение памяти — уменьшенная версия той же проблемы. Аллокатор общего назначения может обходить списки свободных блоков, брать лок или просить память у ядра, и именно этот вызов попадает в хвост. Лечение одно в обоих случаях: не выделять память на горячем пути вовсе.
Движок детерминирован, когда одна и та же входная последовательность даёт ту же выходную, байт в байт, на другой машине и через год. Всё, что читает системные часы, планирование потоков или порядок обхода хеш-таблицы внутри пути сведения, это свойство ломает.
Поэтому отметки времени — вход, а не то, что движок читает сам. Секвенсор ставит штамп на команду, когда её принимает, а цикл сведения обращается со штампом как с данными. Случайность, если она нужна, берётся из генератора с зерном, и это зерно — часть журнала.
Восстановление — не фича, прикрученная после того, как заработало сведение. Движок пишет append-only журнал принятых команд в порядке последовательности, а стакан в памяти есть не что иное, как результат свёртки этого журнала. Восстановление после сбоя — это его повторное проигрывание.
Журнал пишет движок, но долговечность — свойство пути хранения и того, на скольких машинах есть запись до отправки подтверждения. Это решение про репликацию и железо, и именно там на самом деле выигрывается или проигрывается время восстановления.
Детерминизм — это то, что делает движок тестируемым. Раз один и тот же вход даёт один и тот же выход, записанная сессия становится регрессионным тестом, а найденный однажды сбой воспроизводится точно, а не отлавливается заново.
Эталонная модель стоит больше, чем кажется. Две реализации, написанные по одной спецификации, расходятся ровно там, где спецификация была двусмысленной, а правила сведения полны двусмысленностей на краях: пересекающиеся лимиты, нулевые остатки, отмены, гоняющиеся с исполнениями.
Rust убирает целый класс проблем, а не ускоряет цикл сам по себе. Сборщика мусора нет, поэтому никакая пауза не планируется у вас за спиной. Владение делает дисциплину единственного писателя тем, что проверяет компилятор, а не тем, что должно заметить ревью. Handle'ы в слабе и интрузивные связи, легко дающие ошибку в языке без времён жизни, здесь проверяемы. Паника при переполнении целых в debug-сборках ловит класс багов, тихо портящих стакан.
Честная граница в том, что почти всё, что определяет хвост задержки, — не язык:
Rust и стоит чего-то. Borrow checker тормозит первые недели дизайна, который ещё движется, экосистема биржевых протоколов тоньше, чем в более старых языках, а unsafe-блоки вокруг lock-free структур требуют той же дисциплины ревью, что и аналогичный код где угодно. Выбор Rust — решение про хвост задержки и про безопасность памяти в ядре с единственным писателем, а не про комфорт разработчика.
amBrain строит торговую инфраструктуру в Ереване, Армения, с 2019 года, с горячими путями на Rust: рыночные данные доставляются менее чем за 5 ms, предторговые риск-проверки — менее чем за 1 ms. Если вы проектируете матчинг-движок и хотите разобрать структуру стакана, формат журнала или стенд реплея, такой разговор стоит провести до того, как написана первая строка горячего пути.
Наша инженерная команда специализируется на решениях FinTech. Давайте обсудим, как воплотить ваш проект в жизнь.