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







