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.
allLiquidation.<SYMBOL>topic onwss://stream.bybit.com/v5/public/linear. - OKX.
liquidation-orderschannel onwss://ws.okx.com:8443/ws/v5/public, instTypeSWAP.
A single Rust binary mt-liqd — all seven assets across the three liquidation venues in one process — 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— themt-liqdbinary'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— the venue's source reference price for the print, in quote currency: Binance's forceOrder price, Bybit's and OKX's bankruptcy price.usd= base quantity × that source reference price. Treat these as estimates, not execution notionals: Bybit and OKX publish the bankruptcyprice on a liquidation, the level at which the position's margin is exhausted, not the price the closing order actually filled at. 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".
One venue-specific note: Bybit's allLiquidation.S is the position side, not the closing-order side — so S: "Buy"means a long was liquidated, the inverse of Binance's forceOrder convention. mt-liqd maps it accordingly.
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.
Cascade alert threshold
A separate daemon, mt-cascade, reads the same streams and posts an alert when a window clears the bar. Every 30 s it re-reads the trailing 5-minute window per asset; a cascade fires when the window total reaches the threshold AND at least two venues contributed. The threshold is the trailing p99 of non-empty clock-aligned 5-minute liquidation buckets over a 7-day lookback, recomputed every 5 minutes and floored at $50 M; until 100 such buckets exist a $50 M default holds instead. On a detection the daemon waits 90 s and re-reads the window before posting, so the published figure is the settled peak rather than the first trigger, and that asset then stays quiet for 30 minutes. The floor went from $1 M to $50 M on 2026-08-18: an adaptive p99 tracking the prevailing regime is the point, but a quiet week had dragged the effective BTC bar down to roughly $2 M and ordinary flow was posting as a cascade.
fire when Σ usd over the trailing 300 s ≥ max(p99(5-min buckets, 7 d), $50M)
and distinct venues ≥ 2
and no alert for this asset in the last 1800 sTop-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 mt-liqd binary'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.1.0 · updated 2026-09-02. v1.1.0 on 2026-09-02: USD notionals relabelled as estimates built on each venue's source reference price (2026-08-04), and the cascade-alert threshold documented, with its floor raised to $50 M (2026-08-18). Material changes (new venues, formula tweaks, threshold changes) bump the version and update dateModified in the structured data above.