Real-Time Order Book Scraping System

Real-Time Order Book Data Scraping Imagine your trading bot executes a trade at a price that has already changed – the order book is out of sync due to a WebSocket disconnect. Losses from a single such event can reach 2-3% of capital. We encountered this in early projects and developed a system t

Blockchain Development Services

Frequently Asked Questions

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1451
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1309
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    1005
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1270
  • image_logo-advance_0.webp
    B2B Advance company logo design
    719
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    1011

Real-Time Order Book Data Scraping

Imagine your trading bot executes a trade at a price that has already changed – the order book is out of sync due to a WebSocket disconnect. Losses from a single such event can reach 2-3% of capital. We encountered this in early projects and developed a system that eliminates such incidents.

Order book data is critical for three scenarios: building a trading bot, creating a liquidity aggregator, and market monitoring. In each, the common challenge is stable high-frequency data acquisition without loss and with minimal latency. Wrong protocol choice or lack of reconnect handling leads to order book desync and unprofitable trades. Over the years, we have implemented over 30 integrations with various exchanges. We guarantee stable data collection and low latency.

Why WebSocket over REST?

REST polling (GET /api/v3/depth?symbol=BTCUSDT) is the wrong choice for real-time order book. On active markets, the order book updates 10–100 times per second. Polling once per second gives stale data and strains API rate limits. The right approach is WebSocket streams with incremental updates.

Parameter REST Polling WebSocket Stream
Latency 1 sec+ (poll interval) 10-100 ms (event-driven)
API Load High (requests every second) Low (single connection)
Data Freshness Instantly outdated Always latest state
Scaling Issues with multiple instruments Up to 1024 streams per key

Most major CEXes (Binance, Bybit, OKX) follow the same scheme:

  1. Obtain a snapshot via REST (full order book at current moment)
  2. Subscribe to a WebSocket update stream
  3. Apply updates to the snapshot, maintaining a local copy of the order book
import asyncio, json, aiohttp from sortedcontainers import SortedDict class OrderBook: def __init__(self): self.bids = SortedDict(lambda x: -x) # descending order 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 # stale update, ignore for price, qty in bids: price, qty = float(price), float(qty) if qty == 0: self.bids.pop(price, None) # remove level 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 

How to Resynchronize the Order Book After a Disconnect?

When a connection drops or packets are lost, the risk of inconsistency is high. We use the following techniques:

  • Buffer updates until a snapshot is received (as shown above)
  • Check the sequence ID of each update: if update_id does not match the expected one, discard the packet and request a new snapshot
  • Exponential backoff on reconnection with a cap of 60 seconds
  • Monitor latency and alert when threshold is exceeded (e.g., >500 ms)
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 seconds logging.warning(f"WS disconnected: {e}, retry in {wait}s") await asyncio.sleep(wait) 
Details of Working with Binance Depth Stream

Binance is the most frequent request. They have two stream variants:

  • btcusdt@depth — updates every 100ms or 1000ms (parameter @depth@100ms)
  • btcusdt@depth20 — top-20 levels every 100ms (no incremental updates, always full)

For the full order book with patching:

async def maintain_binance_orderbook(symbol: str): ob = OrderBook() buffer = [] # buffer updates until snapshot received async def handle_ws_message(msg): data = json.loads(msg) # Accumulate updates UNTIL we get snapshot if ob.last_update_id == 0: buffer.append(data) return # Binance: update valid if U <= lastUpdateId+1 <= u if data['U'] <= ob.last_update_id + 1 <= data['u']: ob.apply_update(data['b'], data['a'], data['u']) # Start WS ws_task = asyncio.create_task(connect_ws( f"wss://stream.binance.com:9443/ws/{symbol.lower()}@depth@100ms", handle_ws_message )) # Get snapshot (wait a bit for buffer to accumulate) 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() # Initialize order book from 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'] # Apply buffered updates for update in buffer: if update['u'] > ob.last_update_id: ob.apply_update(update['b'], update['a'], update['u']) await ws_task 

Critical point: if an update is missed (gap in Uu sequence) the order book becomes desynchronized. Resync logic is needed: detect the gap and reinitialize from a new snapshot.

Case Study: Aggregating Binance and Bybit for Arbitrage

For cross-exchange arbitrage, it is necessary to maintain order books of multiple exchanges in parallel. Here is an example of an aggregator that finds the best price:

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] } 

Real-time aggregation allows seeing the best bid and ask across all exchanges. This is the foundation for arbitrage strategies and building a unified order book.

Data Storage: TimescaleDB vs File-Based

For backtesting or auditing, it is better to store the stream of updates rather than only snapshots. L2 order book updates generate a large volume: for BTC/USDT on Binance ~100MB/hour of uncompressed data.

Criterion TimescaleDB File-Based (Parquet)
Real-time queries Yes (SQL) No (analytics only)
Compression Automatic Configurable (lz4)
Stream replay Requires additional processing Direct read
Kafka integration Yes Not available

We recommend using TimescaleDB for long-term storage and Parquet for analytics. If needed, we integrate the stream into Kafka for downstream systems.

# Writing to binary format via 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 for time-series storage # Hypertable automatically partitions by time CREATE TABLE ob_updates ( time TIMESTAMPTZ NOT NULL, exchange TEXT NOT NULL, symbol TEXT NOT NULL, side CHAR(1) NOT NULL, -- 'b' or 'a' price NUMERIC NOT NULL, quantity NUMERIC NOT NULL ); SELECT create_hypertable('ob_updates', 'time'); 

What’s Included in the Work?

When ordering an order book scraping system, you get:

  • An architectural solution with protocol selection and resynchronization strategy
  • Source code in Python with asyncio and deployment documentation
  • Grafana dashboards for monitoring latency and errors
  • 99.9% stability guarantee and two weeks of support after deployment

Development timeline: 2 to 4 weeks depending on the number of exchanges and required architecture. Cost is calculated individually. Contact us to discuss your project – we will find an optimal solution.

Common Mistakes in Order Book Scraping

  • Ignoring sequence ID and lack of resynchronization after a gap
  • Using REST polling instead of WebSocket (leads to delays and rate limits)
  • Wrong order: snapshot first, then subscription to updates
  • No buffering of updates before snapshot (first packets are lost)
  • Incorrect reconnect handling without exponential backoff

Order the development of an order book scraping system with a guarantee of stability and low latency. Our experience in high-frequency trading enables us to create solutions that do not lose data and do not desync even under peak loads.