Граббинг данных order book с бирж в реальном времени

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1Все 1306 услуг
Граббинг данных order book с бирж в реальном времени
Средний
~2-3 дня
Часто задаваемые вопросы

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

Этапы блокчейн-разработки

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

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

Парсинг данных order book с бирж в реальном времени

Order book данные нужны в трёх сценариях: построение торгового бота, создание агрегатора ликвидности, или мониторинг рынка. В каждом сценарии есть своя специфика, но общая проблема одна — стабильное получение high-frequency данных без потерь и с минимальной латентностью.

WebSocket vs REST polling

REST polling (GET /api/v3/depth?symbol=BTCUSDT) — неправильный выбор для real-time order book. На активных рынках стакан обновляется 10–100 раз в секунду. Поллинг раз в секунду даст устаревшие данные и нагрузит API лимиты. Правильный подход — WebSocket потоки с инкрементальными обновлениями.

Большинство крупных CEX (Binance, Bybit, OKX) следуют одной схеме:

  1. Получить snapshot через REST (полный стакан на текущий момент)
  2. Подписаться на WebSocket поток обновлений
  3. Применять обновления к snapshot, поддерживая локальную копию стакана
import asyncio, json, aiohttp
from sortedcontainers import SortedDict

class OrderBook:
    def __init__(self):
        self.bids = SortedDict(lambda x: -x)  # убывающий порядок
        self.asks = SortedDict()
        self.last_update_id = 0

    def apply_update(self, bids: list, asks: list, update_id: int):
        if update_id <= self.last_update_id:
            return  # устаревшее обновление, игнорируем

        for price, qty in bids:
            price, qty = float(price), float(qty)
            if qty == 0:
                self.bids.pop(price, None)  # удалить уровень
            else:
                self.bids[price] = qty

        for price, qty in asks:
            price, qty = float(price), float(qty)
            if qty == 0:
                self.asks.pop(price, None)
            else:
                self.asks[price] = qty

        self.last_update_id = update_id

    @property
    def best_bid(self) -> tuple[float, float] | None:
        if self.bids:
            price = self.bids.keys()[0]
            return price, self.bids[price]
        return None

    @property
    def best_ask(self) -> tuple[float, float] | None:
        if self.asks:
            price = self.asks.keys()[0]
            return price, self.asks[price]
        return None

Binance depth stream: детали протокола

Binance — самый частый запрос. У них два stream варианта:

  • btcusdt@depth — обновления каждые 100ms или 1000ms (параметр @depth@100ms)
  • btcusdt@depth20 — топ-20 уровней каждые 100ms (без инкрементальных обновлений, всегда полный)

Для полного стакана с применением патчей:

async def maintain_binance_orderbook(symbol: str):
    ob = OrderBook()
    buffer = []  # буфер обновлений до получения snapshot

    async def handle_ws_message(msg):
        data = json.loads(msg)
        # Накапливаем обновления ПОКА не получим snapshot
        if ob.last_update_id == 0:
            buffer.append(data)
            return

        # Binance: обновление валидно если U <= lastUpdateId+1 <= u
        if data['U'] <= ob.last_update_id + 1 <= data['u']:
            ob.apply_update(data['b'], data['a'], data['u'])

    # Запускаем WS
    ws_task = asyncio.create_task(connect_ws(
        f"wss://stream.binance.com:9443/ws/{symbol.lower()}@depth@100ms",
        handle_ws_message
    ))

    # Получаем snapshot (немного ждём чтобы буфер накопился)
    await asyncio.sleep(0.5)
    async with aiohttp.ClientSession() as session:
        async with session.get(
            f"https://api.binance.com/api/v3/depth",
            params={"symbol": symbol.upper(), "limit": 1000}
        ) as resp:
            snapshot = await resp.json()

    # Инициализируем стакан из snapshot
    for price, qty in snapshot['bids']:
        ob.bids[float(price)] = float(qty)
    for price, qty in snapshot['asks']:
        ob.asks[float(price)] = float(qty)
    ob.last_update_id = snapshot['lastUpdateId']

    # Применяем буферизованные обновления
    for update in buffer:
        if update['u'] > ob.last_update_id:
            ob.apply_update(update['b'], update['a'], update['u'])

    await ws_task

Критический момент: если пропущен update (gap в Uu последовательности) — стакан рассинхронизирован. Нужна логика ресинхронизации: детектировать gap и переинициализировать с нового snapshot.

Агрегация нескольких бирж

Для агрегатора ликвидности или кросс-биржевого арбитража — поддерживать стаканы нескольких бирж параллельно:

EXCHANGES = {
    "binance": BinanceOrderBook,
    "bybit": BybitOrderBook,
    "okx": OKXOrderBook,
}

async def run_aggregator(symbol: str):
    books = {name: cls(symbol) for name, cls in EXCHANGES.items()}
    tasks = [book.run() for book in books.values()]
    await asyncio.gather(*tasks)

def get_best_price_across_exchanges(books: dict[str, OrderBook]) -> dict:
    best_bids = [(name, *ob.best_bid) for name, ob in books.items() if ob.best_bid]
    best_asks = [(name, *ob.best_ask) for name, ob in books.items() if ob.best_ask]

    best_bids.sort(key=lambda x: x[1], reverse=True)
    best_asks.sort(key=lambda x: x[1])

    return {
        "best_bid": {"exchange": best_bids[0][0], "price": best_bids[0][1], "qty": best_bids[0][2]},
        "best_ask": {"exchange": best_asks[0][0], "price": best_asks[0][1], "qty": best_asks[0][2]},
        "spread": best_asks[0][1] - best_bids[0][1]
    }

Хранение и воспроизведение

Для backtesting или аудита — хранение потока обновлений, а не только снапшотов. L2 order book updates — это большой объём: для BTC/USDT на Binance ~100MB/час несжатых данных.

Эффективное хранение:

# Запись в бинарный формат через msgpack
import msgpack, lz4.frame

def serialize_update(update: dict) -> bytes:
    packed = msgpack.packb(update, use_bin_type=True)
    return lz4.frame.compress(packed)

# TimescaleDB для time-series хранения
# Гипертаблица автоматически партиционирует по времени
CREATE TABLE ob_updates (
    time        TIMESTAMPTZ NOT NULL,
    exchange    TEXT NOT NULL,
    symbol      TEXT NOT NULL,
    side        CHAR(1) NOT NULL,  -- 'b' или 'a'
    price       NUMERIC NOT NULL,
    quantity    NUMERIC NOT NULL
);
SELECT create_hypertable('ob_updates', 'time');

Rate limits и управление соединениями

Binance: максимум 5 WebSocket соединений на stream при публичном доступе, 1024 при использовании API ключа. При отключении — exponential backoff:

async def connect_ws_with_retry(url: str, handler, max_retries=10):
    for attempt in range(max_retries):
        try:
            async with websockets.connect(url, ping_interval=20) as ws:
                async for message in ws:
                    await handler(message)
        except (websockets.exceptions.ConnectionClosed, Exception) as e:
            wait = min(2 ** attempt, 60)  # max 60 секунд
            logging.warning(f"WS disconnected: {e}, retry in {wait}s")
            await asyncio.sleep(wait)

Полная система агрегации 3–5 бирж с хранением в TimescaleDB и REST API для запросов: 3–4 недели.