Как на самом деле работают матчинговые движки криптобирж
Содержание
- Программа, которая решает судьбу вашей биржи
- Что на самом деле делает матчинговый движок
- Структуры данных стакана ордеров: где рождается или умирает производительность
- Приоритет цена-время: контракт справедливости
- Задержка и пропускная способность: что на самом деле значит «быстро»
- Типы ордеров: сложнее, чем кажется
- Крайние случаи, которые разрушат вашу биржу
- Стратегии масштабирования: дилемма однопоточности
- Память против диска: вопрос сохранения данных
- Как Codono справляется со всем этим
- Итог
Программа, которая решает судьбу вашей биржи
Каждый основатель биржи хочет говорить об интерфейсе. Эффектные графики TradingView, переключатель тёмной темы, мобильное приложение. И да, всё это важно для привлечения пользователей. Но если вы хотите понять, что на самом деле определяет, переживёт ли ваша биржа свой первый настоящий торговый день, смотрите на матчинговый движок.
Матчинговый движок — это ключевой программный компонент, который принимает входящие ордера на покупку и продажу, определяет, какие из них можно свести друг с другом, и исполняет сделки. Звучит просто. На деле — нет. Плохо спроектированный движок будет незаметно терять ордера под нагрузкой, создавать фантомные исполнения, выдавать некорректные балансы и порождать такие бухгалтерские кошмары, от которых увольняются аудиторы. Я отладил достаточно таких систем, чтобы знать эту боль не понаслышке.
Людей удивляет, насколько обманчиво малой выглядит базовая логика. Простейший алгоритм сведения ордера на покупку с существующими ордерами на продажу можно написать менее чем в 100 строк кода. Но разница между такой игрушечной реализацией и торговым движком production-уровня — это примерно 50 000 строк обработки крайних случаев, логики персистентности, механизмов восстановления и оптимизации производительности.
Давайте разберём, что на самом деле происходит внутри современного матчингового движка и почему важно каждое архитектурное решение.
Что на самом деле делает матчинговый движок
Забудьте на минуту об определении из учебника. Вот что движок делает на практике, каждую миллисекунду каждого торгового дня:
- Принимает входящий ордер — запрос на покупку или продажу с типом (лимитный, рыночный, стоп), ценой (возможно), количеством и идентификатором трейдера.
- Валидирует ордер — проверяет, что цена соответствует размеру тика, количество — минимальному лоту, у трейдера достаточно средств, а ордер не нарушает правила биржи.
- Пытается свести — сканирует противоположную сторону стакана в поисках совместимых размещённых ордеров. Покупка по $50 000 может свестись с любой продажей по $50 000 или ниже.
- Исполняет сделки — для каждого совпадения создаёт запись о сделке, обновляет балансы обоих трейдеров и корректирует стакан.
- Обрабатывает остаток — если входящий ордер исполнен не полностью, он либо размещается в стакане (лимитные ордера), либо отменяется (рыночные ордера при отсутствии ликвидности).
- Публикует события — рассылает исполнения сделок, обновления стакана и изменения балансов остальной системе.
Шаги с 1 по 6 должны выполняться атомарно. Не «согласованно в конечном счёте», не «сверим позже». Атомарно. Если шаг 4 успешен, а шаг 6 падает, вы получаете скрытые исполнения, которых трейдер никогда не увидит. Если шаг 3 успешен, а шаг 4 падает частично, в стакане появляется фантомная ликвидность. Обе ситуации разрушают доверие быстрее любого взлома.
Именно поэтому матчинговый движок почти всегда является однопоточным последовательным процессором для каждой торговой пары. Конкурентность здесь не друг, а источник проблем. Подробнее об этом — в разделе о масштабировании.
Структуры данных стакана ордеров: где рождается или умирает производительность
Стакан ордеров — это структура данных, которая хранит все размещённые лимитные ордера, организованные по ценовым уровням. У него две стороны: биды (ордера на покупку) и аски (ордера на продажу). Биды отсортированы от высшей цены к низшей. Аски — от низшей к высшей. Лучший бид и лучший аск — цены на вершине каждой стороны — определяют спред.
Выбор правильной структуры данных — одно из самых значимых архитектурных решений, которые вы примете. Вот реальные варианты:
Отсортированные массивы или связные списки
Самый простой подход. Вы поддерживаете отсортированный список ценовых уровней, и каждый уровень содержит очередь ордеров.
Плюсы: Крайне просто реализовать. Дружелюбное к кэшу расположение в памяти. Быстрая итерация при обходе стакана.
Минусы: Вставка — O(n), где n — число ценовых уровней. Для пары вроде BTC/USDT с тысячами активных ценовых уровней это становится медленным. Удаление тоже O(n) из-за сдвигов.
Вывод: Подойдёт для прототипа. Неприемлемо для production с серьёзными объёмами.
Сбалансированные бинарные деревья поиска (красно-чёрные деревья, АВЛ-деревья)
Именно это использует большинство production-движков. Самобалансирующееся BST даёт O(log n) на вставку, удаление и поиск. Лучший бид и лучший аск также легко находить, храня указатели на минимальный и максимальный узлы.
Плюсы: O(log n) для всех критических операций. Хорошо изученные алгоритмы. Легко поддерживать указатели на лучший бид и лучший аск.
Минусы: Структуры, перегруженные указателями, вызывают промахи кэша. Повороты дерева при перебалансировке добавляют постоянные накладные расходы.
Вывод: Отраслевой стандарт — и на то есть причины. Используйте именно это, если только у вас нет совсем специфических требований, которые толкают вас в другую сторону.
Хэш-таблицы со списками с пропусками
Некоторые HFT-компании используют хэш-таблицу с ключом по ценовому уровню в сочетании со списком с пропусками (skip list) или похожей структурой для упорядочивания. Это даёт O(1) на поиск конкретного ценового уровня и O(log n) на упорядоченный обход.
Плюсы: Молниеносно для типичного случая добавления ордера на существующий ценовой уровень.
Минусы: Сложнее реализовать корректно. Компонент skip list всё равно требует O(log n) на вставку новых ценовых уровней.
Вывод: Стоит рассмотреть, если профилирование показывает, что большинство ордеров попадает на существующие ценовые уровни (что действительно верно для активно торгуемых пар).
Реальный гибридный вариант
Вот как на самом деле выглядит большинство серьёзных реализаций: сбалансированное BST для индекса ценовых уровней, где каждый уровень содержит двусвязный список (FIFO-очередь) отдельных ордеров. Плюс поддерживается хэш-таблица от ID ордера к узлу ордера, так что отмена — это O(1) на поиск плюс O(1) на удаление из связного списка.
Эта хэш-таблица для отмены ордеров критически важна. На любой активной бирже объём отмен обычно в 10–50 раз превышает объём исполненных сделок. Маркет-мейкеры постоянно выставляют и отменяют ордера. Если ваш путь отмены медленный, весь движок захлёбывается.
Приоритет цена-время: контракт справедливости
Приоритет цена-время, также называемый FIFO-сведением, — стандартный алгоритм практически каждой легитимной биржи. Правило простое:
- Ордера в первую очередь приоритизируются по цене. Ордер на покупку по $50 001 исполняется раньше покупки по $50 000.
- На одном ценовом уровне ордера приоритизируются по времени. Ордер, пришедший первым, исполняется первым.
Звучит очевидно, но у этого глубокие последствия. Приоритет цена-время — это фундаментальная гарантия справедливости. Он означает: если вы разместили ордер первым по данной цене, никто не может перепрыгнуть очередь, отправив ту же цену позже. Ваша позиция в очереди — ваша собственность.
Почему это так важно? Потому что без строгого приоритета цена-время вы открываете дверь целому классу манипулятивных практик. Оператор биржи теоретически мог бы перестраивать очередь в пользу определённых маркет-мейкеров. Трейдер с более быстрым соединением мог бы отменять и перевыставлять ордера, чтобы пролезть вперёд. Это не гипотетические сценарии — такое происходило на сомнительных биржах, и именно поэтому регуляторы обращают внимание на честность матчинговых движков.
Некоторые биржи экспериментировали с альтернативными схемами приоритета — пропорциональным сведением (pro-rata, используется на некоторых фьючерсных рынках), где исполнения на ценовом уровне распределяются пропорционально между всеми размещёнными ордерами, или приоритетом размер-время, где крупные ордера получают преимущество. Но для стандартной спотовой биржи приоритет цена-время FIFO — это норма, и отклонение от неё требует очень веской причины плюс прозрачного раскрытия трейдерам.
Деталь реализации, на которой все спотыкаются: разрешение меток времени имеет значение. Если точность ваших часов — миллисекунды, два ордера, пришедшие в одну миллисекунду, получат произвольный порядок. Production-движки используют микросекундные или наносекундные метки. И метка должна присваиваться в момент поступления ордера в очередь обработки движка, а не когда он достиг вашего API-шлюза.
Задержка и пропускная способность: что на самом деле значит «быстро»
Люди бросаются цифрами производительности матчинговых движков без контекста, поэтому позвольте привести реальные ориентиры.
Биржи первого уровня (Binance, Coinbase, Kraken): Эти движки обрабатывают отдельные ордера за единицы микросекунд. Публикуемые цифры обычно заявляют 100 000+ ордеров в секунду на торговую пару с задержкой сведения менее 5 микросекунд. На этом уровне вы пишете на C++ или Rust, используете lock-free структуры данных, закрепляете потоки за ядрами CPU и обходите сетевой стек ядра.
Биржи второго уровня (средние, устоявшиеся): Обработка ордеров в диапазоне 50–500 микросекунд. Пропускная способность 10 000–50 000 ордеров в секунду. Обычно написаны на Java, Go или C++. Именно здесь работает большинство легитимных бирж, и, честно говоря, этого более чем достаточно для большинства сценариев.
Биржи третьего уровня (стартапы, небольшие платформы): Обработка в диапазоне 1–10 миллисекунд. Пропускная способность 1 000–10 000 ордеров в секунду. Часто написаны на Python, PHP, Node.js или Java. Вполне адекватно для биржи с дневным объёмом менее $10 млн.
Вот что никто не хочет говорить вслух: для 95% новых бирж производительности третьего уровня совершенно достаточно. Если ваша биржа обрабатывает 1 000 сделок в день — что было бы очень здоровым запуском — вам нужна средняя пропускная способность примерно 0,01 ордера в секунду. Даже с учётом пиковых всплесков в 100 раз выше среднего вам нужно, может быть, 1 ордер в секунду. Движок на Python, работающий на одном ядре, справится с этим не напрягаясь.
Вопрос производительности не про день запуска. Он про то, что произойдёт, когда вы вырастете. Сможет ли ваш движок масштабироваться с 1 000 ордеров в секунду до 100 000 без полной переработки? Вот архитектурный вопрос, который действительно важен.
Это весомая причина, почему выбор проверенного биржевого ПО имеет значение. Криптобиржевая платформа Codono поставляется с матчинговым движком, уже протестированным под нагрузкой production-масштаба, так что вы не ставите на то, что ваша собственная реализация переживёт первый всплеск трафика. А поскольку биржевой скрипт полностью незашифрован, ваша команда может изучать и настраивать логику сведения напрямую.
Типы ордеров: сложнее, чем кажется
Лимитный и рыночный ордера — это просто. Но стоит добавить стоп-ордера, и всё становится интересным, а после добавления OCO-ордеров (one-cancels-other) сложность управления состоянием примерно удваивается.
Лимитные ордера
Основа основ. «Купить 1 BTC по $49 500 или лучше». Ордер остаётся в стакане, пока не будет исполнен, отменён или не истечёт. Реализация прямолинейна: валидация, попытка сведения с противоположной стороной, размещение неисполненного остатка.
Рыночные ордера
«Купить 1 BTC по текущей цене, какой бы она ни была». Ордер проходит по противоположной стороне стакана, съедая ценовые уровни, пока не исполнится полностью или пока не закончится ликвидность. Критическая деталь реализации: нужен механизм ценовой защиты. Рыночная покупка в тонком стакане не должна исполняться по $999 999 только потому, что какой-то шутник разместил там ордер на продажу. Большинство движков реализуют параметр максимального проскальзывания или неявную лимитную цену на основе процентного отклонения от цены последней сделки.
Стоп-лосс и стоп-лимит ордера
Вот где матчинговые движки становятся по-настоящему сложными. Стоп-ордер — это условный ордер: «Когда рыночная цена достигнет $48 000, отправить рыночный ордер на продажу». Сам стоп-ордер не находится в видимом стакане. Он хранится в отдельной структуре данных — стоп-книге — и срабатывает, когда сделка исполняется по стоп-цене или через неё.
Сложность — в каскадных стопах. Если сделка по $48 000 активирует стоп на продажу, и исполнение этого стопа опускает цену до $47 500, что активирует новые стопы, которые давят цену ещё ниже… можно получить каскад, опустошающий стакан за миллисекунды. Именно это происходило во время нескольких исторических мгновенных обвалов. Вашему движку нужны автоматические ограничители: максимальное движение цены за временное окно, проверки минимальной глубины стакана и возможность приостановить сведение, если всё идёт наперекосяк.
OCO-ордера (one-cancels-other)
OCO связывает два ордера: обычно лимитный тейк-профит и стоп-лосс. Когда исполняется любой из них, другой автоматически отменяется. Это требует поддержания графа связей между ордерами, причём отмена должна происходить атомарно с исполнением. Если стоп-лосс сработал и исполнился, а отмена тейк-профита задержалась хотя бы на несколько миллисекунд, могут исполниться оба ордера — и трейдер получит позицию, которой не хотел.
Движок Codono поддерживает эти продвинутые типы ордеров нативно, включая типы ордеров для фьючерсной торговли, такие как reduce-only и post-only, которые добавляют ещё один уровень сложности.
Крайние случаи, которые разрушат вашу биржу
Это раздел, который отличает инженеров, выпускавших матчинговые движки, от тех, кто о них читал. Вот крайние случаи, ставшие причиной реальных production-инцидентов:
Защита от самоторговли (STP)
Что происходит, когда у трейдера есть размещённый ордер на продажу по $50 000, и он отправляет ордер на покупку по $50 000? Без STP трейдер торгует сам с собой — платя комиссии с обеих сторон без всякого экономического смысла. Маркет-мейкеры делают это случайно постоянно, когда у их алгоритмов одновременно работает несколько стратегий.
Вам нужна политика STP. Три распространённых варианта:
- Отменить новый: Входящий ордер, который привёл бы к самоторговле, отменяется.
- Отменить старый: Размещённый ордер отменяется, а входящий продолжает сведение.
- Отменить оба: Отменяются оба ордера.
Большинство бирж по умолчанию используют отмену нового, но это должно настраиваться для каждого трейдера. У маркет-мейкеров на этот счёт твёрдые убеждения.
Ордера-пыль
Что делать с ордером на 0,00000001 BTC? Или с исполнением, после которого остаётся остаток 0,000000003 BTC? Такие пылевые суммы слишком малы для торговли, слишком малы для вывода, но они засоряют стакан и базу данных.
Нужны минимальные объёмы ордеров, минимальные номинальные стоимости (цена, умноженная на количество, должна превышать некий порог, обычно несколько долларов) и механизм сбора пыли, периодически вычищающий субминимальные остатки.
Контроль размера тика и лота
Цены должны быть кратны размеру тика (скажем, $0,01 для BTC/USDT). Количества должны быть кратны размеру лота (скажем, 0,00001 BTC). Если не обеспечить это на уровне матчингового движка, вы получите ордера по ценам вроде $50 000,003, которые не совпадают ни с какими другими и висят в стакане вечно. Хуже того — проблемы десятичной точности, приводящие к расхождениям балансов.
Переполнение и точность
Этот пункт укусил больше бирж, чем любой другой. Если вы используете арифметику с плавающей точкой для цен и количеств — остановитесь. Прямо сейчас. Математика с плавающей точкой порождает ошибки округления, которые на миллионах сделок накапливаются в реальные расхождения балансов. Production-движок обязан использовать либо целочисленную арифметику с фиксированной точкой (представляя $50 000,00 как целое 5000000), либо библиотеки десятичной арифметики произвольной точности. Третьего варианта не существует.
Проверки балансов после сведения
Даже при валидации баланса до сделки нужны проверочные утверждения после неё. После каждого исполнения проверяйте, что сумма всех пользовательских балансов плюс комиссии биржи равна ожидаемому итогу. Если этот инвариант нарушается — остановите сведение и разбирайтесь. Это ваша последняя линия защиты от багов, которые иначе будут медленно выкачивать средства пользователей.
Стратегии масштабирования: дилемма однопоточности
Помните, я говорил, что матчинговые движки почти всегда однопоточны в рамках торговой пары? Это создаёт очевидную проблему масштабирования. Один поток на современном железе справится, пожалуй, со 100 000–500 000 простых операций в секунду. Для большинства бирж этого хватает. Но что, если нет?
Вертикальное масштабирование
Бросьте на задачу более быстрое железо. Процессоры с более высокой тактовой частотой, больше L3-кэша, более быстрая память. Это первое, что стоит попробовать, и часто этого достаточно. Современные HFT-движки работают на специализированном железе с сетевыми картами на FPGA, полностью обходящими CPU для базовых операций сведения. Но для типичной биржи мощный сервер с процессором на 5+ ГГц и 128 ГБ оперативной памяти переварит огромный объём.
Горизонтальное масштабирование по торговым парам
Простейшая стратегия горизонтального масштабирования — запускать по одному экземпляру движка на торговую пару. BTC/USDT получает свой движок. ETH/USDT — свой. Они полностью независимы: никакого общего состояния, никакой координации. Масштабируется линейно с ростом числа торговых пар.
Сложность — в управлении балансами. Если у трейдера 10 BTC, и он размещает ордера по 5 торговым парам с BTC, каждый движок должен знать доступный баланс. Стандартный подход — централизованный сервис балансов, который каждый движок опрашивает перед приёмом ордера. Этот сервис балансов становится вашим новым узким местом, но масштабировать его гораздо проще, потому что он выполняет простые операции чтения-записи, а не сложную логику сведения.
Шардирование внутри торговой пары
Для самых высокообъёмных пар может понадобиться шардировать сам стакан. Один подход — шардировать по ценовым диапазонам: один движок обрабатывает цены от $49 000 до $50 000, другой — от $50 000 до $51 000. Но межшардовое сведение (рыночный ордер, пересекающий границы диапазонов) дьявольски сложно.
Честно? Если вы не обрабатываете объёмы уровня Binance, это вам не нужно. А если обрабатываете — у вас есть более серьёзные архитектурные проблемы, которые нужно решить первыми.
Для большинства операторов бирж разумный ход — использовать агрегацию ликвидности для углубления стакана, а не пытаться шардировать движок одной пары.
Память против диска: вопрос сохранения данных
Стакан матчингового движка живёт в памяти. По-другому никак: дисковый ввод-вывод добавляет миллисекунды задержки, совершенно неприемлемые для сведения в реальном времени. Но что происходит при падении движка? Нельзя просто потерять все размещённые ордера.
Стандартная архитектура использует три слоя:
Слой 1: Стакан в памяти. Это горячий путь. Всё сведение происходит здесь. Ноль обращений к диску при нормальной работе.
Слой 2: Журнал предзаписи (WAL). Каждый входящий ордер и каждое событие сведения добавляется в последовательный журнал на диске до того, как движок его обработает. Если движок падает, вы проигрываете журнал с последней контрольной точки, чтобы восстановить стакан. Последовательная запись быстрая: NVMe-диск выдерживает 500 000+ последовательных операций записи в секунду — более чем достаточно.
Слой 3: Периодические снимки. Каждые N секунд (или N событий) движок записывает на диск полный снимок стакана. Это ограничивает, насколько далеко назад нужно проигрывать WAL после падения. Без снимков падение после 24 часов работы могло бы потребовать воспроизведения миллионов событий.
WAL — критический компонент. Он даёт надёжность хранения без жертв задержкой. Расплата в том, что вы пишете на диск асинхронно: существует крошечное окно (микросекунды) между моментом сведения сделки в памяти и её сохранением на диск. Если в это самое окно у машины отключится питание, вы потеряете эти события. Для большинства бирж такой риск приемлем. Тем, кому нужна абсолютная гарантия нулевых потерь, добавляют синхронную репликацию на резервную машину до подтверждения сделки.
Как Codono справляется со всем этим
Построение матчингового движка с нуля означает решение каждой из описанных выше проблем. Структуры данных, логика приоритетов, крайние случаи, персистентность, масштабирование — всё целиком. А затем вечная поддержка, потому что рынки развиваются и появляются новые типы ордеров.
Именно поэтому мы построили торговый движок Codono так, как построили. Биржевое ПО Codono включает матчинговый движок production-уровня, который обеспечивает:
- Сведение FIFO с приоритетом цена-время с микросекундным разрешением меток времени
- Все стандартные типы ордеров, включая лимитные, рыночные, стоп-лосс, стоп-лимит и OCO
- Защиту от самоторговли с настраиваемыми политиками для каждого API-ключа
- Арифметику с фиксированной точкой на всём торговом конвейере — никакой плавающей точки рядом с расчётами балансов
- Журнал предзаписи с автоматическими контрольными точками и восстановлением
- Горизонтальное масштабирование с независимыми экземплярами движка для каждой торговой пары
- Встроенные ограничители для защиты от мгновенных обвалов
Движок также напрямую интегрирован с нашим движком ликвидности, так что даже если ваш органический стакан тонкий, входящие ордера могут сводиться с агрегированной внешней ликвидностью. Это особенно важно для новых бирж, где наращивание органической ликвидности занимает время.
Для операторов, которым нужен программный доступ, API-слой предоставляет низколатентные WebSocket-потоки обновлений стакана и отчётов об исполнении, чтобы маркет-мейкеры могли эффективно взаимодействовать с движком.
Итог
Матчинговый движок обманчиво прост в концепции и жестоко сложен в исполнении. Базовый алгоритм помещается в голове. Но production-задачи — персистентность, восстановление, честность, точность, крайние случаи, производительность под нагрузкой — это годы инженерной работы.
Мой честный совет: если разработка матчинговых движков — не ваша ключевая компетенция и не конкурентное преимущество, не стройте его с нуля. Используйте проверенное в боях ПО и направьте инженерные ресурсы на то, что действительно отличает вашу биржу: пользовательский опыт, листинги монет, регуляторное соответствие, маркетинг. Матчинговый движок должен быть решённой задачей, которую вы разворачиваете, а не научным проектом, съедающим ваш runway.
Успешны не те биржи, у которых самые навороченные движки. А те, кто принял разумное решение «строить или купить», быстро запустился и итерировал то, что важно пользователям. Ваш движок должен быть корректным, достаточно быстрым и надёжным. Он не обязан быть произведением искусства. Он обязан работать.
Матчинговый движок Codono обрабатывает тысячи ордеров в секунду с приоритетом цена-время, встроенными ограничителями и агрегацией ликвидности. Посмотрите страницу Spot Trading Exchange или запросите демо, чтобы протестировать его вживую.
Codono Team
Codono builds enterprise-grade crypto exchange software deployed by 250+ operators across 40+ countries. Our team writes from production experience running spot, derivatives, custody, and compliance at scale.
View all posts by Codono Team →