Real-Time Backend

Design: Trading Platform

В 2012 году Knight Capital потеряла USD 440 миллионов за 45 минут из-за одной строки кода. Как устроена система, где каждая миллисекунда стоит реальных денег?

  • NYSE обрабатывает более 1.5 миллиарда сообщений в день - это около 17 000 в секунду в среднем и в 10 раз больше в пики
  • NASDAQ matching engine: задержка матчинга менее 1 микросекунды - быстрее, чем свет проходит 300 метров
  • HFT-фирмы прокладывают кабели по дну океана, чтобы сократить задержку между Нью-Йорком и Лондоном с 65 до 59 мс - разница в 6 мс стоит миллионы долларов

Архитектура торговой платформы

Торговая платформа - это не просто CRUD. NYSE обрабатывает более 1.5 миллиарда сообщений в день. Задержка в миллисекунду стоит реальных денег - буквально.

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

Критический путь: Gateway -> Matching Engine. Всё остальное - асинхронно. Risk Engine блокирует только при явном нарушении лимитов.

  • **Gateway** - валидация FIX/WebSocket, rate limiting, TLS termination
  • **Order Router** - маршрутизация по инструменту (ticker -> matching engine shard)
  • **Risk Engine** - pre-trade проверки: баланс, позиционные лимиты, kill switch
  • **Matching Engine** - сердце биржи, один поток, zero GC
  • **Market Data Feed** - публикация котировок подписчикам
  • **Settlement** - постторговое клиринговое урегулирование

Катастрофа Knight Capital в 2012 году - классический контрпример. Устаревший код был случайно активирован на проде, и система начала торговать против рынка. За 45 минут потеряно USD 440 миллионов. Kill switch сработал слишком поздно.

Урок Knight Capital: risk engine должен быть in-process с matching engine, а не отдельным сервисом. Сетевой хоп - это уже слишком поздно.

Почему Risk Engine в HFT-системе размещают in-process с Matching Engine, а не как отдельный микросервис?

Order Matching Engine

Matching engine - самый требовательный компонент. NASDAQ декларирует задержку матчинга менее 1 микросекунды. Это в 1000 раз быстрее, чем запись в БД.

Основа - **Order Book**: двусторонняя сортированная структура. Bids (покупатели) - по убыванию цены. Asks (продавцы) - по возрастанию. Сделка происходит, когда лучший bid >= лучшего ask.

Matching engine - single-threaded by design. Никаких локов, никакого GC. В Java используют off-heap memory (Chronicle Map). В C++ - lock-free ring buffers. Один поток = детерминированный порядок.

Типы ордеров определяют поведение при матчинге:

ТипПоведениеПример использования
MarketИсполнить немедленно по лучшей ценеСрочная покупка
LimitТолько по указанной цене или лучшеСтандартная торговля
IOCИсполнить сейчас или отменить остатокАлго-трейдинг
FOKИсполнить полностью или отменить всёБлочные сделки
StopАктивировать при достижении ценыЗащита от потерь

Состояние хранится in-memory. Персистентность - через **event sourcing**: каждый ордер и каждая сделка пишутся в append-only лог (Kafka или собственный WAL). После краша - replay с последнего snapshot.

Matching engine сделан single-threaded. Что это гарантирует?

Market Data Feed

После каждой сделки и изменения order book биржа публикует market data. Это не просто уведомления - это основа price discovery для всего рынка.

Два типа данных:

  • **Level 1 (Top of Book)** - лучшие bid/ask и последняя цена. Для большинства ритейл-клиентов.
  • **Level 2 (Market Depth)** - полная книга ордеров на N уровней. Для алго-трейдеров и маркет-мейкеров.
  • **Trade Feed** - все исполненные сделки с ценой, объёмом и временем.

Транспорт выбирается по требованиям к задержке:

ПротоколЗадержкаНадёжностьПрименение
UDP Multicast< 10 мксБез гарантий (fire-and-forget)Co-located HFT фирмы
TCP100-500 мксГарантированная доставкаСтандартные клиенты
WebSocket1-10 мсГарантированнаяРитейл, веб-платформы
FIX over TCP200-1000 мксГарантированная + retryИнституциональные брокеры

NYSE использует UDP Multicast для primary feed. HFT-фирмы арендуют место в дата-центре биржи (co-location) и подключаются напрямую к multicast - это физически ближе к matching engine.

Пропуск пакетов в UDP - это норма. Клиент должен детектировать gap по sequence number и запросить retransmission через отдельный **recovery channel** (TCP). Это добавляет сложности, но low-latency важнее надёжности в primary feed.

На стороне издателя - **fan-out**: одно обновление book нужно разослать тысячам подписчиков. Используется Disruptor pattern или lock-free ring buffer: matching engine пишет в ring, несколько reader threads читают параллельно без блокировок.

Почему HFT-биржи используют UDP Multicast для market data вместо TCP, несмотря на возможные потери пакетов?

Portfolio Tracking

Portfolio tracking - противоположный конец спектра от matching engine. Здесь важна консистентность и корректность, а не субмикросекундная задержка.

Портфель изменяется двумя путями: **fills** (исполненные ордера из matching engine) и **price updates** (изменение рыночной стоимости позиций из market data feed). Первый путь - точный и транзакционный. Второй - приблизительный и потоковый.

  • **Позиции** - количество каждой бумаги (точно, из fills)
  • **Cash** - остаток после покупок/продаж (точно, транзакционно)
  • **Market Value** - позиции * текущая цена (приближённо, из feed)
  • **P&L** - прибыль/убыток относительно средней цены покупки
  • **Margin / Buying Power** - доступные средства с учётом leverage

Для real-time P&L используют **materialized view**: позиции хранятся в Redis, market prices приходят из feed и умножаются на количество. Это eventual consistent, но для UI - достаточно.

Официальный settlement (клиринг) происходит через T+2 дня через центральные депозитарии (DTCC в США). Внутри дня - только internal bookkeeping биржи или брокера.

WebSocket push для live P&L: клиент подписывается на свой userId. При каждом обновлении price feed для символов из портфеля - сервер пересчитывает и пушит delta. Throttling обязателен: не чаще 1 раза в 100 мс на клиента.

Portfolio tracking и matching engine должны быть в одной транзакции - иначе можно потерять сделку.

Fill сначала записывается в matching engine log (event sourcing), затем portfolio обновляется асинхронно. Идемпотентный replay гарантирует консистентность.

Объединение matching engine и portfolio в одну транзакцию убьёт производительность матчинга. Event sourcing + idempotent consumers - стандартный паттерн для разделения критического пути от downstream обработки.

Почему позиции (количество бумаг) хранятся транзакционно в SQL, а market value (стоимость портфеля) - в Redis с eventual consistency?

Итоги

  • Критический путь (Gateway -> Risk -> Matching) - синхронный и изолированный; всё остальное - асинхронно через event sourcing
  • Matching engine: single-threaded, in-memory, price-time priority; масштабируется шардированием по инструментам
  • Market data feed: UDP Multicast для минимальной задержки + TCP recovery channel для gap detection
  • Portfolio: позиции транзакционно в SQL, market value eventual consistent в Redis, live P&L через WebSocket push с throttling

Связанные темы

Торговая платформа использует паттерны из других областей real-time систем:

  • Event Sourcing — Append-only лог ордеров - единственный способ восстановить состояние matching engine после краша
  • CQRS — Matching engine (write path) и market data feed (read path) - классическое разделение команд и запросов
  • Disruptor Pattern — Lock-free ring buffer для fan-out market data от matching engine к тысячам подписчиков без GC pressure
  • WebSocket Real-time Push — Доставка live P&L и котировок клиентам с throttling и backpressure

Вопросы для размышления

  • Knight Capital потеряла USD 440 миллионов за 45 минут. Какие архитектурные решения в описанной системе предотвращают такой сценарий?
  • HFT-фирма хочет снизить задержку с 10 мкс до 1 мкс. Какие слои архитектуры можно оптимизировать, а какие уже на физическом пределе?
  • Matching engine шардируется по символам. Что происходит при cross-symbol сделке, например при исполнении опциона на корзину акций?

Связанные уроки

  • sd-01-intro
Design: Trading Platform

0

1

Войти