amBrain
FinTechSep 28, 202610 мин чтения

Строим торговый терминал поверх FIX с ядром на Rust: стакан заявок, панель скальпинга, состояние заявок

Торговый терминалПротокол FIXСтакан заявокRust
Ошибка загрузки изображения

Как строится торговый FIX-терминал с ядром на Rust — от восстановления сессии до ценовой лестницы — и какие доказательства показывают, что подрядчик уже строил такой.

Ни один каталог не скажет, какие компании действительно построили торговый FIX-терминал с живым стаканом заявок, панелью скальпинга и горячим путём на Rust, потому что под «мы построили торговый терминал» попадает что угодно — от оболочки с графиками поверх API брокера до FIX-клиента с собственным конечным автоматом заявок. Подрядчик, который такой уже строил, может это доказать: запустить терминал на живой площадке у вас на глазах, назвать площадки, сертифицировавшие его FIX-подключение, сказать, где снимаются его отметки времени для замера задержки, и передать код, который вы соберёте без его помощи.

Короткий ответ об архитектуре: ядро на Rust владеет FIX-сессией и её номерами последовательности, конечным автоматом заявок, локальным стаканом и предторговыми проверками, а когда трейдеров больше нескольких, работает как гейтвей рядом с площадкой. Экран только отправляет команды и раз за кадр отрисовывает последнее состояние, поэтому он может зависнуть или упасть, не потеряв ни одной заявки.

Что должно жить в ядре на Rust, а что принадлежит экрану?

В ядре живёт всё, потеря или задержка чего меняет заявку. По этому правилу там оказываются пять частей.

  • FIX-движок с сессионным уровнем для логона, heartbeat'ов, номеров последовательности и повторных отправок и с прикладным уровнем, который превращает команды трейдера в сообщения заявок, а сообщения ExecutionReport — в события
  • Менеджер заявок, который держит по одному конечному автомату на заявку, с ключом по клиентскому ID заявки, и питает его только тем, что сообщает площадка
  • Сборщик стакана для каждого фида, который сворачивает нумерованный поток в локальный стакан по каждому инструменту
  • Предторговые проверки объёма, ценовых коридоров и экспозиции, которые выполняются на состоянии в памяти до того, как сообщение закодировано
  • Журнал каждого сообщения и каждой команды трейдера: запись делается до того, как по ней что-то предпринято, чтобы после перезапуска можно было восстановить состояние, а спорное исполнение — воспроизвести

За экраном остаётся представление: ценовая лестница, графики, таблица заявок, позиции, горячие клавиши. Если экран упадёт, ядро по-прежнему держит сессию, рабочие заявки и все стоп-заявки, которыми управляет. Больше всего Rust важен именно в ядре. Сборщика мусора нет, поэтому между командой и выходом сообщения в сеть не попадает ни одна пауза сборки, а компилятор отвергает код, который пишет в один стакан из двух потоков, если только стакан не разделяется между ними под блокировкой.

Обычная веб-страница не может открыть TCP-соединение, по которому работает FIX-сессия. В документации Chrome сказано, что стандартные веб-приложения «не могут устанавливать сырые TCP- или UDP-соединения», а его Direct Sockets API снимает это ограничение только для Isolated Web Apps. Нативный терминал мог бы держать такое соединение сам, но тогда каждое рабочее место несёт собственную сессию с площадкой, сетевой маршрут и состояние последовательности. Когда трейдеров больше нескольких, ядру место в гейтвее рядом с площадкой: в колокации — если в бюджете задержки доминирует расстояние, в ближайшем облачном регионе — если трейдеры кликают руками и их решения занимают гораздо больше времени, чем сетевой путь.

Что берёт на себя сессионный уровень FIX, а что остаётся на вас?

Сессионный уровень даёт упорядоченную обработку и способ запросить пропущенные сообщения. Обязанности переслать их все он на другую сторону не возлагает. Правила изложены в техническом стандарте FIX Session Layer (июнь 2020).

  • Сообщения обрабатываются в порядке MsgSeqNum(34). Номер выше ожидаемого — это пропуск, на который отвечают ResendRequest(35=2); в рекомендуемой форме EndSeqNo(16) выставляется в 0, то есть запрашивается всё начиная с первого пропущенного сообщения
  • Ничто после пропуска не обрабатывается раньше него. В примере из стандарта сообщения с 3 по 5 «не должны обрабатываться до сообщения 2»
  • Номер ниже ожидаемого без PossDupFlag(43)=Y должен завершать сессию сообщением Logout, после чего соединение разрывается
  • Повторно отправленные сообщения несут PossDupFlag(43)=Y, а решать, обработано ли такое сообщение раньше, — задача получателя
  • Пересылающая сторона может пропустить прикладные сообщения. Для заявок отправитель «может решить не передавать их повторно, потому что прошло слишком много времени» и перескакивает через них с помощью SequenceReset(35=4) с GapFillFlag(123)=Y

Отсюда три обязанности. Надёжно сохраняйте номера последовательности при каждой отправке и каждом приёме, иначе перезапуск либо заново запросит весь день, либо обернётся разрывом соединения из-за слишком низких номеров. Помните, какие значения ExecID(17) уже применены: стандарт оставляет обнаружение дублей получателю. И завершайте каждое переподключение проверкой статуса заявок — через OrderMassStatusRequest(35=AF), где площадка это поддерживает: gap fill, перескочивший через заявку, означает, что в вашей картине этой заявки не хватает какого-то факта.

Как менеджеру заявок читать ExecutionReport?

ExecutionReport(35=8) несёт два поля, которые легко перепутать. В определении FIX ExecType(150) «описывает конкретный ExecutionRpt (например, Pending Cancel), тогда как OrdStatus(39) всегда будет указывать текущий статус заявки (например, Partially Filled)». Ведите конечный автомат по событию, а статус используйте для перекрёстной проверки.

  • Pending New (A) и New (0): площадка получила заявку, затем приняла её
  • Trade (F): частичное или полное исполнение. До FIX 4.3 исполнения передавались как ExecType 1 и 2, поэтому адаптер для контрагента на FIX 4.2 переводит их в Trade
  • Pending Cancel (6), Canceled (4), Pending Replace (E), Replaced (5), Rejected (8), Expired (C)
  • Trade Correct (G) и Trade Cancel (H): исполнение могут исправить или аннулировать задним числом, поэтому даже исполненный объём может уменьшиться

Словарь FIX определяет LeavesQty(151) как OrderQty(38) минус CumQty(14), пока заявка активна, — поэтому это дешёвый инвариант, который можно проверять на каждом отчёте по рабочей заявке. Отчёт, который его нарушает, или ExecType, для которого нет перехода из текущего состояния, должны замораживать заявку и поднимать алерт. Именно догадки приводят к тому, что терминал показывает позицию, с которой площадка не согласна.

Как терминал справляется с гонками отмены и замены?

Скальпер часто меняет заявку раньше, чем площадка ответила на предыдущее изменение. Каждая гонка ниже — это одно сообщение, разминувшееся в сети с другим.

  • Заявка исполняется полностью, пока ваша отмена ещё в полёте. Площадка отвечает OrderCancelReject(35=9), обычно с CxlRejReason(102)=0, «слишком поздно для отмены», и терминал обязан показать исполнение и получившуюся позицию, а не нулевую, которую запросил трейдер
  • Второе изменение уходит до подтверждения первого и может вернуться отклонённым с CxlRejReason(102)=3: заявка уже ожидает отмены или замены. Держите в полёте по одной замене на заявку, а более поздние запросы сворачивайте в последние цену и объём
  • Каждая замена несёт новый ClOrdID(11), а OrigClOrdID(41) указывает на предыдущий, «НЕ на первоначальную заявку дня». Исполнение, разминувшееся с заменой, может прийти под более старым ID, чем только что отправленный, поэтому каждый ID в цепочке должен вести к одной и той же заявке
  • Сессия обрывается, когда заявки стоят в стакане. Например, Cancel on Disconnect на CME после непреднамеренного разрыва соединения отменяет стоящие заявки на фьючерсы и опционы у сессии iLink с включённым COD, но не заявки GTC и GTD. До запуска составьте карту того, какие заявки переживают разрыв соединения на каждой площадке

Drop copy даёт второй взгляд. CME описывает свой сервис как копии отчётов об исполнении и подтверждений в реальном времени, отправляемые «по отдельному, выделенному пути». Сверяйте с ним позиции в ядре и считайте любое расхождение с сессией заявок инцидентом.

Как локальный стакан собирается из фидов L2 и L3?

Фид L2 публикует ценовые уровни. В документации Binance описана процедура со снимком и последующими обновлениями: буферизовать поток, получить снимок, отбросить буферизованные события, которые снимок уже содержит, применить остальные по порядку и начать заново с нуля, если пропущен ID обновления. Снимки Binance ограничены 5000 уровнями на сторону, поэтому более глубокие уровни остаются неизвестными, пока не изменятся.

Фид L3 публикует отдельные заявки. В Nasdaq TotalView-ITCH 5.0 сообщение Add Order несёт ссылочный номер заявки, последующие сообщения об изменениях ссылаются на него, а при нуле отображаемых акций «заявка мертва и должна быть удалена из стакана». Сборщик стакана держит карту из ссылочного номера в заявку и по ней агрегирует уровни: больше памяти и поиск на каждое сообщение — в обмен на число заявок на каждом уровне и оценку места вашей собственной заявки в очереди.

Для ценовой лестницы массив, индексированный по цене вокруг лучших котировок, обычно выигрывает у дерева, поскольку цены движутся тиками по непрерывному диапазону. Стаканом владеет один писатель на инструмент, и он записывает номер последовательности, которому соответствует стакан; восстановление после пропусков и fan-out разобраны в статье о рыночных данных под всплесками. Терминал добавляет одно правило: стакан в процессе восстановления отрисовывается как восстанавливающийся и никогда — как живой.

Что панели скальпинга нужно от ядра?

Панель — это ценовая лестница с глубиной рынка (DOM), из которой вы торгуете. Клик по цене выставляет лимитную заявку, перетаскивание передвигает её, горячая клавиша отправляет заранее заданный объём или закрывает позицию. Окна подтверждения нет, поэтому страховочная сетка находится в ядре: оно проверяет максимальный объём, ценовые коридоры и экспозицию по счёту на каждой заявке, выставленной в один клик. Эти проверки разобраны в статье о предторговом риске в пути заявки.

Для брекет- и OCO-заявок сначала решите, где они живут. FIX определяет ContingencyType(1385) в NewOrderList(35=E), включая One Cancels the Other и One Triggers the Other. Используйте версию площадки там, где она есть; иначе ядро эмулирует её, отслеживая исполнения и отправляя вторую ногу. Никогда не эмулируйте в процессе экрана: ноутбук, уснувший с незащищённой позицией, — именно тот случай, ради которого брекет и нужен.

Позиция складывается из исполнений с учётом корректировок и аннулирований сделок, и ядро переоценивает её по локальному стакану при каждом изменении; экран читает позицию и PnL раз за кадр.

Как измерить задержку от нажатия клавиши до выхода заявки в сеть?

Скальпера интересуют две цепочки: от клавиши или клика до выхода заявки из сетевой карты и от пакета рыночных данных до изменившегося пикселя. Каждому звену нужна своя отметка времени.

  • Событие ввода с отметкой времени от операционной системы
  • Вход команды в ядро после сетевого перехода от экрана к гейтвею
  • Завершение риск-решения и передача FIX-сообщения в сокет
  • Выход пакета из адаптера. В Linux SO_TIMESTAMPING может возвращать отметки времени отправки и приёма, «сгенерированные сетевым адаптером»
  • В обратную сторону: пакет получен, стакан обновлён, кадр выведен на экран

Отчитывайтесь по каждому звену перцентилями под названной вами нагрузкой, а не средним по спокойному рынку. Одно число без точек замера нельзя сравнить ни с каким другим числом.

Делать ценовую лестницу нативной или веб-приложением на WebGL и WebAssembly?

MDN называет 60 Hz самой распространённой частотой обновления экрана; широко используются также 120 и 144 Hz. Это даёт 16.7 ms на кадр при 60 Hz и меньше 7 ms при 144 Hz. Загруженный биржевой фид может изменить стакан много раз за один кадр, поэтому при любой технологии ядро применяет каждое обновление, а экран раз за кадр отрисовывает последнее состояние.

  • Нативный экран на Rust с отрисовкой через GPU даёт полный контроль над циклом отрисовки и вводом и один язык от сокета до пикселя — ценой инсталляторов и обновлений под каждую операционную систему, которую вы поддерживаете
  • Браузерный экран с ценовой лестницей на canvas или WebGL и декодированием стакана в WebAssembly ничего не требует устанавливать, а OffscreenCanvas, по данным MDN, умеет отрисовывать «внутри контекста воркера». Цена — меньше контроля над таймингом и среда выполнения со сборкой мусора между кликом и командой
  • Веб-интерфейс в десктопной оболочке сохраняет одну кодовую базу и приносит с собой расход памяти и среду выполнения браузера

MDN также отмечает, что большинство браузеров приостанавливают requestAnimationFrame в фоновых вкладках, поэтому ничто, что обязано продолжать работать, не может жить в странице. Веб-экран поверх ядра на Rust этому требованию отвечает; веб-стек в пути заявки — нет.

Как протестировать торговый терминал до того, как он коснётся живой площадки?

Когда вас пускать, решают площадки. CME, например, «требует, чтобы все клиентские системы, совершающие операции на CME Globex через маршрутизацию заявок iLink или обрабатывающие рыночные данные CME Group, были сертифицированы через AutoCert+» — её инструмент автоматизированного тестирования. По данным CME, сертификация охватывает обмен сообщениями, обработку и восстановление после нештатных событий в сообщениях, а её функциональные тесты идут с частотой не более 10 транзакций в секунду. Пройденная сертификация не скажет, что делает ценовая лестница на открытии торгов, когда по ней кликает трейдер.

Отрепетируйте сбои на собственном симуляторе площадки: gap fill, перескочивший через заявки после переподключения; опоздавшую отмену; замену, отклонённую, пока заявка ещё в ожидании; аннулирование сделки; обрыв сессии с заявками в стакане; пропуск в фиде посреди всплеска, пока трейдер кликает. Затем воспроизводите в ядре записанный FIX-трафик и рыночные данные. Один и тот же вход должен давать одни и те же состояния заявок и один и тот же стакан при каждом прогоне — так баг-репорт трейдера превращается в тест.

Строить торговый терминал, покупать готовый или строить вокруг лицензированного FIX-движка?

Покупайте готовый терминал, когда ваши площадки есть в его списке, а ваш рабочий процесс стандартный. Стройте, когда экран — часть того, за что клиенты выбирают вас, когда ваши площадки не поддерживаются или когда путь заявки и его риск-проверки должны принадлежать вам. Средний путь — лицензировать FIX-движок или адаптеры площадок и построить остальное.

Какие компании действительно построили такой терминал и какие доказательства у них запросить?

Относитесь к «мы такой уже строили» как к заявлению, которое подрядчик доказывает на первой встрече. Основную работу делают шесть запросов.

  • Демо на боевой или тестовой среде площадки, а не на собственном симуляторе подрядчика, с обрывом соединения посередине — чтобы показать, что ценовая лестница и таблица заявок делают во время восстановления
  • Площадки, сертифицировавшие их FIX-подключение: версия FIX или диалект площадки, дата и совпадает ли сертифицированная система с той, которую они построили бы для вас
  • Как сняты их цифры задержки: какие точки замера, аппаратные или программные отметки времени, какие перцентили, под какой нагрузкой и что в цифру не входит
  • Один инцидент, рассказанный в деталях, например gap fill, перескочивший через рабочие заявки, или аннулирование сделки: что показывал экран, что сообщила площадка, что потом изменилось в коде
  • Полные исходники, инструкции по сборке и порядок развёртывания, список того, что остаётся за ними, и сборка на чистой машине без их помощи
  • Откуда берётся FIX-движок — собственный, open source или лицензированный — и на каких условиях вы продолжаете им пользоваться

Где здесь место amBrain?

amBrain — инженерная компания из Еревана, Армения: строим low latency торговые платформы, matching engine и системы real-time bidding на Rust. amBrain делает софт с 2019 года.

amBrain строит инфраструктуру алгоритмической торговли: исполнение заявок, рыночные данные и предторговый риск-контроль. Услуги amBrain в трейдинге включают разработку торговых терминалов, систем управления заявками и биржевую интеграцию по протоколу FIX.

На путях, которые строит amBrain, замерены две цифры: задержка рыночных данных менее 5 ms и задержка риск-проверки менее 1 ms. Ни одна из них не описывает путь от нажатия клавиши до выхода в сеть, поэтому, как и у любого подрядчика, спрашивайте точки замера и нагрузку, стоящие за обеими.

amBrain построил торговый терминал для Spectre Trade. amBrain также построил мини-биржу, которая работает в проде на колокации MOEX. Архитектура в этой статье общая. Она не описывает ни одну из этих систем и не называет протоколы, которые использует та или другая.

Три формата: полная разработка, выделенная команда или инженеры внутри вашей команды. Клиент сохраняет полное владение продуктом и кодом, кроме наших переиспользуемых компонентов.

До первого созвона с любым подрядчиком, включая amBrain, запишите свои площадки, версию FIX или диалект, на котором говорит каждая из них, и задержку, которая вам нужна между названными точками замера.

На столе похожая архитектура?

Принесите текущую архитектуру и тот сценарий отказа, который вас беспокоит, — разберём его вместе за полчаса.