Cross-exchange liquidations
How the liquidations tape is sourced, normalised, sized and read: WS feeds, schema, side semantics, adaptive bucketing, cluster detection, freshness rules.
See the live widget at /perpetuals/liquidations.
Data sources
Three exchange WebSocket feeds, one normalised Redis stream per asset. Every executed liquidation print lands in the tape — we do not estimate liquidation pressure from open interest, do not infer stop zones, do not synthesise prints.
- Binance.
wss://fstream.binance.com/market/ws/<symbol>@forceOrder— per-symbol public stream. The legacy/ws/path is silently throttled by Binance, the/market/ws/path is the live one. - Bybit.
liquidationtopic onwss://stream.bybit.com/v5/public/linear. - OKX.
liquidation-orderschannel onwss://ws.okx.com:8443/ws/v5/public, instTypeSWAP.
Each daemon writes events into a per-asset Redis Streamprod:liq:<asset>:events capped at MAXLEN ≈ 10 000 (≈ 4 hours of cascade-heavy tape, often the full 24 h on quiet days). The backend reads from one stream per asset for both REST and WebSocket surfaces; there is no separate pubsub channel — the stream itself is the broadcast surface.
Event schema
Per-event fields are identical across the three sources. The two timestamps are deliberately kept distinct:
ts_ms— the exchange-stamped trade timestamp. Use this when comparing print times across venues.producer_ts_ms— our daemon's wall-clock at write time.producer_ts_ms − ts_msis the visible feed latency plus any clock skew between us and the venue. Sampled into a rolling 200-event ring buffer for p50/p99 reporting.exchange∈{binance, bybit, okx}.side∈{long, short}— see semantics below.qty— base-asset units (BTC, ETH, …).price— execution price in quote currency.usd=qty × price. Cross-asset comparisons are in USD; raw qty comparisons across BTC vs DOGE are meaningless.
Side semantics
Liquidation streams encode the side from the perspective of the exchange's closing order, which is the opposite of the trader's position. We normalise to the position that lost:
side: "long"— a long position got force-closed (price moved down through its liquidation level). On Binance this is aSELLforceOrder; on Bybit, a position withside: "Buy"being liquidated; on OKX,posSide: "long".side: "short"— a short position got force-closed (price moved up). On Binance this is aBUYforceOrder; on Bybit, positionside: "Sell"; on OKX,posSide: "short".
Tape colour follows this: red dot = long blew up (price dropped); green dot = short blew up (price rose).
Size scaling
Each dot's radius in viewBox-px is clamp(√(usd / $10 000) × 2, 4, 22). The sqrt damps linear-by-area scaling so a $1 M event reads as ~3.16× a $100 K event — not 10× — and a single cascade print can't blot out everything around it. The lower clamp keeps a readable hit zone on sub-$10 K dust; the upper clamp keeps the biggest cascade from overflowing the panel.
r = clamp(sqrt(usd / 10_000) * 2, 4, 22) // viewBox-px # Reference values (vs the size legend) $10 K → r ≈ 4 $100 K → r ≈ 6.3 $1 M → r = 20 $10 M+ → r = 22 (clamped)
Adaptive rendering
Raw scatter only scales to the ~15-minute regime. Past that, hundreds of small prints in a single second smear into a uniform glow, and cascade structure disappears. We switch render modes by window:
- ≤ 15 m. Raw scatter — one circle per event. The biggest dots are drawn last so cascades stay legible underneath dust.
- 1 h–4 h. 1-minute buckets per (time, side). One circle per non-empty bucket, sized by
Σ usd, plotted at the volume-weighted price within the bucket. A smallN×badge inside the circle shows how many underlying events the bucket merged. - 24 h. Same logic with 5-minute buckets.
Hit-testing, top-N annotations and tooltips run against the rendered (bucketed) events; cluster detection, cumulative pane and rate sub-pane scan raw events since those signals are about per-event density and side flow, not hover targets.
Cluster detection
Price-cluster bands are detected on the raw events of the active window:
reference_price = current mid (latest event price)
bin_width = reference_price * 0.001 // 0.1 %
bin events by round((price - reference) / bin_width)
for each bin:
if Σ usd ≥ 15 % of window total AND distinct events ≥ 3:
flag as cluster
cluster_price = volume-weighted mean of bin's prices
keep top 3 clusters by Σ usdEach cluster gets a dashed horizontal line + badge $X cluster · N× @ price. The volume-weighted mean (rather than the nominal bin midpoint) keeps the line sitting at the depth where the dollars actually concentrated.
Top-N inline annotations
Up to three of the largest dots in the window get an inline $amount · time · venue|N× label so the eye lands on the biggest cascade immediately. The $50 K floor avoids labelling dust. Label anchors flip based on which half of the plot the dot is in, so the text never spills past the right axis or under the left edge.
Liquidation rate sub-pane
Under the cumulative pane, a mirrored bar chart shows Σ usdper time bucket split by side: shorts extend UP from the baseline (green), longs DOWN (red). Bucket size follows the scatter's adaptive thresholds (30 s at ≤ 15 m, 1 min at 1–4 h, 5 min at 24 h) so the columns visually align with the dots above.
Feed health
A per-(asset, exchange) status hash prod:liq:status:<asset>:<exchange> is refreshed every 5 s with a 60 s TTL. The backend converts that into three states:
- ok — daemon alive (status hash refreshed within 30 s) AND an event landed within the last 2 minutes.
- stale — daemon alive, but the venue has been silent for ≥ 2 minutes. Could be a genuinely quiet venue or a silent feed; the FE renders amber.
- missing — daemon dead (status hash expired or never written). Rendered red.
The latency p50 / p99 numbers in the health tooltip are sampled from the daemon's rolling 200-event ring buffer of producer_ts_ms − ts_ms.
Empty-time bands
Contiguous spans of the window with no liquidations at all get a subtle background tint. Without this, a 24 h view with 6 h of local data reads as a broken renderer rather than as “there were no cascades in that stretch”. The threshold is gap ≥ 5 % of window — short enough to flag a quiet 30 m on a 24 h view, long enough to avoid striping the chart with micro-bands during quiet seconds.
Caching
- WebSocket. Live, no cache. The tail is driven by Redis Streams
XREAD BLOCKso the WS task awaits new entries without polling. - /api/liquidations/recent. 5 s public cache — enough to absorb a refresh storm without making the tape stale.
- /api/liquidations/health.3 s public cache. Short enough to look live, long enough that a refresh storm doesn't fan out 18
HGETALLs per request.
Limitations
- Self-reported by venue.Each exchange decides what counts as a liquidation print. Rare spoofed prints (cancel-replace flickers, partial fills counted twice) can sneak in. We do not attempt cross-venue deduplication: a single trader's cascade spread across three venues is three events, by design.
- Stream depth.
MAXLEN ≈ 10 000entries per asset stream. On a busy day a single asset can fill the stream in ~4 hours; on a quiet day the same buffer holds the full 24 h. The 24 h widget window is the hard upper bound — there is no longer-term replay surface. - Mark price is the latest liquidation, not a continuous mid.The horizontal mark line and the price trace are derived from liquidation prints. With sparse data the trace is jaggy; on a continuous mid feed it would smooth out. Don't read the trace as a precise mid history — read it as “where did prints happen?”.
- Cross-exchange temporal skew is real. Binance stamps with 1-second granularity, Bybit and OKX go finer; clocks can drift.
ts_msis what to use for inter-venue alignment;producer_ts_msis only useful as a latency measure, not as a sort key. - OKX free-tier coverage. The
liquidation-orderschannel is available on the public (no-auth) WS, but at the lowest VIP tier OKX's liquidation firehose can lag during synchronised cascades. The health pill flips to stale in those windows.
Versioning
Methodology version v1.0.0 · updated 2026-05-12. Material changes (new venues, formula tweaks, threshold changes) bump the version and update dateModified in the structured data above.