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 фирмы |
| TCP | 100-500 мкс | Гарантированная доставка | Стандартные клиенты |
| WebSocket | 1-10 мс | Гарантированная | Ритейл, веб-платформы |
| FIX over TCP | 200-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 сделке, например при исполнении опциона на корзину акций?