---
title: "Order book HUD"
description: "A card at the middle left of the price pane that shows what is resting near the price, read from the exchange's own order book and kept current on the live bar…"
order: 95
section: "cookbook"
---

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

# Order book HUD

A card at the middle left of the price pane that shows what is resting near the price, read from the exchange's own order book and kept current on the live bar. It is drawn in the cockpit look: a dark see-through panel held by four amber corner brackets, with glowing cyan type and its title and labels in capitals. It names the heavier side first, as the headline: Bids heavier or Asks heavier in amber, Balanced in cyan. Under the word, the bids' share of the dollars resting within 1% of the mid lights a bar of 20 segments; three rows give the dollars bid and offered within 0.25%, 0.5% and 1% of the mid, and two rows name the biggest bid and ask within 2%: where each wall sits and how many dollars rest there. The two walls are also drawn on price as thin dotted lines, green under the price and red over it.

The card is one `render.hud(...)` of four tiles with `look: "cockpit"`: a headline pill, a meter and two rows tiles whose lines are words written on the live bar ([HUD cards](../presentation/hud-and-hover-cards.md#hud-cards), [Looks](../presentation/hud-and-hover-cards.md#looks), [Blocks and tiles](../presentation/hud-and-hover-cards.md#blocks-and-tiles)). The book arrives as `book.cells`, one `[price, size, side]` row per level ([Data sources](../core-concepts/data-sources.md), [Order flow](../functions/order-flow-kit.md#what-the-chart-serves)); the numbers are data-only outputs written on every bar, the first word a string slot written on the live bar ([Plotting](../presentation/plotting.md)), and the walls two line handles ([Drawing objects](../presentation/drawing-objects.md#handles)). This is also the `order-book-hud` template: the **Order Book HUD** card under **HUDs** in the editor's starter list, and it compiles as written.

## The wrun indicator

```typescript sample=order-book-hud
// Order Book HUD: what is resting near the price, read from the exchange's own order book, drawn by the chart in the
// cockpit look. One card names the heavier side first (Bids heavier or Asks heavier in amber, Balanced in the look's
// cyan), then the bids' share of the dollars resting within 1% of the mid on a 20-segment bar (50 marked), the
// dollars bid and offered within 0.25%, 0.5% and 1% of the mid (one row per distance), and the biggest bid and ask
// walls within 2%: where each sits and how many dollars rest there (one row per wall). The imbalance and its change
// since the last bar stay data-only outputs for the Console and a watch. The two walls are also drawn on price as thin dotted lines, green under
// the price and red over it, from line_bars bars back to the live bar and on to the price axis. A market without an
// order book (FX, stocks) says so in the first tile. The book arrives as one snapshot per bar, refreshed about once a
// second on the live bar, so the card follows the live book.

// Settings: how far the imbalance must lean before the card names a side, and how far back the wall lines reach.
// The distances the rows name (0.25%, 0.5%, 1% and the 2% wall reach) are constants below, because a tile's label is
// fixed text: change a distance and its label together.
section("Reading");
param.number("lean", 10.0, { min: 1.0, max: 50.0, step: 1.0, label: "Balanced band in points", description: "Imbalance points either side of zero before the card names the heavier side; inside the band it reads Balanced" });
section("Wall lines");
param.int("line_bars", 30, { min: 2, max: 300, label: "Wall line length, bars", description: "Bars the two wall lines reach back from the live bar" });

// Inputs: the chart's own candles set the grid (a file whose first feed is a block starts with them), then the book:
// one [price, size, side] row per price level, side +1 a bid and -1 an ask, sizes in coins. The chart serves its own
// book at its own price grouping, up to 500 levels a side, so 1000 rows hold any bar; block_size is required by the
// declaration, and the chart does not read it.
input("close", ohlcv.close);
input("book", book.cells, { max_cells: 1000, block_size: 10 });

// Outputs: all data-only, one value per bar, so a watch or an alert can read them too; the card shows the words the
// slots below carry, written from these numbers on the live bar.
output("side", none, overlay, { description: "0 asks heavier, 1 balanced or no book, 2 bids heavier: the first tile's colour" });
output("imbalance", none, overlay, { format: "%", description: "Bids minus asks over their sum within 1% of the mid, percent" });
output("imbalance_change", none, overlay, { format: "%", description: "The imbalance's change since the previous bar's snapshot, points" });
output("bid_share", none, overlay, { color: "#10b981", format: "%", description: "Bids as a percent of all dollars resting within 1% of the mid" });
output("bids_near", none, overlay, { color: "#10b981", format: "si", description: "Dollars bid within 0.25% of the mid" });
output("asks_near", none, overlay, { color: "#ff003c", format: "si", description: "Dollars offered within 0.25% of the mid" });
output("bids_mid", none, overlay, { color: "#10b981", format: "si", description: "Dollars bid within 0.5% of the mid" });
output("asks_mid", none, overlay, { color: "#ff003c", format: "si", description: "Dollars offered within 0.5% of the mid" });
output("bids_far", none, overlay, { color: "#10b981", format: "si", description: "Dollars bid within 1% of the mid" });
output("asks_far", none, overlay, { color: "#ff003c", format: "si", description: "Dollars offered within 1% of the mid" });
output("bid_wall", none, overlay, { color: "#10b981", format: "price", description: "The price of the biggest bid within 2% of the mid" });
output("bid_wall_size", none, overlay, { format: "si", description: "Dollars resting at the bid wall" });
output("ask_wall", none, overlay, { color: "#ff003c", format: "price", description: "The price of the biggest ask within 2% of the mid" });
output("ask_wall_size", none, overlay, { format: "si", description: "Dollars resting at the ask wall" });
string("heavier", { max_bytes: 40 }); // the first tile's words, written on the live bar
string("depth_near", { max_bytes: 24 }); // "48.9M / 61.7M": dollars bid / offered within 0.25%, written on the live bar
string("depth_mid", { max_bytes: 24 });
string("depth_far", { max_bytes: 24 });
string("bid_wall_text", { max_bytes: 32 }); // "84000 · 262.0M": the wall's price and the dollars resting there
string("ask_wall_text", { max_bytes: 32 });
handles.line({ width: 1, lineStyle: "dotted" }); // the two wall lines, in chart time and price

// The card, in the cockpit look (corner brackets, monospace type, a faint glow, segment bars): the deciding word leads
// as the headline, the bid-share bar spans the card under it, then one row per distance (dollars bid / offered) and
// one row per wall (price and dollars). Two columns give the rows room for both numbers.
render.hud("order_book", { position: "middle_left", look: "cockpit", title: "Order book", columns: 2, tiles: [
  tile.pill("Heavier side, within 1%", "heavier", { headline: true, color_by: "side", colors: ["#FFB000", "#7DF9FF", "#FFB000"] }),
  tile.meter("Bid share within 1%", "bid_share", { min: 0, max: 100, marks: [50], color: "#7DF9FF" }),
  tile.rows("Bids / asks, USD", [["Within 0.25%", "depth_near"], ["Within 0.5%", "depth_mid"], ["Within 1%", "depth_far"]]),
  tile.rows("Walls within 2%, price and USD", [["Bid wall", "bid_wall_text"], ["Ask wall", "ask_wall_text"]]),
] });

const NEAR_PCT = 0.25; // the three depth bands and the wall reach, percent from the mid (the rows' labels name them)
const MID_PCT = 0.5;
const FAR_PCT = 1.0; // also the band the imbalance reads
const WALL_PCT = 2.0;

const BID_GREEN = rgb(16, 185, 129); // #10b981, the bid wall line
const ASK_RED = rgb(255, 0, 60); // #ff003c, the ask wall line
const bidLine = draw.line(0); // handle objects allocate once; ids are one space across kinds
const askLine = draw.line(1);

const ASKS_HEAVIER = 0.0; // the first tile's ladder rungs, in the colours' order: bear, neutral, bull
const BALANCED = 1.0;
const BIDS_HEAVIER = 2.0;

let lean = 10.0; // settings, read in onStart()
let lineBars = 30;
let bookSeen = false; // a snapshot with both sides has arrived on some bar: the market has a book
let prevImbalance: f64 = NaN; // the previous bar's imbalance, for the capsule
let prevTime: f64 = NaN; // the previous bar's open time, for the bar's width in seconds
let barSeconds: f64 = NaN;

// This bar's readings, set by readBook(): NaN when the bar has no usable snapshot.
let bidsNear: f64 = NaN;
let asksNear: f64 = NaN;
let bidsMid: f64 = NaN;
let asksMid: f64 = NaN;
let bidsFar: f64 = NaN;
let asksFar: f64 = NaN;
let bidWall: f64 = NaN;
let bidWallSize: f64 = NaN;
let askWall: f64 = NaN;
let askWallSize: f64 = NaN;

function clearReadings(): void {
  bidsNear = NaN;
  asksNear = NaN;
  bidsMid = NaN;
  asksMid = NaN;
  bidsFar = NaN;
  asksFar = NaN;
  bidWall = NaN;
  bidWallSize = NaN;
  askWall = NaN;
  askWallSize = NaN;
}

// readBook() walks this bar's snapshot twice: first for the touch (the highest bid and the lowest ask: the chart hands
// the asks over from the highest price down, so position says nothing), then for the dollars in each band and the
// biggest level on each side within the wall reach. A level's dollars are its price times its size in coins. It
// answers false when the bar has no usable snapshot: none delivered, a side missing, or a crossed book.
function readBook(): bool {
  clearReadings();
  const n = in_book_cells(); // numbers delivered this bar: 0 when the block is empty, -1 when the bar has none
  if (n < 3) return false;
  const cells = in_book_view();
  let bestBid = -Infinity;
  let bestAsk = Infinity;
  for (let i = 0; i + 2 < n; i += 3) {
    const price = cells[i];
    if (!(price > 0.0) || !(cells[i + 1] > 0.0)) continue; // an empty level is served but rests nothing
    if (cells[i + 2] > 0.0) bestBid = Math.max(bestBid, price);
    else if (cells[i + 2] < 0.0) bestAsk = Math.min(bestAsk, price);
  }
  if (!(bestBid > 0.0) || !(bestAsk < Infinity) || !(bestAsk > bestBid)) return false; // one side missing, or a crossed book
  bookSeen = true;
  const mid = (bestBid + bestAsk) * 0.5;
  bidsNear = 0.0;
  asksNear = 0.0;
  bidsMid = 0.0;
  asksMid = 0.0;
  bidsFar = 0.0;
  asksFar = 0.0;
  bidWallSize = 0.0;
  askWallSize = 0.0;
  for (let i = 0; i + 2 < n; i += 3) {
    const price = cells[i];
    const size = cells[i + 1];
    if (!(price > 0.0) || !(size > 0.0)) continue;
    const dollars = price * size;
    if (cells[i + 2] > 0.0) {
      const away = ((mid - price) / mid) * 100.0; // percent below the mid
      if (away <= NEAR_PCT) bidsNear += dollars;
      if (away <= MID_PCT) bidsMid += dollars;
      if (away <= FAR_PCT) bidsFar += dollars;
      if (away <= WALL_PCT && dollars > bidWallSize) {
        bidWallSize = dollars;
        bidWall = price;
      }
    } else if (cells[i + 2] < 0.0) {
      const away = ((price - mid) / mid) * 100.0; // percent above the mid
      if (away <= NEAR_PCT) asksNear += dollars;
      if (away <= MID_PCT) asksMid += dollars;
      if (away <= FAR_PCT) asksFar += dollars;
      if (away <= WALL_PCT && dollars > askWallSize) {
        askWallSize = dollars;
        askWall = price;
      }
    }
  }
  if (isNaN(bidWall)) bidWallSize = NaN; // no bid within the reach: no wall to name
  if (isNaN(askWall)) askWallSize = NaN;
  return true;
}

// One wall line: from lineBars bars back to the live bar at the wall's price, extended to the price axis; deleted when
// there is no wall (a delete on a line that was never drawn does nothing).
function drawWall(line: LineHandle, price: f64, t: f64, color: i32): void {
  if (isNaN(price) || isNaN(barSeconds)) {
    line.delete();
    return;
  }
  line.set(t - f64(lineBars) * barSeconds, price, t, price).color(color).style(LineStyle.Dotted).extend(Extend.Right);
}

// Money as text: 48.9M, 262.0M, 950.0K, one decimal, in the row's slot.
function sbUsd(x: f64): void {
  if (x >= 1.0e9) { sb_f64(x / 1.0e9, 1); sb_text("B"); }
  else if (x >= 1.0e6) { sb_f64(x / 1.0e6, 1); sb_text("M"); }
  else { sb_f64(x / 1.0e3, 1); sb_text("K"); }
}

// One depth row: "<bid dollars> / <ask dollars>", or a dash while the bar has no snapshot.
function sbDepth(bids: f64, asks: f64): void {
  sb_clear();
  if (isNaN(bids) || isNaN(asks)) sb_text("-");
  else { sbUsd(bids); sb_text(" / "); sbUsd(asks); }
}

// One wall row: "<price> · <dollars>", or a dash when no level sits within the reach.
function sbWall(price: f64, dollars: f64): void {
  sb_clear();
  if (isNaN(price) || isNaN(dollars)) sb_text("-");
  else { sb_f64(price, 2); sb_text(" · "); sbUsd(dollars); }
}

function onStart(): void {
  lean = p_lean();
  lineBars = i32(p_line_bars());
}

// onBar() runs once per bar: read the snapshot, write every output, and on the live bar write the first tile's words
// and draw the two wall lines.
function onBar(): void {
  const t = bar.time();
  if (!isNaN(prevTime) && t > prevTime && (isNaN(barSeconds) || t - prevTime < barSeconds)) barSeconds = t - prevTime; // the smallest step is one bar
  prevTime = t;
  const hasSnapshot = readBook();
  const depth = bidsFar + asksFar;
  const imbalance = depth > 0.0 ? ((bidsFar - asksFar) / depth) * 100.0 : NaN;
  let side = BALANCED;
  if (imbalance >= lean) side = BIDS_HEAVIER;
  else if (imbalance <= -lean) side = ASKS_HEAVIER; // a NaN imbalance passes neither test and stays Balanced
  out_side(side);
  out_imbalance(imbalance);
  out_imbalance_change(imbalance - prevImbalance); // NaN until two bars in a row have a book
  out_bid_share(depth > 0.0 ? (bidsFar / depth) * 100.0 : NaN);
  out_bids_near(bidsNear);
  out_asks_near(asksNear);
  out_bids_mid(bidsMid);
  out_asks_mid(asksMid);
  out_bids_far(bidsFar);
  out_asks_far(asksFar);
  out_bid_wall(bidWall);
  out_bid_wall_size(bidWallSize);
  out_ask_wall(askWall);
  out_ask_wall_size(askWallSize);
  prevImbalance = imbalance;

  if (bar.isLast()) {
    sb_clear();
    if (!bookSeen) sb_text("No order book on this market");
    else if (!hasSnapshot) sb_text("Waiting for the book");
    else if (isNaN(imbalance)) sb_text("Nothing resting within 1%");
    else if (side == BIDS_HEAVIER) sb_text("Bids heavier");
    else if (side == ASKS_HEAVIER) sb_text("Asks heavier");
    else sb_text("Balanced");
    str_heavier_sb();
    sbDepth(bidsNear, asksNear);
    str_depth_near_sb();
    sbDepth(bidsMid, asksMid);
    str_depth_mid_sb();
    sbDepth(bidsFar, asksFar);
    str_depth_far_sb();
    sbWall(bidWall, bidWallSize);
    str_bid_wall_text_sb();
    sbWall(askWall, askWallSize);
    str_ask_wall_text_sb();
    drawWall(bidLine, bidWall, t, BID_GREEN);
    drawWall(askLine, askWall, t, ASK_RED);
  }
}
```

## How it works

**One snapshot per bar.** `input("book", book.cells, { max_cells: 1000, block_size: 10 })` hands each bar the book the chart loads for its own market, at its own price grouping, up to 500 levels a side. History bars carry the snapshot of their bar; the live bar's book is refreshed about once a second. `block_size` is required by the declaration and the chart does not read it.

**The touch first.** The chart hands the bids over best first and the asks from the highest price down, so the first pass looks for the highest bid and the lowest ask by price, never by position, and skips empty levels. The mid is halfway between them; a book missing a side, or crossed, reads as no snapshot.

**Dollars by distance.** The second pass prices each level (its price times its size in coins) and adds it to every band it sits inside: 0.25%, 0.5% and 1% from the mid. The 1% band also gives the imbalance, bids minus asks over their sum in percent, and the bid share, bids over that sum. The biggest level on each side within 2% is that side's wall.

**The deciding word.** An imbalance at or above `lean` (10 points) reads Bids heavier, at or below minus `lean` reads Asks heavier, and anything between reads Balanced. A data-only `side` output (0, 1 or 2) picks the pill's colour from its ladder: amber for either heavier side and cyan for Balanced, so the colour says whether the book leans and the word says which way. The imbalance and its change since the previous bar's book, in points, are written on every bar as `imbalance` and `imbalance_change` for the Console.

**The bid share in segments.** The meter reads `bid_share` from 0 to 100 in the cyan it declares; the cockpit look draws a meter as 20 segments, so each segment stands for 5 points of the share and ten lit segments is an even book. The number beside the label is the share itself.

**The look draws the tiles.** `look: "cockpit"` sets the dark see-through panel with square corners, the four corner brackets in its amber accent, monospaced cyan type with a soft glow, the title and the labels in capitals, and each tile kind's drawing: the pill as its word alone in its colour, the meter as segments. It keeps its colours on a dark and a light chart alike ([Looks](../presentation/hud-and-hover-cards.md#looks)).

**The walls on price.** Two line handles, made once at load, are set on the live bar at the two wall prices, reaching back `line_bars` (30) bars and extended to the price axis, dotted, green for the bid wall and red for the ask wall. A side with no level inside 2% deletes its line.

## Where it runs

Every market whose order book the chart serves to an indicator: crypto perpetuals and spot such as Binance Futures BTCUSDT and Binance ETHUSDT. FX and stocks have no order book, and some thin perpetuals have none on the chart either: there the first tile reads "No order book on this market" and nothing is drawn. On a prediction market the chart serves a book in coarse price steps, wider than 1% of a mid in the tens of cents, so the card reads "Nothing resting within 1%" and names no wall.

## When data is missing

- A market with no order book on the chart: the first tile says so, every number reads a dash and no line is drawn.
- A bar whose snapshot has not arrived yet, or arrived with one side empty or crossed: that bar writes no numbers, and on the live bar the first tile reads "Waiting for the book" until it does.
- A book with no level inside 1% of the mid: the first tile reads "Nothing resting within 1%", every band reads 0 (the bands nest) and the bid share reads a dash with no segment lit; a level within 2% is still named as a wall.
- A band with no level inside it reads 0; a side with no level within 2% names no wall and draws no line.

## Customize it

- **A wider middle.** Raise `lean` so the card names a side only on a clear lean.
- **Longer or shorter wall lines.** `line_bars` sets how far back the two lines reach from the live bar.
- **Other distances.** The bands and the wall reach are the constants `NEAR_PCT`, `MID_PCT`, `FAR_PCT` and `WALL_PCT`; change one together with the row labels that name it, since a tile label is fixed text.
- **The imbalance as a number.** `tile.value("Imbalance within 1%", "imbalance", { format: "%", delta: "imbalance_change" })` puts the imbalance on the card with its change since the last bar's book in the capsule.
- **Another corner.** Change `position: "middle_left"` to any of the nine anchors, `top_left` to `bottom_right`.
- **Change the look.** `look:` takes any of the twelve shipped looks, `default` to `classic` ([Looks](../presentation/hud-and-hover-cards.md#looks)), and a word declared beside it still wins; the Look row on the indicator's Style page switches it without code ([The Style page](../settings/style-page.md#the-look-row)).

## Run it

1. In the editor's Explorer, press the **Templates** icon ("Browse starter templates") and pick **Order Book HUD** under **HUDs**.
2. Press **Run** on a crypto chart: the cockpit card appears at the middle left and the two walls draw on price.
3. At the editor's Console prompt, type `outputs` to read the newest value of every output: the imbalance and its change, the bid share, the six depth sums and the two walls with their sizes.

## Concepts used

- [HUD cards](../presentation/hud-and-hover-cards.md#hud-cards) and [Blocks and tiles](../presentation/hud-and-hover-cards.md#blocks-and-tiles) for `render.hud`, the four tiles, the headline and the pill's colour ladder
- [Looks](../presentation/hud-and-hover-cards.md#looks) for `look: "cockpit"` and what it draws for each tile kind
- [The Style page](../settings/style-page.md#the-look-row) for the Look row that switches the look without code
- [Data sources](../core-concepts/data-sources.md) and [Order flow](../functions/order-flow-kit.md#what-the-chart-serves) for `book.cells`, its row shape and its order
- [Drawing objects](../presentation/drawing-objects.md#handles) for line handles made once and set on the live bar
- [Plotting](../presentation/plotting.md) for data-only outputs and string slots
