Розробка системи зберігання order book snapshots
Дані order book — найбільш інформативні та найскладніші для зберігання біржові дані. Повний стакан BTC/USDT на Binance містить 5000 рівнів з обох сторін, оновлюється кілька разів в секунду й генерує сотні мегабайтів даних на годину. Наївний підхід до зберігання приводить до катастрофічного зростання обсягів; правильна система балансує повноту даних з практичними обмеженнями.
Типи даних order book
Перед проектуванням сховища важливо розуміти, які дані насправді потрібні:
Full snapshots — повний зріз стакана в момент часу. Великі (кілька КБ на снапшот), але дозволяють точно відновити стан ринку. При 1 снапшот/сек на 100 символах — ~100 GB на день.
Depth snapshots — перші N рівнів (зазвичай 5, 10, 20). Достатньо для більшості стратегій, вимагають 50–250 разів менше місця.
Order book diffs — лише зміни (додання/видалення/оновлення рівнів). Мінімальний обсяг, але вимагають повного снапшоту для відновлення стану.
Mid-price та spread — агреговані похідні. Мікроскопічний обсяг, підходить для довгострокового аналізу.
На практиці системи зберігають комбінації: повні снапшоти раз в хвилину для відновлення, diffs для секундної роздільної здатності між снапшотами.
Формат зберігання: 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)]
Повний снапшот: ~10 KB. Diff: ~200 байт. При 5 оновленнях/сек та снапшоті раз в хвилину — 300 diffs + 1 snapshot = ~60 KB/хв замість 3 MB/хв.
Схема бази даних
Використовуємо ClickHouse з власною сериалізацією:
-- Повні снапшоти (раз в хвилину)
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))), -- [(price, qty)]
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))), -- qty=0 означає видалення
asks_changes Array(Tuple(Decimal(24,8), Decimal(24,8)))
)
ENGINE = MergeTree()
PARTITION BY (exchange, toYYYYMM(diff_time))
ORDER BY (exchange, symbol, diff_time);
Відновлення стану стакана
Ключова операція — відновлення стакана на довільний момент часу:
class OrderBookReplay:
async def reconstruct_at(self, exchange: str, symbol: str, target_ts: int) -> OrderBook:
# 1. Знайти останній снапшот до target_ts
snapshot = await self.storage.get_last_snapshot_before(
exchange, symbol, target_ts
)
# 2. Завантажити всі дельти від снапшоту до target_ts
diffs = await self.storage.get_diffs(
exchange, symbol,
from_ts=snapshot.timestamp,
to_ts=target_ts
)
# 3. Застосувати дельти послідовно
book = OrderBook.from_snapshot(snapshot)
for diff in diffs:
book.apply_diff(diff)
return book
Критично: підтримувати порядок послідовності diffs та валідувати через update_id — кожен diff має lastUpdateId, наступний diff повинен мати firstUpdateId = lastUpdateId + 1. Пропуски в послідовності вказують на відсутність даних.
Компресія та оптимізація
Order book містить багато подібних чисел (ціни групуються разом). Перед записом у ClickHouse застосовуйте:
Delta encoding для цін — зберігайте різниці цін від найкращого bid/ask в basis points замість абсолютних цін.
Binary serialization — використовуйте Protocol Buffers або MessagePack замість JSON для скорочення розміру в 3–5 разів.
ClickHouse compression — кодек ZSTD забезпечує краще стискання для типів Float/Decimal ніж стандартний LZ4.
Streaming ingestion
Pipeline ingestion для снапшотів та diffs працює паралельно, буферизуючи diffs та періодично зберігаючи снапшоти та партії змін.
Моніторинг
Критично: відслідкувати пропуски в послідовностях diffs. Валідація системи порівнює lastUpdateId кожного diff з firstUpdateId наступного та алертує про пропуски. Пропуски в даних роблять відновлення order book між снапшотами неможливим.







