Ми — команда Web3-інженерів з 10+ річним досвідом розробки криптоінфраструктури. Нами реалізовано понад 20 проєктів для бірж і трейдингових фірм, включаючи системи зберігання order book з навантаженням до 5000 оновлень за секунду. У криптобіржах order book — одне з найбільш навантажених джерел даних. Трейдери вимагають низьку затримку, а аналітики — повну історію. Неправильне зберігання призводить до величезних витрат на інфраструктуру.
Order book — найбільш інформативні та найскладніші в зберіганні біржові дані. Повний стакан BTC/USDT на Binance містить 5000 рівнів з обох сторін, оновлюється 5–10 разів на секунду і генерує сотні мегабайт даних на годину. При наївному підході (зберігання кожного зрізу) обсяг сягає 100 ГБ на добу тільки для одного символу. Правильна система балансує повноту даних з практичними обмеженнями. Наше рішення використовує комбінацію повних знімків і дельт (diff), що дає стиснення в 50 разів без втрати роздільної здатності.
Який формат зберігання order book обрати?
Перш ніж проєктувати сховище, важливо зрозуміти, які дані реально потрібні. У таблиці нижче — порівняння основних форматів.
| Тип даних | Розмір (на одне оновлення) | Частота запису | Використання |
|---|---|---|---|
| Full snapshot | 8–15 KB | 1 раз на хвилину | Відновлення стану, бекапи |
| Depth snapshot (20 рівнів) | 200–500 байт | 1–5 разів на секунду | Торгові стратегії, візуалізація |
| Order book diff | 150–300 байт | кожне оновлення | Секундне розділення між знімками |
| Mid-price + spread | 40 байт | кожне оновлення | Довгостроковий аналіз, моніторинг |
На практиці системи зберігають комбінацію: повні знімки для відновлення та дельти для історичної точності.
Формат зберігання: Delta Encoding
Delta encoding — критичний елемент для зменшення обсягів. Замість повного стакана ми зберігаємо лише зміни відносно попереднього стану.
Snapshot @ T=0:
bids: [(43250.0, 1.5), (43249.5, 2.0), (43249.0, 0.8)]
asks: [(43251.0, 1.2), (43251.5, 3.0), (43252.0, 0.5)]
Diff @ T=1 (лише зміни):
bids_updated: [(43250.0, 2.1)] # обсяг змінився
bids_removed: [(43249.5, 0)] # рівень зник
bids_added: [(43248.5, 1.0)] # новий рівень
asks_updated: []
asks_removed: []
asks_added: [(43251.75, 0.3)]Повний знімок: ~8 KB. Diff: ~200 байт. При 5 оновленнях на секунду та знімку раз на 60 секунд — 300 diffs + 1 snapshot = ~60 KB/хв замість 3 MB/хв. Виграш — 50 разів.
Чому ClickHouse — оптимальний вибір?
Використовуємо ClickHouse з custom serialization. Колонкове зберігання та підтримка масивів кортежів ідеально підходять для структури стакана. ZSTD-стиснення дає додаткове скорочення обсягу. Згідно з документацією ClickHouse, колонкове зберігання та ZSTD можуть стискати числові дані в 2-3 рази ефективніше за LZ4.
CREATE TABLE orderbook_snapshots (
exchange LowCardinality(String),
symbol LowCardinality(String),
snapshot_time DateTime64(3, 'UTC'),
depth UInt16,
bids Array(Tuple(Decimal(24,8), Decimal(24,8))),
asks Array(Tuple(Decimal(24,8), Decimal(24,8)))
) ENGINE = MergeTree()
PARTITION BY (exchange, toYYYYMM(snapshot_time))
ORDER BY (exchange, symbol, snapshot_time);
CREATE TABLE orderbook_diffs (
exchange LowCardinality(String),
symbol LowCardinality(String),
diff_time DateTime64(3, 'UTC'),
first_update_id UInt64,
last_update_id UInt64,
bids_changes Array(Tuple(Decimal(24,8), Decimal(24,8))),
asks_changes Array(Tuple(Decimal(24,8), Decimal(24,8)))
) ENGINE = MergeTree()
PARTITION BY (exchange, toYYYYMM(diff_time))
ORDER BY (exchange, symbol, diff_time);
CREATE TABLE orderbook_metrics (
exchange LowCardinality(String),
symbol LowCardinality(String),
ts DateTime64(3, 'UTC'),
mid_price Decimal(24,8),
spread Decimal(24,8),
spread_bps Decimal(10,4),
bid_1 Decimal(24,8),
ask_1 Decimal(24,8),
bid_vol_10 Decimal(24,8),
ask_vol_10 Decimal(24,8),
imbalance Decimal(10,6)
) ENGINE = MergeTree()
PARTITION BY (exchange, toYYYYMM(ts))
ORDER BY (exchange, symbol, ts)
SETTINGS default_codec = ZSTD(3); Відновлення стану стакана
Ключова операція — відновлення стакана на довільний момент часу. Реалізується через послідовне застосування дельт від останнього знімка.
class OrderBookReplay:
def __init__(self, storage: OrderBookStorage):
self.storage = storage
async def reconstruct_at(self, exchange: str, symbol: str, target_ts: int) -> OrderBook:
snapshot = await self.storage.get_last_snapshot_before(exchange, symbol, target_ts)
if not snapshot:
raise ValueError("No snapshot available before target timestamp")
diffs = await self.storage.get_diffs(exchange, symbol, from_ts=snapshot.timestamp, to_ts=target_ts)
book = OrderBook.from_snapshot(snapshot)
for diff in diffs:
book.apply_diff(diff)
return book
class OrderBook:
def apply_diff(self, diff: OrderBookDiff):
for price, qty in diff.bids_changes:
if qty == 0:
self.bids.pop(price, None)
else:
self.bids[price] = qty
for price, qty in diff.asks_changes:
if qty == 0:
self.asks.pop(price, None)
else:
self.asks[price] = qty
Важливий порядок застосування дельт та перевірка через update_id — у Binance кожен diff має lastUpdateId, наступний повинен починатися з lastUpdateId+1. Розрив означає пропущені дані.
Стиснення та оптимізація
Перед записом у ClickHouse застосовуємо:
- Delta encoding для цін: зберігаємо різницю від кращого bid/ask у базисних пунктах (bps). Цілі числа стискаються краще.
- Binary serialization: Protocol Buffers або MessagePack замість JSON. Виграш 3–5x за розміром та швидкістю.
- ClickHouse compression: алгоритм ZSTD(3) для даних Decimal і Float — на 20% ефективніший за дефолтний LZ4.
Потоковий запис
Ingestion pipeline працює паралельно: знімки кожні 60 секунд, дельти буферизуються та зберігаються батчами по 100 штук.
class OrderBookIngester:
SNAPSHOT_INTERVAL = 60
DIFF_BATCH_SIZE = 100
def __init__(self, storage):
self.storage = storage
self.diff_buffer = []
self.last_snapshot_time = 0
async def on_orderbook_update(self, book: OrderBook, diff: OrderBookDiff):
now = time.time()
if now - self.last_snapshot_time >= self.SNAPSHOT_INTERVAL:
await self.storage.save_snapshot(book.to_snapshot())
self.last_snapshot_time = now
self.diff_buffer.append(diff)
if len(self.diff_buffer) >= self.DIFF_BATCH_SIZE:
await self.storage.save_diffs(self.diff_buffer)
self.diff_buffer.clear()
Аналітичні запити
Після накопичення даних відкриваються можливості для аналізу. Наприклад, середній спред по годинах або кореляція imbalance з рухом ціни.
-- Середній спред BTC/USDT по годинах за вибраний місяць
SELECT toStartOfHour(ts) AS hour, avg(spread_bps) AS avg_spread_bps, avg(imbalance) AS avg_imbalance
FROM orderbook_metrics
WHERE exchange = 'binance' AND symbol = 'BTC/USDT' AND ts BETWEEN '2024-01-01' AND '2024-02-01'
GROUP BY hour
ORDER BY hour;
-- Кореляція imbalance з наступним рухом ціни
WITH book AS (
SELECT ts, imbalance, mid_price
FROM orderbook_metrics
WHERE exchange = 'binance' AND symbol = 'BTC/USDT'
),
future AS (
SELECT b.ts, b.imbalance, (f.mid_price - b.mid_price) / b.mid_price * 10000 AS fwd_return_bps
FROM book b
ASOF JOIN book f ON b.symbol = f.symbol AND f.ts BETWEEN b.ts + INTERVAL 1 MINUTE AND b.ts + INTERVAL 2 MINUTE
)
SELECT round(imbalance, 1) AS imbalance_bucket, avg(fwd_return_bps) AS avg_1min_return_bps, count() AS count
FROM future
GROUP BY imbalance_bucket
ORDER BY imbalance_bucket; Моніторинг та якість даних
Критично важливо відстежувати розриви в послідовностях дельт. Система валідації порівнює lastUpdateId кожного diff з firstUpdateId наступного та алертує при прогалинах. Розрив між знімками робить відновлення неможливим.
Метрики для моніторингу: частота запису знімків на символ, затримка від біржового timestamp до запису в ClickHouse, розмір буфера дельт, відсоток пропущених оновлень.
Чек-лист перевірки якості даних
- Перевірити послідовність update_id в дифах - Переконатися, що інтервал знімків не перевищує 60 секунд - Моніторити затримку запису (повинна бути < 1 секунди) - Регулярно відновлювати стакан тестового символу та порівнювати з останнім знімкомПроцес роботи
| Етап | Тривалість | Результат |
|---|---|---|
| Аналіз вимог | 2-3 дні | Технічне завдання, прототип схеми |
| Проектування схеми | 3-5 днів | ER-діаграма, вибір інструментів |
| Реалізація pipeline | 5-10 днів | Працюючий ingestion, тести |
| Розробка API | 3-5 днів | Документація, приклади запитів |
| Моніторинг та налагодження | 2-3 дні | Дашборди, алерти |
| Документація та навчання | 1-2 дні | README, інструкції |
Орієнтовні строки — від 2 до 4 тижнів залежно від складності. Вартість розраховується індивідуально після ознайомлення із завданням.
Що входить у роботу
- Проектування схеми зберігання під ваше навантаження (частота оновлень, кількість символів, вимоги до затримки).
- Реалізація ingestion pipeline на Python з інтеграцією через WebSocket або REST API.
- Розробка API для доступу до історичних даних (відновлення стакана, вибірка дельт, агрегати).
- Документація з відновлення та аналітичних запитів.
- Навчання команди.
- Підтримка протягом місяця після запуску.
Якщо вас цікавить оптимізація зберігання біржових даних — зверніться до нас для попередньої оцінки. Зв'яжіться з нами для оцінки вашого проекту. Замовте розробку системи зберігання ордербука під ключ — отримайте консультацію з архітектури та строків.







