---
title: "Book Heat"
description: "The chart market's resting order book painted behind the candles, bar by bar: every price level the book held when the bar closed is a cell, and the deeper its…"
order: 86
section: "cookbook"
---

<!-- source: docs/indicators/cookbook/book-heat.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# Book Heat

![Resting order book painted behind the candles as a heatmap, with a bid-ask imbalance pane](/wrun/images/book-heat.png)

The chart market's resting order book painted behind the candles, bar by bar: every price level the book held when the bar closed is a cell, and the deeper its shade the more size rested there, bids below the price and asks above it on one square-root scale, so a wall reads as a bright band the price has to trade through and a thin book as a faint wash. The heatmap sits under the candles, so the bars stay readable on top of it. Below the chart, the bid-ask imbalance inside a depth window around the mid price, sky while the bids outweigh the asks and orange while they do not.

The parts are a celled `book.cells` input ([Order book functions](../functions/order-flow-kit.md)), a `plot.heatmap` declaration that paints that input's block without any output carrying it ([Price canvases](../presentation/price-canvases.md)), one histogram output read off the same block ([Plotting](../presentation/plotting.md)), and a label handle for the one sentence shown on a market without a book ([Drawing objects](../presentation/drawing-objects.md)). This is also the `book-heat` template: the **Book Heat** card under **Order flow** in the editor's starter list, and it compiles as written.

## The wrun indicator

```typescript sample=book-heat
// Book Heat: the chart market's resting order book painted behind the candles as a heatmap of resting size by price,
// bar by bar: the deeper the shade, the more size rested at that price when the bar closed, bids below the price and
// asks above it on one scale, so a wall reads as a bright band the price has to trade through. Below the chart, the
// bid-ask imbalance inside a depth window around the mid: sky while the bids outweigh the asks, orange while they do not.

param.number("depth_pct", 1.0, { min: 0.1, max: 10, step: 0.1, label: "Depth window in percent", description: "Depth window for the imbalance: percent around the mid price, each side" });
input("close", ohlcv.close); // the primary input: the chart's own candles define the grid
input("book", book.cells, { max_cells: 1000, block_size: 10, description: "The resting book at the bar's close, one [price, size, side] level per tuple" }); // side +1 bid, -1 ask; the chart serves its own market's book, up to 500 levels a side
output("imbalance", histogram, lower, { colors: ["#38bdf8", "#f86800"], format: "%", label: "Book imbalance", description: "Bid size minus ask size inside the depth window, as a percent of both" }); // sky above zero, orange below
plot.heatmap({ name: "book_heat", cells: "book", value: "size", behind_candles: true, palette: ["theme.bg", "#2563eb", "#22d3ee", "#facc15"], scale: "sqrt", auto_quantile: 0.98, opacity: 0.85, label: "Resting size", tooltip: "{{value:si}} resting at {{price}}" }); // the host paints the block the input already delivers; no output carries it
string("note", { max_bytes: 40 }); // the one sentence shown when the market serves no order book
handles.label({ anchor: "top_right", align: "right", color: "#94a3b8", size: 12, style: "plain", safe_area: true }); // that sentence: plain slate words at the pane's top right, clear of the chart's buttons

const TUPLE = 3; // f64s per level: price, size, side
const note = draw.label(0); // the one slate sentence; a handle allocates once at module load
let depth = 0.01; // the setting, read in onStart()
let bookSeen = false; // the market served a book on some bar

function onStart(): void {
  depth = p_depth_pct() / 100.0;
}

// onBar() runs once per bar: the host paints the block itself; the module reads the same block for the imbalance.
function onBar(): void {
  const n = in_book_cells();
  if (n > 0) bookSeen = true;
  if (bar.isLast() && !bookSeen) { // no book on any bar (FX, stocks): one sentence in place of an empty map and pane
    sb_clear();
    sb_text("No order book on this market");
    note.set(16.0, 12.0).text(str_note_sb);
  }
  if (n < TUPLE * 2) { // an empty block (no book served for this bar) or one level alone: no reading
    out_imbalance(NaN);
    return;
  }
  const cells = in_book_view(); // this bar's levels in place: the first n values of the build's own buffer
  let bestBid = -Infinity;
  let bestAsk = Infinity;
  for (let i = 0; i + TUPLE - 1 < n; i += TUPLE) {
    if (cells[i + 2] > 0.0) {
      if (cells[i] > bestBid) bestBid = cells[i];
    } else if (cells[i] < bestAsk) bestAsk = cells[i];
  }
  if (!isFinite(bestBid) || !isFinite(bestAsk)) { // one side only: no mid to measure around
    out_imbalance(NaN);
    return;
  }
  const mid = (bestBid + bestAsk) * 0.5;
  let bids = 0.0;
  let asks = 0.0;
  for (let i = 0; i + TUPLE - 1 < n; i += TUPLE) {
    if (Math.abs(cells[i] - mid) > mid * depth) continue; // outside the window
    if (cells[i + 2] > 0.0) bids += cells[i + 1];
    else asks += cells[i + 1];
  }
  const total = bids + asks;
  out_imbalance(total > 0.0 ? (100.0 * (bids - asks)) / total : NaN);
}
```

## How it works

**The block is the canvas.** `input("book", book.cells, { max_cells: 1000, block_size: 10 })` delivers the resting book at each bar's close as `[price, size, side]` tuples, side `+1` for a bid level and `-1` for an ask level, up to 500 levels a side. `plot.heatmap({ name: "book_heat", cells: "book", value: "size" })` tells the host to paint that block: one cell per level, one column per bar, the cell's height the book's own price grouping (`row_height` overrides it), its colour the level's size on the palette. The module never copies or forwards the block; the host reads the same rows the input already delivered.

**The scale.** `palette` runs from the chart's background (`theme.bg`) through blue and cyan to yellow, low to high; `scale: "sqrt"` keeps the many small levels visible beside the walls; `auto_quantile: 0.98` sets the top of the scale at the 98th percentile of every cell in the run, so a single outsized order does not flatten the rest. `value` picks what a cell reads: `"size"` (both sides), `"bid"`, `"ask"`, or `"signed"` (bids positive, asks negative, which centres the palette on zero). `behind_candles: true` is the default; `false` paints over the candles instead. `opacity: 0.85` leaves the grid visible through the cells.

**The reading beside it.** The histogram sums bid and ask size inside `depth_pct` (1%) of the mid, the mid being the best bid and the best ask averaged, and prints their difference as a percent of both: sky above zero, orange below (the two `colors` of a histogram are its sign colours). A bar whose block is empty reads NaN and draws nothing, in the pane and on the canvas alike.

**The hover.** `tooltip: "{{value:si}} resting at {{price}}"` puts a readout under the cursor over any cell, `label` its title; drop `tooltip` for a heatmap with no hover.

## Where it runs

Any market the chart serves an order book for: crypto spot and perpetual venues, Polymarket markets. The chart serves the book snapshots its own order book lane fetches for the chart's market; `block_size` is required by the declaration but the chart reads neither it nor `max_depth`. The heatmap paints as far back as the book history the chart holds; older bars carry an empty block.

## When data is missing

A market with no book lane is refused by name before any fetch, naming the input and the way out. A market whose bars all arrive without a book (FX and stocks, where the lane answers empty) runs: the map stays empty, the histogram reads NaN, and one line of words shows at the top right of the chart, `No order book on this market`, in plain slate: the `note` label handle, written on the live bar while no bar has brought a book, pinned to the pane's top right corner, right-aligned and `safe_area: true`, so it clears the chart's own buttons. A bar with no snapshot carries an empty block: the column is blank and the histogram reads NaN. A book that lists one side only (a halted market, a one-sided prediction market) has no mid, so the histogram reads NaN while the heatmap still paints the side it has.

## Customize it

- **Read one side.** Change `value: "size"` to `"bid"` or `"ask"` to paint one side of the book, or to `"signed"` for a diverging map (bids warm, asks cold) with the palette `["theme.down", "theme.bg", "theme.up"]`.
- **Fix the scale.** Set `min` and `max` in size units to compare days on one scale; `auto_quantile` then goes away.
- **Coarser rows.** `row_height: 50` groups the book into 50-unit price rows on a market whose levels are finer than the chart can show.
- **Over the candles.** `behind_candles: false` paints the cells over the bars, with `opacity: 0.5` to keep them visible.

## Run it

1. In the editor's Explorer, press the **Templates** icon ("Browse starter templates") and pick **Book Heat** under **Order flow**.
2. Press **Run** on a liquid crypto chart (Binance Futures BTCUSDT, for example): the book paints behind the candles as the snapshots arrive and the imbalance pane fills below.
3. Hover a cell for its resting size; at the editor's Console prompt, type `imbalance` to read the live bar's reading.
4. Open the indicator's settings: on the **Style** page, the heatmap's **Sensitivity** slider recolours it live, toward Vivid to light up thinner levels, toward Subtle to keep the brightest colours for the walls ([The Style page](../settings/style-page.md#the-sensitivity-row)).

## Concepts used

- [Order book functions](../functions/order-flow-kit.md) for the `book` celled class, its tuple and the depth-window scans
- [Price canvases](../presentation/price-canvases.md) for `plot.heatmap`, the `value` words, the colour scale and the hover readout
- [Plotting](../presentation/plotting.md) for histogram outputs and their sign colours
