Order book and liquidation maps
Read the liquidity heatmap's walls, the liquidity in a price range and what became of a wall, and the liquidation model's clusters by leverage, from a script.
A chart can show two heatmaps that look alike and mean very different things.
- The liquidity heatmap is the order book: real orders resting at each price, recorded once a minute. A wall is liquidity someone placed, but it can be pulled before price gets there (spoofing), or eaten through.
- The liquidation heatmap is a model: from open interest and taker flow it estimates where positions were opened, at which leverage, and so where they would be liquidated. Dense clusters act as magnets: price is drawn to them, and when it arrives the forced orders fire. On Hyperliquid the maps are built from public positions and orders instead, closer to real data.
Scripts read both: data.book gives each bar’s book as the liquidity heatmap’s cells
hold it, data.liqmap (a Flowscope extension) the level maps. Both are snapshot
sources: they have no handlers of their own and are read from another
subscription’s handler, one period per bar.
The book as the heatmap shows it
The heatmap draws one cell per bar and price bin (about 0.012 % of the price on a
round step: $10 on BTC). A cell holds the mean size its bin rested at the bar’s
minute closes; on 1m bars that is the book at the minute’s close, and an order that
rested two minutes of five counts two fifths of its size. A data.book period is
exactly that bar’s column:
| Read | What it is |
|---|---|
book.binSize() | The recorded price bucket of the levels (bids(), asks(), depth()). |
book.cellSize() | The heatmap’s bin: the cells sizeAt, wallAbove, wallBelow and change use. |
book.sizeAt(price) | What rests in the cell of price: the wall’s size there. |
book.liquidity(from, to) | Bids and asks resting from from to to. |
book.wallAbove(pct) / book.wallBelow(pct) | The largest cell within pct percent above or below the mid, or with nearest: true the nearest one of at least minSize. |
book.change(price, bars: 1) | The cell of price now against bars periods before. |
Sizes are in base units by default; unit: bookunit.quote gives the notional.
script "Nearest walls"
input (
pct = input.float(0.5, title: "Search range (%)", min: 0.05, max: 5.0)
minUsd = input.float(1000000.0, title: "Smallest wall (USD)", min: 0.0)
)
data (
chart = subscribe(data.ohlcv)
book = subscribe(data.book)
)
plot (
bidWall = plot.line(title: "Bid wall", color: color.green, style: linestyle.step)
askWall = plot.line(title: "Ask wall", color: color.red, style: linestyle.step)
)
on chart.close {
let below = book.wallBelow(pct, minSize: minUsd, nearest: true, unit: bookunit.quote)
let above = book.wallAbove(pct, minSize: minUsd, nearest: true, unit: bookunit.quote)
bidWall.plot(below == null ? null : below.price + below.binSize / 2.0)
askWall.plot(above == null ? null : above.price + above.binSize / 2.0)
}A found cell is a PriceBin: its lower edge price, its top, size, value
(notional) and distancePct from the reference (the mid by default, ref: to choose).
Pulled or filled
A wall that is gone in the next bar either traded away or was cancelled. The book
alone cannot tell; the volume traded at the wall’s prices can. Compare the cell’s
size before and after with the footprint of the bar (data.volume):
script "Pulled or filled walls"
data (
chart = subscribe(data.ohlcv)
book = subscribe(data.book)
vol = subscribe(data.volume)
)
plot (
pulled = plot.marker(title: "Pulled", shape: shape.cross, color: color.gray, size: 6.0)
filled = plot.marker(title: "Filled", shape: shape.square, color: color.amber, size: 6.0)
)
on chart.close {
// The largest bid wall within 0.5 % on the bar before.
let wall = book.wallBelow(0.5, barsAgo: 1)
if wall == null {
return
}
let c = book.change(wall.price + wall.binSize / 2.0)
if c == null || c.before <= 0.0 || c.change > 0.0 - c.before / 2.0 {
return
}
// Lost at least half: what traded at its prices in this bar?
let s = vol.summary(from: wall.price - wall.binSize * 0.001, to: wall.top - wall.binSize * 0.001)
let traded = s == null ? 0.0 : s.total
if traded >= (0.0 - c.change) / 2.0 {
filled.plot(wall.price)
} else {
pulled.plot(wall.price)
}
}The book is sampled once a minute: a wall placed and pulled within a minute is never seen. The terminal’s heatmap marks live pulls and fills as they happen; scripts work from the minute-sampled history, the same in backtests and live.
Liquidation maps
subscribe(data.liqmap) reads the liquidation model of the Liquidation Heatmap
(Binance, Bybit and OKX futures and their aggregates). Each period holds the map’s
levels averaged over the period by the time they stood: long liquidations below the
period’s price (below()), short liquidations above (above()), each level as coins
at its price, so its value is the notional. Every query reports notional in USD.
| Read | What it is |
|---|---|
liq.densityBelow(pct) / liq.densityAbove(pct) | The notional within pct percent below or above the price. |
liq.density(outerPct, innerPct:) | Both sides of a band at once, as a LiqDensity. |
liq.clusterBelow(pct, minSize:) / liq.clusterAbove(pct, minSize:) | The nearest cell of at least minSize within pct percent (nearest: false: the largest). |
liq.sizeAt(price) | The notional in the cell of price. |
liq.change(price, bars: 1) | A cell now against bars periods before: was the cluster swept? |
liq.price() | The market’s close at the end of the period, the default reference. |
script "Liquidation magnets"
data (
chart = subscribe(data.ohlcv)
liq = subscribe(data.liqmap)
)
pane sidePane = pane(title: "Liquidations within 2 % (USD)", height: 0.2)
plot (
below = plot.histogram(title: "Longs below", color: color.red, on: sidePane)
above = plot.histogram(title: "Shorts above", color: color.green, on: sidePane)
magnetDown = plot.line(title: "Cluster below", color: color.orange, style: linestyle.dotted)
magnetUp = plot.line(title: "Cluster above", color: color.cyan, style: linestyle.dotted)
)
on chart.close {
below.plot(0.0 - (liq.densityBelow(2.0) ?? 0.0))
above.plot(liq.densityAbove(2.0))
let down = liq.clusterBelow(2.0, minSize: 2000000.0)
let up = liq.clusterAbove(2.0, minSize: 2000000.0)
magnetDown.plot(down == null ? null : down.price)
magnetUp.plot(up == null ? null : up.top)
}Leverage tiers
The model spreads every position it estimates over 5x, 10x, 25x, 50x, 75x and 100x
(weights 10, 25, 30, 15, 8 and 12 %). High tiers sit close to the entry (100x about
0.6 % away), low tiers far from it (5x about 20 %). minLeverage: and maxLeverage:
keep only the tiers between them:
data highLev = subscribe(data.liqmap, minLeverage: 50.0)The recorded levels (the session’s and the hub’s history) carry no split by leverage. The split of each level comes from the same model run again over the subscription’s bars, open interest and taker volume, up to the period read; a level that run does not hold counts by the tiers’ weights. The filtered map is therefore as exact as the bars: finer bars, a closer split.
Hyperliquid’s maps
map: picks one of Hyperliquid’s maps of the chart’s coin, built from the public
positions and trigger orders of the accounts Flowscope tracks:
liqmap.hlLiquidations (liquidation prices), liqmap.hlStops (stop losses: long stops
below, short stops above) and liqmap.hlTakeProfits (take profits: short ones below,
long ones above). How complete they are depends on how much of the open interest the
tracked accounts hold.
data stops = subscribe(data.liqmap, map: liqmap.hlStops)Real liquidations and open interest
The maps say where liquidations would fire; data.stat says how much did, and
data.oi whether positions closed. A sweep into a cluster that is real shows both:
stats.sellLiq (longs liquidated) spiking and open interest falling in the same bar.
On a chart the bars before the session read what the market recorded of both (the
hub’s history), as the Liquidations and Open Interest studies show them.
script "Flush confirmation"
data (
chart = subscribe(data.ohlcv)
stats = subscribe(data.stat)
oi = subscribe(data.oi)
)
plot flush = plot.marker(title: "Long flush", shape: shape.up, color: color.green, size: 6.0)
on chart.close {
let longs = stats.sellLiq ?? 0.0
let avg = ta.sma(longs, 60)
let open = oi.open
let close = oi.close
if avg == null || open == null || close == null || open <= 0.0 {
return
}
if longs >= 3.0 * avg && longs >= 100000.0 && close < open * 0.999 {
flush.plot(chart.low)
}
}History
Books, profiles and level maps reach back 2,000 bars from the newest bar (up to 250
more: the start moves in steps of 250 bars), and only as far as they were recorded: the session’s own recording and the hours the data
hub holds. Bars without a recording read null, never an invented book. Reads in
on chart.close use the bar that just closed; on the forming bar the latest is the
bar before. The same data gives the same values in a backtest and live.
Related
- Liquidation Sweep Reversal and Wall Absorption, two strategies built on these reads.
- Data and update events for the sources and how other exchanges are read.
- The data family lists every method.