---
title: "Orderbook functions"
description: "Orderbook functions read a book snapshot to measure depth, the distribution of resting liquidity, and bid-versus-ask pressure. kScript (legacy) shipped six…"
order: 39
section: "functions"
---

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

# Orderbook functions

Orderbook functions read a book snapshot to measure depth, the
distribution of resting liquidity, and bid-versus-ask pressure.
kScript (legacy) shipped six accessors over an `orderbook` source, each
windowed
by a depth percentage around the mid price. In Indicators the book is a
celled input (`book.cells`) delivering one `[price, size, side]` tuple
per level, and each accessor is a scan over those tuples with the same
`depthPct` window, worked below and compiled in one module.

| Category | What the scans give you |
| --- | --- |
| Volume analysis | bid and ask size summed within the depth window, and their imbalance |
| Order size analysis | the largest and smallest resting orders on each side, to spot size |
| Market depth | how far liquidity reaches and where it clusters, for support and resistance reads |

## Opening the source

```typescript
input("close", ohlcv.close);
input("book", book.cells, { max_cells: 200, block_size: 10, max_depth: 100 });
```

- `block_size` is REQUIRED: the venue's price-bucket width the snapshot
  is aggregated at. `om block-sizes` lists the legal widths per exchange
  and symbol; a width the venue does not serve is refused at fetch.
- `max_depth` is optional: the top N levels per side. With it, the block
  holds at most `2 * max_depth` tuples, which is what `max_cells` should
  cover.
- `max_cells` is the cap in tuples, required like on every celled input.
- An optional `symbol` + `exchange` pin pair reads a fixed market's book.

```bash
om block-sizes --exchange BINANCE_FUTURES
```

The block is ordered: bids first, descending from the best bid, then asks
ascending from the best ask. `side` is `+1` for a bid level and `-1` for
an ask level, and `size` is the absolute resting quantity. The generated
accessors are the celled trio (`in_book_cells()`, `in_book_read(ptr)`,
`in_book_capacity`, three cells per tuple), callable in `state()` only.

## The depth window

Every kScript accessor took `depthPct` (default `10`): only levels within
that percentage of the mid price count. The mid is the average of the
best bid and the best ask, both read straight off the ordering above (the
first bid tuple and the first ask tuple), and a level is inside the window
when `abs(price - mid) <= mid * depthPct / 100`. Smaller percentages (1
to 5) focus on top-of-book activity; larger ones (10 to 20) read overall
depth.

```typescript
// The average of the best bid and the best ask, or NaN when either side is missing.
function bookMid(cells: StaticArray<f64>, n: i32): f64 {
  let bestBid = NaN;
  let bestAsk = NaN;
  for (let i = 0; i + 2 < n; i += 3) {
    if (cells[i + 2] > 0.0) {
      if (isNaN(bestBid)) bestBid = cells[i];
    } else if (isNaN(bestAsk)) bestAsk = cells[i];
  }
  return isNaN(bestBid) || isNaN(bestAsk) ? NaN : (bestBid + bestAsk) / 2.0;
}

// True when a level sits within depthPct percent of the mid.
function inWindow(price: f64, mid: f64, depthPct: f64): bool {
  return Math.abs(price - mid) <= (mid * depthPct) / 100.0;
}
```

## Accessor functions

| kScript | Indicator | Returns |
| --- | --- | --- |
| `sumBids(book, depthPct)` | `sumSide(cells, n, 1.0, depthPct)` | total bid size within the window |
| `sumAsks(book, depthPct)` | `sumSide(cells, n, -1.0, depthPct)` | total ask size within the window |
| `maxBidAmount(book, depthPct)` | `maxSide(cells, n, 1.0, depthPct)` | the largest single bid within the window |
| `maxAskAmount(book, depthPct)` | `maxSide(cells, n, -1.0, depthPct)` | the largest single ask |
| `minBidAmount(book, depthPct)` | `minSide(cells, n, 1.0, depthPct)` | the smallest bid within the window |
| `minAskAmount(book, depthPct)` | `minSide(cells, n, -1.0, depthPct)` | the smallest ask |

Three scans parameterized by side replace six builtins; each returns
`NaN` when no level of that side sits inside the window, and the sums
return `0` on an empty window.

```typescript
function sumSide(cells: StaticArray<f64>, n: i32, side: f64, depthPct: f64): f64 {
  const mid = bookMid(cells, n);
  if (isNaN(mid)) return NaN;
  let total = 0.0;
  for (let i = 0; i + 2 < n; i += 3) {
    if (cells[i + 2] == side && inWindow(cells[i], mid, depthPct)) total += cells[i + 1];
  }
  return total;
}

function maxSide(cells: StaticArray<f64>, n: i32, side: f64, depthPct: f64): f64 {
  const mid = bookMid(cells, n);
  if (isNaN(mid)) return NaN;
  let best = NaN;
  for (let i = 0; i + 2 < n; i += 3) {
    if (cells[i + 2] == side && inWindow(cells[i], mid, depthPct)) {
      if (isNaN(best) || cells[i + 1] > best) best = cells[i + 1];
    }
  }
  return best;
}

function minSide(cells: StaticArray<f64>, n: i32, side: f64, depthPct: f64): f64 {
  const mid = bookMid(cells, n);
  if (isNaN(mid)) return NaN;
  let best = NaN;
  for (let i = 0; i + 2 < n; i += 3) {
    if (cells[i + 2] == side && inWindow(cells[i], mid, depthPct)) {
      if (isNaN(best) || cells[i + 1] < best) best = cells[i + 1];
    }
  }
  return best;
}
```

## Every accessor in one module

The six accessors plus the imbalance they are usually combined into,
`(bids - asks) / (bids + asks)`, smoothed with the shipped `Ema` so the
per-snapshot noise reads as pressure. `depth_pct` is a param, so the
window is a setting the chart user can change.

```typescript
import { book, input, line, lower, ohlcv, output, param } from "./sdk/declare";
import { in_book_capacity, in_book_cells, in_book_read, in_close } from "./gen/inputs";
import {
  emitRow,
  out_ask_volume,
  out_bid_volume,
  out_imbalance,
  out_imbalance_ema,
  out_max_ask,
  out_max_bid,
  out_min_ask,
  out_min_bid,
} from "./gen/outputs";
import { p_depth_pct, p_smooth } from "./gen/params";
import { Ema } from "./sdk/ta";

param("depth_pct", 10, { min: 0.1, max: 50, description: "Depth window as a percent of the mid price" });
param("smooth", 21, { min: 1, max: 200, description: "EMA window over the imbalance" });
input("close", ohlcv.close);
input("book", book.cells, { max_cells: 200, block_size: 10, max_depth: 100 });
output("bid_volume", line, lower, { color: "#26a69a", width: 2, description: "Bid size within the window" });
output("ask_volume", line, lower, { color: "#ef5350", width: 2, description: "Ask size within the window" });
output("max_bid", line, lower, { color: "#0f766e", width: 1, description: "Largest resting bid within the window" });
output("max_ask", line, lower, { color: "#be123c", width: 1, description: "Largest resting ask within the window" });
output("min_bid", line, lower, { color: "#5eead4", width: 1, description: "Smallest resting bid within the window" });
output("min_ask", line, lower, { color: "#fda4af", width: 1, description: "Smallest resting ask within the window" });
output("imbalance", line, lower, { color: "#94a3b8", width: 1, description: "(bids - asks) / (bids + asks), -1..1" });
output("imbalance_ema", line, lower, { color: "#2563eb", width: 2, description: "Smoothed imbalance" });

// The average of the best bid and the best ask, or NaN when either side is missing.
function bookMid(cells: StaticArray<f64>, n: i32): f64 {
  let bestBid = NaN;
  let bestAsk = NaN;
  for (let i = 0; i + 2 < n; i += 3) {
    if (cells[i + 2] > 0.0) {
      if (isNaN(bestBid)) bestBid = cells[i];
    } else if (isNaN(bestAsk)) bestAsk = cells[i];
  }
  return isNaN(bestBid) || isNaN(bestAsk) ? NaN : (bestBid + bestAsk) / 2.0;
}

// True when a level sits within depthPct percent of the mid.
function inWindow(price: f64, mid: f64, depthPct: f64): bool {
  return Math.abs(price - mid) <= (mid * depthPct) / 100.0;
}

function sumSide(cells: StaticArray<f64>, n: i32, side: f64, depthPct: f64): f64 {
  const mid = bookMid(cells, n);
  if (isNaN(mid)) return NaN;
  let total = 0.0;
  for (let i = 0; i + 2 < n; i += 3) {
    if (cells[i + 2] == side && inWindow(cells[i], mid, depthPct)) total += cells[i + 1];
  }
  return total;
}

function maxSide(cells: StaticArray<f64>, n: i32, side: f64, depthPct: f64): f64 {
  const mid = bookMid(cells, n);
  if (isNaN(mid)) return NaN;
  let best = NaN;
  for (let i = 0; i + 2 < n; i += 3) {
    if (cells[i + 2] == side && inWindow(cells[i], mid, depthPct)) {
      if (isNaN(best) || cells[i + 1] > best) best = cells[i + 1];
    }
  }
  return best;
}

function minSide(cells: StaticArray<f64>, n: i32, side: f64, depthPct: f64): f64 {
  const mid = bookMid(cells, n);
  if (isNaN(mid)) return NaN;
  let best = NaN;
  for (let i = 0; i + 2 < n; i += 3) {
    if (cells[i + 2] == side && inWindow(cells[i], mid, depthPct)) {
      if (isNaN(best) || cells[i + 1] < best) best = cells[i + 1];
    }
  }
  return best;
}

const cells = new StaticArray<f64>(in_book_capacity);
let depthPct: f64 = 10.0;
let ema = new Ema(21);
let n: i32 = -1;
let imbalance: f64 = NaN;
let smoothed: f64 = NaN;

export function init(): void {
  depthPct = p_depth_pct();
  ema = new Ema(i32(p_smooth()));
}

export function state(): i32 {
  in_close();
  n = in_book_cells();
  if (n <= 0) return 0; // no snapshot, or an empty one: nothing to measure
  in_book_read(i32(changetype<usize>(cells)));
  const bids = sumSide(cells, n, 1.0, depthPct);
  const asks = sumSide(cells, n, -1.0, depthPct);
  imbalance = bids + asks > 0.0 ? (bids - asks) / (bids + asks) : NaN;
  smoothed = isNaN(imbalance) ? NaN : ema.update(imbalance);
  return 1;
}

export function finalize(): void {
  out_bid_volume(sumSide(cells, n, 1.0, depthPct));
  out_ask_volume(sumSide(cells, n, -1.0, depthPct));
  out_max_bid(maxSide(cells, n, 1.0, depthPct));
  out_max_ask(maxSide(cells, n, -1.0, depthPct));
  out_min_bid(minSide(cells, n, 1.0, depthPct));
  out_min_ask(minSide(cells, n, -1.0, depthPct));
  out_imbalance(imbalance);
  out_imbalance_ema(smoothed);
  emitRow();
}

export function reset(): void {
  ema.reset();
  n = -1;
  imbalance = NaN;
  smoothed = NaN;
}
```

The `Ema` is fed in `state()` (once per bar, in order) and read in
`finalize()`; feeding a class from `finalize()` would fold the value a
second time on the forming bar's replays.

## Where it runs

The chart lane serves `book` snapshots, so the module draws on the chart;
on your machine, alerts, `om metric get`, and `om metric series` evaluate
it live (an imbalance metric is an alert operand like any other). Screens
and backtests refuse celled packages by name
(`wrun_celled_metric_unsupported`). A bar with no snapshot delivers a
present empty block, never a carried-forward one, so a stale book never
masquerades as a fresh read ([Data sources](../core-concepts/data-sources.md)).

## Practices

- **Depth analysis.** Small windows read the top of book, large ones
  overall depth; make `depth_pct` a param and let the chart user choose.
- **Imbalance.** A large bid-over-ask imbalance often precedes an upward
  move and the reverse a downward one; smooth it before alerting on it,
  since a single snapshot is noisy.
- **Large orders.** `max_bid` and `max_ask` jumping is size arriving; pair
  them with `min_bid` / `min_ask` to tell a thin book from a deep one.
- **Bucket width.** `block_size` decides how many levels a snapshot holds
  at a given depth; pick it with `om block-sizes` and size `max_cells` from
  `max_depth`.
