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

Розробка системи ордерів (limit, market, stop) Запуск криптобіржі: баг в matching engine може за секунди обнулити ліквідність. Помилка в обробці ринкового ордера здатна викликати slippage на 20% і відтік користувачів. Ми розробили десятки торгових систем за 5+ років і знаємо, як уникнути цих ризи

Напрямки блокчейн-розробки

Часті запитання

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1441
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1301
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    998
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1267
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    713
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    1003

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

Запуск криптобіржі: баг в matching engine може за секунди обнулити ліквідність. Помилка в обробці ринкового ордера здатна викликати slippage на 20% і відтік користувачів. Ми розробили десятки торгових систем за 5+ років і знаємо, як уникнути цих ризиків. Наш matching engine на Go досягає latency <1 мс на рівні order book, що в 10 разів швидше типових реалізацій на Node.js. Ми використовуємо btree для зберігання price levels та бедлок-фрі структури для конкурентного доступу. В результаті система витримує до 100 000 ордерів за секунду на одному інстансі. Середня економія на інфраструктурі порівняно з рішеннями на Node.js досягає 40%, а термін окупності становить 6–9 місяців. Нижче — деталі реалізації та ключові архітектурні рішення.

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

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.

Market order

Виконується негайно за найкращою доступною ціною. Гарантує виконання, але не гарантує ціну. На неліквідних ринках можливий значний slippage. Безпечна реалізація включає ліміт slippage — якщо виконання вимагає проходження більше ніж на X%, ордер відхиляється з помилкою PRICE_IMPACT_TOO_HIGH.

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) — O(log n) insert/delete, Skip List — конкурентний доступ, Array + binary search — швидко на малих книгах. Для <10,000 active orders btree достатньо; при >100,000 та latency <100 мкс потрібна складніша архітектура. Згідно з CME Globex Matching Algorithm, гібридні схеми забезпечують найкращий баланс.

Структура даних O-нотація Конкурентність Застосовність
B-tree (Go btree) O(log n) RWLock <100k orders
Skip List O(log n) avg Lock-free >100k orders, high concurrency
Array + Binary Search O(log n) search, O(n) insert Lock per operation Small order books, <1k

Алгоритм matching

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

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 } if !ob.priceMatches(incoming, bestLevel) { break } 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:] } } if len(bestLevel.Orders) == 0 { ob.removeLevel(incoming.Side.Opposite(), bestLevel.Price) } } incoming.RemainingQty = remaining return trades, incoming } 
Алгоритм Застосування Особливості
FIFO (Price-Time) Більшість CEX Простий, справедливий
Pro-Rata Ф'ючерси (CME) Великі ордери отримують пріоритет
FIFO + Pro-Rata ICE, Euronext Гібридний
Uniform Price (Batch) DEX, аукціони Всі угоди за однією ціною

Для стандартної CEX обираємо FIFO. Pro-Rata ускладнює реалізацію та провокує спам дрібними ордерами.

Чому ми використовуємо in-memory matching engine?

Matching engine працює в пам'яті — це дає latency в одиниці мілісекунд замість десятків. База даних (PostgreSQL) використовується лише для персистентності: при старті сервер завантажує всі open ордери в пам'ять. Запис в БД — асинхронний, через чергу. Такий підхід витримує 50,000–100,000 ордерів/сек на одному інстансі. Для масштабування використовуємо шардинг за торговими парами. Розробка власного рішення обходиться в 3–5 разів дешевше щорічної ліцензії готового пропрієтарного двигуна.

Як захиститися від race conditions при відміні та fill?

Перед розміщенням ордера резервуємо кошти: buy limit — price * quantity в quote, sell limit — quantity в base. При відміні звільняємо резерв. Atomicity забезпечується через in-memory баланс з асинхронною синхронізацією в БД. В-пам'яті баланс — source of truth для торгівлі, БД — для персистентності та UI. Всі операції з балансом виконуються під м'ютексом, що запобігає race conditions.

Модель даних

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, type SMALLINT NOT NULL, status SMALLINT NOT NULL DEFAULT 0, price NUMERIC(36,18), stop_price NUMERIC(36,18), quantity NUMERIC(36,18) NOT NULL, filled_qty NUMERIC(36,18) NOT NULL DEFAULT 0, time_in_force SMALLINT NOT NULL DEFAULT 0, 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 працює в пам'яті, БД — лише для персистентності. Запис в БД асинхронний, через чергу.

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

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

func (sp *StopProcessor) OnPriceUpdate(pair string, lastPrice decimal.Decimal) { triggeredBuys := sp.buyStops.GetTriggered(pair, lastPrice) triggeredSells := sp.sellStops.GetTriggered(pair, lastPrice) for _, stop := range append(triggeredBuys, triggeredSells...) { sp.activateStop(stop, lastPrice) } } 

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

Деталі реалізації trailing stop

Trailing stop — динамічний стоп-ордер, чия тригерна ціна слідує за ринком з фіксованим відступом. Алгоритм: при кожному оновленні ціни, якщо ціна рухається в бік клієнта, стоп-ціна перераховується: new_stop_price = current_market_price - distance для sell trailing stop. При зворотному русі (навпроти клієнта) стоп-ціна не змінюється, що дозволяє зафіксувати прибуток. В реалізації ми використовуємо priority queue по stop_price, яка оновлюється при кожному trade.

Decimal precision та floating point

Ніколи не використовуйте float64 для фінансових розрахунків. 0.1 + 0.2 != 0.3 в IEEE 754. Використовуємо: Go — shopspring/decimal, Python — decimal.Decimal, Java — BigDecimal, JavaScript — decimal.js. Всі збережені значення — NUMERIC(36,18). Precision та scale задаються для кожної торгової пари окремо (Bitcoin: 8 знаків, мем-коїни: до 18).

Етапи реалізації

  1. Проектування — аналіз вимог, вибір алгоритмів, моделювання потоків з навантаженням до 100 000 ордерів/сек.
  2. Розробка — написання matching engine з нуля або на основі референсної архітектури (Go, btree, decimal).
  3. Інтеграція — зв'язок з PostgreSQL, налаштування асинхронного запису та балансового модуля.
  4. Тестування — unit-тести (покриття >85%), property-based testing (fuzzing), навантажувальні тести з вимірюванням latency/throughput.
  5. Деплой — розгортання на інфраструктурі, моніторинг, навчання команди.

Що входить в роботу

При замовленні ви отримуєте:

  • Архітектурну документацію matching engine та API специфікацію
  • Репозиторій з вихідним кодом (Go, production-ready)
  • Набір unit-тестів та інтеграційних тестів (покриття >85%)
  • Результати навантажувального тестування з метриками latency/throughput
  • Керівництво з розгортання та експлуатації
  • 2 місяці постпродакшн підтримки та навчання вашої команди

Тестування

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 тижнів

Бюджет проєкту розраховується індивідуально під ваші вимоги. Отримайте консультацію по вашому проєкту — зв'яжіться з нами для попередньої оцінки. Також ви можете замовити аудит поточної архітектури — ми надамо звіт з рекомендаціями.