Liquidation magnets
Mark the price levels where liquidation cascades fired, keep them on the chart until price returns, and plot realised liquidations underneath.
A liquidation cascade leaves a mark on the market: a burst of forced orders at one price, often the end of a move. Traders watch those levels afterwards, because price tends to come back and test where the leverage was flushed.
This script watches the liquidations of each bar. When one side’s liquidations are many times their recent average, it marks the bar’s extreme as a level: long liquidations at the low (longs are liquidated as price falls), short liquidations at the high. Each level extends to the right with its size until price trades back into it; then the line stops at that bar. A pane shows the liquidations themselves, longs below zero and shorts above.
script "Liquidation magnets"
input (
avgLen = input.int(100, title: "Average length (bars)", min: 10, max: 1000)
burstMult = input.float(4.0, title: "Burst (x average)", min: 1.5, max: 50.0)
minUsd = input.float(1_000_000.0, title: "Smallest burst (USD)", min: 0.0)
)
data (
chart = subscribe(data.ohlcv)
stats = subscribe(data.stat)
)
type Pocket {
key: int
price: float
usd: float
longs: bool
start: time
open: bool
}
type Burst {
side: string
price: float
usd: float
}
state (
pockets = Pocket[](40)
count = 0
lines = entities.linePool(max: 60)
)
pane liqPane = pane(title: "Liquidations (USD)", height: 0.2)
plot (
longLiqs = plot.histogram(title: "Longs liquidated", color: color.red, on: liqPane)
shortLiqs = plot.histogram(title: "Shorts liquidated", color: color.green, on: liqPane)
)
alert burstAlert = alert(title: "Liquidation burst", onClose: true, payload: Burst)
fn label(usd: float, longs: bool) -> string {
return str.format("{0} liqs {1:,.1}M", longs ? "Long" : "Short", usd / 1_000_000.0)
}
on chart.close {
// A sell liquidates a long, a buy liquidates a short.
let longUsd = stats.sellLiq ?? 0.0
let shortUsd = stats.buyLiq ?? 0.0
longLiqs.plot(0.0 - longUsd)
shortLiqs.plot(shortUsd)
let longAvg = ta.sma(longUsd, avgLen)
let shortAvg = ta.sma(shortUsd, avgLen)
let t = chart.time
let h = chart.high
let l = chart.low
if t == null || h == null || l == null {
return
}
// Close the levels price has traded back into.
for i, p in pockets {
if p.open && t > p.start && l <= p.price && h >= p.price {
pockets[i] = Pocket { key: p.key, price: p.price, usd: p.usd, longs: p.longs, start: p.start, open: false }
let tone = p.longs ? color.red : color.green
lines.get(p.key).set(p.start, p.price, t, p.price, width: 1.0, color: color.withAlpha(tone, 120), style: linestyle.dotted, text: label(p.usd, p.longs))
}
}
let longBurst = longAvg != null && longUsd >= minUsd && longUsd > longAvg * burstMult
let shortBurst = shortAvg != null && shortUsd >= minUsd && shortUsd > shortAvg * burstMult
if longBurst || shortBurst {
let longs = longBurst && longUsd >= shortUsd
let usd = longs ? longUsd : shortUsd
let price = longs ? l : h
count += 1
if pockets.isFull {
pockets.shift()
}
pockets.push(Pocket { key: count, price: price, usd: usd, longs: longs, start: t, open: true })
lines.get(count).set(t, price, t, price, width: 2.0, color: longs ? color.red : color.green, extend: extend.right, text: label(usd, longs))
burstAlert.trigger(Burst { side: longs ? "longs" : "shorts", price: price, usd: usd })
}
}How it works
Liquidations per bar
data.stat carries the market’s liquidations: sellLiq is the value of liquidation orders that sold in the bar (long positions being closed), buyLiq the value that bought (shorts). On a chart they come from the liquidations the chart has seen; spot markets have none. The script treats a missing value as zero liquidations, which is what it means here.
What counts as a burst
A fixed USD threshold would fire all day on BTC and never on a small coin. The script compares each side with its own average over avgLen bars and also asks for a minimum size, so a quiet market does not turn a few thousand dollars into a signal. ta.sma is called on every bar, before any early return, so its history has no holes.
Levels as records
Each level is a Pocket record in a Pocket[] with room for 40. When it is full, shift() drops the oldest before the new one is pushed. Iterating an array gives copies, so a level that price has reached is written back by index with open: false. Its line keeps its pool key and is redrawn ending at the touching bar, dotted, so the chart shows where the level held until.
Why the bar’s extreme
Long liquidations fire as price falls through the longs’ liquidation prices, so the cascade’s end is near the bar’s low; for shorts it is near the high. With the bar’s data alone that is the best estimate of where the forced orders hit. Use a shorter chart interval to place the level more precisely.
Measured liquidation maps
The levels here come from liquidations that already happened. For where open positions would be liquidated, use the chart’s liquidation heatmap: on Hyperliquid it is measured from public positions, on other venues it is modelled from open-interest changes. Scripts do not read those maps; combine the two by eye.
Adapting it
- Only the bigger side. Drop
shortBurstto track long flushes alone, the classic capitulation bottom. - Weight by size. Pass
width: math.clamp(usd / minUsd, 1.0, 4.0)so larger bursts draw thicker lines. - Expire old levels. Close levels older than a day: compare
t - p.startwithtime.days(1).
Related
- Funding extremes for positioning stress from funding.
- User-defined types explains writing records back by index.
datareference,entitiesreference