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







