Cross-exchange order book
How the aggregated walls widget combines Binance, Bybit and OKX perpetual depth into a single price-bucketed view: sources, normalization, edge cases, limits.
See the live widget at /perpetuals/order-book.
Data sources
Three exchange feeds and one local aggregator daemon. All depth feeds are public; none require an API key.
- Binance USDT-M perpetuals. WebSocket diff stream
fstream.binance.com/stream?streams=<sym>@depth@100msplus a REST snapshot fromfapi/v1/depth?limit=1000at cold start. The microstructure daemon mirrors the top 50 levels into Redis on the same heartbeat. - Bybit V5 linear perpetuals. WebSocket
stream.bybit.com/v5/public/linear, channelorderbook.50.<SYMBOL>. Snapshot frame on subscribe plus monotonicu-incremented deltas. - OKX V5 SWAP. WebSocket
ws.okx.com:8443/ws/v5/public, channelbooks(400 levels, no auth). Snapshot plus update frames chained viaseqId/prevSeqId. - Tracked assets. BTC, ETH, SOL, BNB, XRP, DOGE — same roster as the rest of the dashboard.
- Aggregator daemon.
scripts/cross_exchange_aggregator.pyreads three Redis depth keys per asset every second, normalizes, sums, and writes the union snapshot back to Redis (with a 30-second TTL).
Normalization: prices and quantities
Cross-exchange aggregation only works once every source speaks the same units. Two adjustments happen at ingest, both at the per-exchange daemon (not the aggregator).
Quantity to base coin. Binance and Bybit return quantities in the base currency directly (BTC for BTC-USDT perp, etc.). OKX returns size in contracts, where one contract is ctVal units of the base coin. For BTC-USDT-SWAP ctVal = 0.01; for DOGE-USDT-SWAP it is 1000. The OKX daemon fetches ctVal from api.okx.com/api/v5/public/instruments?instType=SWAP at startup and applies the multiplier on every level before publishing. Without this step, DOGE depth would be 1000× too deep on the cross-exchange view.
Price into per-asset buckets. Each exchange uses a slightly different tick size; the aggregator rounds prices down to the nearest bucket boundary so adjacent ticks across exchanges merge into the same wall. Defaults below; overridable per-asset via AGG_BUCKET_<ASSET>.
btc: $1.0 eth: $0.10 sol: $0.05 bnb: $0.10 xrp: $0.001 doge: $0.0001
Aggregation: sum + per-exchange split
The aggregator sums quantities at each price bucket across the three sources and keeps the per-exchange contribution alongside the total. That lets the UI render a single wall row plus a stacked-bar tooltip on hover, without a second round-trip.
for src in (binance, bybit, okx):
for (price, qty_base) in src.depth.bids:
bucket = floor(price / BUCKET_SIZE) × BUCKET_SIZE
bid_buckets[bucket]['_total'] += qty_base
bid_buckets[bucket][src] += qty_base
# same for asks
top_bids = sorted(bid_buckets, by price desc)[:50]
top_asks = sorted(ask_buckets, by price asc)[:50]Output payload includes the source list and per-exchange best bid / best ask separately. We do not expose a single consolidated best bid and ask; see the limits section below.
Stale handling
Each producer daemon writes its Redis key with a 60-second TTL and a timestamp. The aggregator computes age per source on every cycle and excludes any source older than 60 seconds from the union view. The excluded source still appears in the sources array with a stale status so the FE can render a degraded indicator.
On the FE: the freshness badge flips to stale when any source is missing or stale, and the per-exchange pill in the widget header shows the age. A persistent stale state usually means the producer daemon hit its watchdog reset path or the WebSocket connection dropped without a clean reconnect.
Limits and known gotchas
- No consolidated best bid and ask. Cross-exchange basis is normal: BTC perp on Binance often trades $5-15 below or above the same instrument on Bybit and OKX. A naive union top of book inverts during these periods (best aggregated bid greater than best aggregated ask). The widget shows aggregated walls plus per-exchange best quotes separately to avoid that pitfall.
- Top 50 bid and 50 ask buckets per side, no more. Each producer feeds 50 levels (Binance) or 50/400 levels (Bybit / OKX); after bucketing we keep the top 50 buckets by price proximity to mid. Wall hunting beyond ±0.5% from mid will not surface here.
- OKX
books-l2-tbtrequires VIP login. The MarketTrace aggregator uses the publicbookschannel (400 levels). The tick-by-tick variant gives lower latency but needs an OKX API key and trading-tier login; we explicitly rejected it during the spike to keep the data path key-free. - BTC-only on the dev stack today. The production systemd templates exist for all six assets, but smoke testing has been done on BTC only; SOL and DOGE need the per-asset
ctValresolution path validated on real data before enable. - Aggregator latency floor.Polling the three Redis keys every second sets a 1-second floor on freshness, plus the slowest source's update interval. Median observed end-to-end age: ~265 ms; p95: ~500 ms. Sub-100 ms latency is not the goal — wall analysis runs at human-decision cadence, not market-making cadence.
Versioning
Initial release v1.0.0 on 2026-05-08. Material changes to the compute path bump the version and update the published date. Cosmetic edits do not.