Разработка системы хранения order book snapshots

Мы — команда Web3-инженеров с 10+ летним опытом разработки криптоинфраструктуры. Нами реализовано более 20 проектов для бирж и трейдинговых фирм, включая системы хранения order book с нагрузкой до 5000 обновлений в секунду. В криптобиржах order book — один из самых нагруженных источников данных. Тре

Направления блокчейн-разработки

Часто задаваемые вопросы

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1452
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1310
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1005
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1270
  • image_logo-advance_0.webp
    Разработка логотипа компании B2B Advance
    719
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    1012

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

Если вас интересует оптимизация хранения биржевых данных — обратитесь к нам за предварительной оценкой. Свяжитесь с нами для оценки вашего проекта. Закажите разработку системы хранения ордербука под ключ — получите консультацию по архитектуре и срокам.