MarketTrace
M1PositioningM2Order BookM3LiquidationssoonM4Funding
Methodology · v1.0.0 · published 2026-05-08

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.

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

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.