FlowscopeDocs

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:

ReadWhat 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.

Flowscope Script
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):

Flowscope Script
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.

ReadWhat 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.
Flowscope Script
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:

Flowscope Script
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.

Flowscope Script
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.

Flowscope Script
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.