Ми — команда 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 для доступу до історичних даних (відновлення стакана, вибірка дельт, агрегати).
- Документація з відновлення та аналітичних запитів.
- Навчання команди.
- Підтримка протягом місяця після запуску.
Якщо вас цікавить оптимізація зберігання біржових даних — зверніться до нас для попередньої оцінки. Зв'яжіться з нами для оцінки вашого проекту. Замовте розробку системи зберігання ордербука під ключ — отримайте консультацію з архітектури та строків.







