Разработка системы ордеров (limit, market, stop)

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Разработка системы ордеров (limit, market, stop)
Сложная
~1-2 недели
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1221
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1163
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    855
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1056
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    561
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    828

Разработка системы ордеров (limit, market, stop)

Система ордеров — сердце любой биржи. Именно здесь сосредоточена вся торговая логика, и именно здесь цена ошибки максимальна: баг в matching engine может за секунды обнулить ликвидность или создать несправедливые исполнения. Разберём архитектуру от модели данных до matching algorithm.

Типы ордеров и их семантика

Limit order

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

  • Buy limit: исполняется по цене ≤ указанной
  • Sell limit: исполняется по цене ≥ указанной
  • Может быть частично исполнен (partial fill)
  • Неисполненная часть остаётся в order book

Дополнительные модификаторы: GTC (Good Till Cancelled — живёт до отмены), GTD (Good Till Date — до определённого времени), IOC (Immediate Or Cancel — исполнить что можно сейчас, остаток отменить), FOK (Fill Or Kill — исполнить целиком или отменить полностью), Post-Only (выставить только как maker, если немедленно исполнится как taker — отменить).

Market order

Исполняется немедленно по лучшей доступной цене. Гарантирует исполнение, но не гарантирует цену. На неликвидных рынках market order может дать значительное slippage.

Безопасная реализация: лимит slippage. Если исполнение требует прохождения через стакан более чем на X%, ордер отклоняется с ошибкой PRICE_IMPACT_TOO_HIGH. Без этого злоумышленник может опустошить ликвидность через один market order.

Stop order

Триггерный ордер. Активируется когда рыночная цена достигает stop price. После активации превращается в market или limit ордер.

  • Stop-Market: при достижении stop price создаётся market ордер
  • Stop-Limit: при достижении stop price создаётся limit ордер с указанным limit price
  • Trailing Stop: stop price следует за рынком на заданное расстояние (абсолютное или в %)

Stop ордера не находятся в order book — они хранятся отдельно в stop orders storage и мониторятся по изменению цены.

Архитектура matching engine

Структура данных order book

Классическая реализация — два sorted map (bid side и ask side) с ценой как ключом. В каждом price level — очередь ордеров (FIFO для price-time priority).

type PriceLevel struct {
    Price   decimal.Decimal
    Orders  []*Order // FIFO queue
    Total   decimal.Decimal // cached volume
}

type OrderBook struct {
    Bids    *btree.BTree // descending (max bid first)
    Asks    *btree.BTree // ascending (min ask first)
    mu      sync.RWMutex
}

Выбор структуры данных критичен для производительности:

  • Red-Black Tree (Go btree, Java TreeMap): O(log n) insert/delete, хороший cache locality
  • Skip List: конкурентный доступ без глобального лока, используется в некоторых HFT системах
  • Array + binary search: быстрее для чтения на маленьких книгах, медленнее для обновлений

Для биржи с < 10,000 active orders в book — btree более чем достаточно. При > 100,000 active orders и требованиях к latency < 100 мкс нужна более сложная архитектура.

Алгоритм matching

Price-time priority (FIFO matching) — стандарт для большинства бирж:

func (ob *OrderBook) Match(incoming *Order) ([]Trade, *Order) {
    ob.mu.Lock()
    defer ob.mu.Unlock()
    
    var trades []Trade
    remaining := incoming.Quantity
    
    for remaining > 0 {
        // Получаем лучший противоположный уровень
        bestLevel := ob.getBestOppositeLevel(incoming.Side)
        if bestLevel == nil {
            break
        }
        
        // Проверяем price match
        if !ob.priceMatches(incoming, bestLevel) {
            break
        }
        
        // Matching внутри price level (FIFO)
        for len(bestLevel.Orders) > 0 && remaining > 0 {
            maker := bestLevel.Orders[0]
            fillQty := min(remaining, maker.RemainingQty)
            
            trade := Trade{
                TakerOrderID: incoming.ID,
                MakerOrderID: maker.ID,
                Price:        bestLevel.Price,
                Quantity:     fillQty,
                Timestamp:    time.Now().UnixNano(),
            }
            trades = append(trades, trade)
            
            remaining -= fillQty
            maker.RemainingQty -= fillQty
            
            if maker.RemainingQty == 0 {
                bestLevel.Orders = bestLevel.Orders[1:]
            }
        }
        
        // Удаляем пустой price level
        if len(bestLevel.Orders) == 0 {
            ob.removeLevel(incoming.Side.Opposite(), bestLevel.Price)
        }
    }
    
    incoming.RemainingQty = remaining
    return trades, incoming
}

Типы matching алгоритмов

Алгоритм Применение Особенности
FIFO (Price-Time) Большинство CEX Простой, справедливый
Pro-Rata Фьючерсы (CME) Крупные ордера получают приоритет
FIFO + Pro-Rata ICE, Euronext Гибридный
Uniform Price (Batch) DEX, аукционы Все сделки по одной цене

Для стандартной CEX — FIFO. Pro-Rata усложняет реализацию и провоцирует спам мелкими ордерами для получения приоритета.

Модель данных

CREATE TABLE orders (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id         BIGINT NOT NULL REFERENCES users(id),
    pair_id         SMALLINT NOT NULL,
    side            SMALLINT NOT NULL, -- 0=buy, 1=sell
    type            SMALLINT NOT NULL, -- 0=limit, 1=market, 2=stop_limit, 3=stop_market
    status          SMALLINT NOT NULL DEFAULT 0, -- 0=open, 1=partial, 2=filled, 3=cancelled
    price           NUMERIC(36,18),   -- NULL для market ордеров
    stop_price      NUMERIC(36,18),   -- NULL если не stop
    quantity        NUMERIC(36,18) NOT NULL,
    filled_qty      NUMERIC(36,18) NOT NULL DEFAULT 0,
    time_in_force   SMALLINT NOT NULL DEFAULT 0, -- GTC=0, IOC=1, FOK=2, GTD=3
    expire_at       TIMESTAMPTZ,
    client_order_id VARCHAR(64),      -- клиентский идентификатор
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE trades (
    id              BIGSERIAL PRIMARY KEY,
    pair_id         SMALLINT NOT NULL,
    taker_order_id  UUID NOT NULL,
    maker_order_id  UUID NOT NULL,
    taker_user_id   BIGINT NOT NULL,
    maker_user_id   BIGINT NOT NULL,
    price           NUMERIC(36,18) NOT NULL,
    quantity        NUMERIC(36,18) NOT NULL,
    taker_fee       NUMERIC(36,18) NOT NULL,
    maker_fee       NUMERIC(36,18) NOT NULL,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_orders_user_status ON orders(user_id, status) WHERE status IN (0, 1);
CREATE INDEX idx_orders_pair_side_price ON orders(pair_id, side, price) WHERE status IN (0, 1);

Критичный момент: matching engine работает в памяти, БД используется только для персистентности. При старте сервер загружает все open ордера в память. Запись в БД — асинхронная, через очередь. Это даёт latency в единицы миллисекунд вместо десятков.

Stop orders и триггерный механизм

Stop ордера хранятся в отдельной структуре — sorted по stop price. При каждой сделке matching engine публикует последнюю цену. Stop orders processor подписывается на price updates:

func (sp *StopProcessor) OnPriceUpdate(pair string, lastPrice decimal.Decimal) {
    // Проверяем buy stops (триггер: last price >= stop price)
    triggeredBuys := sp.buyStops.GetTriggered(pair, lastPrice)
    // Проверяем sell stops (триггер: last price <= stop price)
    triggeredSells := sp.sellStops.GetTriggered(pair, lastPrice)
    
    for _, stop := range append(triggeredBuys, triggeredSells...) {
        sp.activateStop(stop, lastPrice)
    }
}

Trailing stop — особый случай. При движении цены в пользу пользователя stop price пересчитывается. Реализация через periodic update или через event-driven пересчёт при каждом trade.

Балансировый контроль и atomicity

Перед размещением ордера нужно зарезервировать средства:

  • Buy limit: резервируем price * quantity в quote currency
  • Sell limit: резервируем quantity в base currency
  • Market buy: резервируем maxSpend (либо явный параметр, либо расчётный с запасом)

При отмене — освобождение резерва. При fill — перемещение резерва в реальный баланс контрагента.

Atomicity обеспечивается через транзакции БД, но matching engine не может ходить в БД на каждый match — это узкое место. Решение: в-памяти баланс с асинхронной синхронизацией в БД. В-памяти баланс — source of truth для торговли, БД — для персистентности и отображения в UI.

Decimal precision и floating point

Никогда не использовать float64 для финансовых расчётов. 0.1 + 0.2 != 0.3 в IEEE 754. Для финансов используем:

  • Go: shopspring/decimal
  • Python: decimal.Decimal
  • Java: BigDecimal
  • JavaScript: decimal.js или bignumber.js

Все хранимые значения в БД — NUMERIC(36,18) или строки. Precision и scale определяются для каждой торговой пары отдельно (Bitcoin: 8 знаков после запятой, мем-коины: может быть 18).

Performance и масштабирование

Один matching engine на Go обрабатывает 50,000–100,000 ордеров/сек на современном железе при latency < 1 мс. Для большинства CEX этого достаточно.

При необходимости масштабирования — шардинг по trading pair. BTC/USDT — отдельный инстанс, ETH/USDT — отдельный. Пары не взаимодействуют между собой на уровне matching.

Компонент Технология Latency
In-memory order book Go btree < 100 мкс
Stop orders processor Go goroutine < 1 мс
Balance check In-memory map < 10 мкс
DB persistence PostgreSQL async 5–20 мс
WebSocket broadcast Go channels < 1 мс

Тестирование

Matching engine покрывается unit-тестами на граничные случаи:

  • Partial fill с остатком
  • FOK при недостаточной ликвидности
  • IOC с частичным исполнением
  • Одновременная отмена и fill (race condition)
  • Stop ордер триггерится в момент своего размещения
  • Decimal overflow на крайних значениях

Property-based testing (fuzzing) — генерируются случайные последовательности ордеров, проверяется инвариант: суммарный объём купленного = суммарному объёму проданного, балансы сходятся.

Сроки разработки

  • MVP (limit + market, без stop, без time-in-force): 3–4 недели
  • Полная система с stop orders, всеми TIF модификаторами, trailing stop: 8–12 недель
  • Production-ready с аудитом, нагрузочными тестами, мониторингом: +4–6 недель

Стоимость зависит от требований к throughput и наличия готовых смежных компонентов (баланс, комиссии, WebSocket).