Парсинг стакана ордеров Binance через WebSockets на Python asyncio

Парсинг стакана ордеров Binance через WebSockets на Python asyncio Python & API
Пошаговое руководство по парсингу и поддержанию локального стакана ордеров (L2 Order Book) Binance через WebSockets и REST API на Python с использованием asyncio и aiohttp.
Суть: Реализация асинхронного клиента на Python с использованием asyncio и aiohttp для построения и локального поддержания точной копии стакана ордеров (L2 Order Book) биржи Binance с автоматической синхронизацией через REST API и WebSocket-стрим.

Исходный код

Для создания торговых роботов и проведения качественного количественного анализа (HFT) критически важно иметь актуальный стакан ордеров с минимальной задержкой. В отличие от простого чтения веб-сокета, профессиональный подход требует синхронизации локального состояния с REST-снимком (snapshot) и последующего пошагового применения дифференциальных обновлений (diff depth stream).

Ниже представлен готовый к продакшену класс BinanceOrderBook, который реализует этот алгоритм. Он автоматически буферизует события, запрашивает снимок стакана, валидирует последовательность обновлений по U и u параметрам, а также выводит лучшие цены покупки (bids) и продажи (asks).

Этот подход к обработке потоковых данных во многом схож с тем, как мы анализируем транзакции в блокчейне. Например, вы можете изучить Мониторинг мемпула Ethereum на Python: Скрипт Whale Alerts через Web3.py или погрузиться в Декодирование транзакций Uniswap V3 Mint/Burn из мемпула на Python для понимания работы с асинхронными потоками данных в DeFi.

import asyncio
import aiohttp
import json
from typing import Dict, List, Optional

class BinanceOrderBook:
    def __init__(self, symbol: str):
        self.symbol = symbol.lower()
        self.symbol_upper = symbol.upper()
        self.bids: Dict[float, float] = {}
        self.asks: Dict[float, float] = {}
        self.last_update_id: int = 0
        self.buffer: List[dict] = []
        self.is_synced: bool = False
        self.ws_url = f"wss://stream.binance.com:9443/ws/{self.symbol}@depth"
        self.snapshot_url = f"https://api.binance.com/api/v3/depth?symbol={self.symbol_upper}&limit=1000"

    async def fetch_snapshot(self, session: aiohttp.ClientSession) -> Optional[dict]:
        try:
            async with session.get(self.snapshot_url) as response:
                if response.status == 200:
                    return await response.json()
                else:
                    print(f"Ошибка получения снимка: {response.status}")
                    return None
        except Exception as e:
            print(f"Исключение при получении снимка: {e}")
            return None

    def apply_update(self, update: dict):
        for bid in update.get("b", []):
            price, qty = float(bid[0]), float(bid[1])
            if qty == 0.0:
                self.bids.pop(price, None)
            else:
                self.bids[price] = qty

        for ask in update.get("a", []):
            price, qty = float(ask[0]), float(ask[1])
            if qty == 0.0:
                self.asks.pop(price, None)
            else:
                self.asks[price] = qty

    def process_buffer(self):
        self.buffer.sort(key=lambda x: x["u"])
        for event in self.buffer:
            u = event["u"]
            U = event["U"]
            if u <= self.last_update_id:
                continue
            if U <= self.last_update_id + 1 <= u:
                self.apply_update(event)
                self.last_update_id = u
                self.is_synced = True
            elif self.is_synced and U == self.last_update_id + 1:
                self.apply_update(event)
                self.last_update_id = u
            elif self.is_synced:
                print("Обнаружен пропуск пакетов! Требуется ресинхронизация.")
                self.is_synced = False
                self.buffer.clear()
                break

    def print_best_levels(self):
        if not self.is_synced:
            return
        sorted_bids = sorted(self.bids.items(), key=lambda x: x[0], reverse=True)
        sorted_asks = sorted(self.asks.items(), key=lambda x: x[0])
        best_bid = sorted_bids[0] if sorted_bids else (0.0, 0.0)
        best_ask = sorted_asks[0] if sorted_asks else (0.0, 0.0)
        print(f"[{self.symbol_upper}] Bid: {best_bid[0]:.2f} ({best_bid[1]:.4f}) | Ask: {best_ask[0]:.2f} ({best_ask[1]:.4f}) | Spread: {best_ask[0]-best_bid[0]:.2f}")

    async def start(self):
        async with aiohttp.ClientSession() as session:
            async with session.ws_connect(self.ws_url) as ws:
                print("WebSocket соединение установлено. Начинаем сбор буфера...")
                
                # Запускаем задачу получения снимка стакана
                snapshot_task = asyncio.create_task(self.fetch_snapshot(session))
                
                async for msg in ws:
                    if msg.type == aiohttp.WSMsgType.TEXT:
                        data = json.loads(msg.data)
                        self.buffer.append(data)
                        
                        if snapshot_task.done() and not self.is_synced:
                            snapshot = snapshot_task.result()
                            if snapshot:
                                self.last_update_id = snapshot["lastUpdateId"]
                                self.bids = {float(price): float(qty) for price, qty in snapshot["bids"]}
                                self.asks = {float(price): float(qty) for price, qty in snapshot["asks"]}
                                print(f"Снимок получен. LastUpdateId: {self.last_update_id}. Синхронизируем...")
                                self.process_buffer()
                            else:
                                # Если снимок не удался, пробуем еще раз
                                snapshot_task = asyncio.create_task(self.fetch_snapshot(session))
                        
                        elif self.is_synced:
                            self.process_buffer()
                            self.print_best_levels()
                            # Очищаем обработанную часть буфера
                            self.buffer = [e for e in self.buffer if e["u"] > self.last_update_id]

if __name__ == "__main__":
    ob = BinanceOrderBook("BTCUSDT")
    try:
        asyncio.run(ob.start())
    except KeyboardInterrupt:
        print("Работа завершена.")

Разбор параметров

Для корректной работы со стаканом ордеров Binance важно понимать структуру получаемых данных и логику их синхронизации:

  • symbol: Торговая пара (например, BTCUSDT). В WebSocket-урле используется нижний регистр, в REST API — верхний.
  • lastUpdateId: Уникальный идентификатор последнего обновления в полученном снимке стакана. Служит отправной точкой для синхронизации.
  • U (First update ID): Идентификатор первого изменения в пакете обновления от WebSocket.
  • u (Final update ID): Идентификатор последнего изменения в пакете обновления. Новое событие должно иметь U == previous_u + 1.
  • bids и asks: Словари, хранящие цену в качестве ключа и объем в качестве значения. Если пришедший объем равен 0.0, уровень удаляется из стакана.

Как запустить

Для запуска скрипта вам понадобится Python версии 3.8 или выше и библиотека aiohttp.

1. Установите необходимую зависимость с помощью pip:

pip install aiohttp

2. Сохраните приведенный выше код в файл binance_order_book.py и запустите его:

python binance_order_book.py

После запуска скрипт подключится к WebSocket-серверу Binance, скачает актуальный снимок стакана через REST API, синхронизирует данные и начнет выводить в консоль лучшие цены покупки, продажи и текущий спред в реальном времени.

Полученные данные стакана можно использовать для построения маркет-мейкинг алгоритмов. О том, как отправлять лимитные ордера с минимальной комиссией, читайте в нашей статье Пинг-понг стратегия на Bybit V5: Post-Only ордера на Python.

Оцените статью
FinFluct