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…

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.

CategoryWhat the scans give you
Volume analysisbid and ask size summed within the depth window, and their imbalance
Order size analysisthe largest and smallest resting orders on each side, to spot size
Market depthhow far liquidity reaches and where it clusters, for support and resistance reads

Opening the source

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

// 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

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

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.

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

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.