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

```flowscope
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 "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. |

```flowscope
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
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
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 "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](/docs/scripting/examples/liquidation-sweep-reversal) and
  [Wall Absorption](/docs/scripting/examples/wall-absorption), two strategies built on
  these reads.
- [Data and update events](/docs/scripting/guides/data-and-events) for the sources and
  how other exchanges are read.
- The [data family](/docs/scripting/reference/catalog/data) lists every method.
