Script definition

Everything a kScript (legacy) define(), input(), source(), and print() call did, an Indicator declares: placement per output, settings as param(...), feeds as…

Everything a kScript (legacy) define(), input(), source(), and print() call did, an Indicator declares: placement per output, settings as param(...), feeds as input(name, source.field), and a debug value as a data-only output. The declarations at the top of src/indicator.ts derive the sheet (wrun/metadata.json), and the sheet is what every host, the registry, and the runtime read. This page is both halves: the port of each kScript definition call, then the sheet dialect field by field, because a hand-written sheet is still the wire format and the only mode for languages without a declaration extractor.

From define() to outputs

define(title, position, showPriceAxis, customTitle, format, maxBarsBack) registered the script and placed it. Nothing in an Indicator registers a script: the package manifest (om-package.json, @scope/name) names it, and placement is per output.

define(...) argumentIndicator
titlethe package name in om-package.json; the sheet's name for listings and legends
position="onchart" / "offchart"output(name, plot, overlay) or output(name, plot, lower), per output; a package can draw in both panes
showPriceAxisnothing to declare: a lower pane has its own axis
customTitle=" ($period)"nothing yet; the legend shows the output description and the chart's settings show the params
format="percentage"unit: "%" on the output (price, %, or a short label)
maxBarsBacknothing: state lives in your variables; size buffers from a param's max

strategy(...): a package that declares it places its own orders through the engine's broker (Strategies overview), and one without it computes values only; the nearest form is om backtest run on the published metric (Strategy functions).

From input() to param

input(name, type, defaultValue, label, constraints, options, group) became param(name, default, { required?, min?, max?, description? }), read once in init() through the generated p_<name>() accessor. Every param is a number (f64); every param shows in the chart's settings for the overlay. The types:

kScript typeIndicator
number, int, float, sliderparam(name, default, { min, max, description }); cast with i32(...) for a bar count
booleanparam(name, 1, { min: 0, max: 1 }), tested as p_flag() > 0.5
select, multiSelect, string, text, color, color[], source, timeframe, session, symbolnot in Indicators yet as params; pins are declarations, colors are per-output options (Typed inputs)
constraints.step, groupno counterpart; label is description

From source() to input

source(src, symbol, exchange) opened a feed and returned a series with history. An Indicator names the FIELD it reads: input(name, source.field, options?) declares one per-bar number, read in state() through in_<name>(), and the host aligns every input to the primary input's grid. There is no series object and no history array; keep the values you need (Execution model).

kScriptIndicator
source("ohlcv", currentSymbol, currentExchange) then .closeinput("close", ohlcv.close): an unpinned input follows the chart
source("ohlcv", "BTCUSDT", "BINANCE")input("btc_close", ohlcv.close, { symbol: "BTCUSDT", exchange: "BINANCE_FUTURES" }), both pins together
source("liquidations", ...)input("liqs", liquidations.liquidations, { missing: "zero" })
source("funding_rate", ...)input("funding", funding.rate_close)
source("buy_sell_volume", ...), .buyinput("buy", trades.volume, { side: "BUY" })
orderbook(...), volume_profile(...)input("book", book.cells, { max_cells, block_size }), input("profile", volume_profile.cells, { max_cells })

The sources and every field are the Data sources catalog; the options are exchange, symbol, interval, outcome, binding, tenor, side, token, missing, description, and for celled classes max_cells, block_size, max_depth.

From print() to a debug output

print() and printTimeSeries() wrote to a script console. There is no console: declare a data-only output (none) for the value you want to see, write it in finalize(), and read it in the chart legend or with om metric series on your machine. It costs nothing to draw and it is a metric you can alert on, which is often what the print was for.

import { funding, input, line, lower, none, ohlcv, output, overlay, param } from "./sdk/declare";
import { in_close, in_funding } from "./gen/inputs";
import { emitRow, out_debug_alpha, out_funding_pct, out_rsi, out_smoothed } from "./gen/outputs";
import { p_period, p_show_raw } from "./gen/params";
import { Ema, Rsi } from "./sdk/ta";

// define(title="RSI", position="offchart", axis=true, format="percentage") became one lower output with a unit.
param("period", 14, { min: 2, max: 200, description: "RSI Period" });
// input(type="boolean") became a 0/1 param.
param("show_raw", 1, { min: 0, max: 1, description: "Write the raw RSI as well as the smoothed one" });
// source("ohlcv", currentSymbol, currentExchange).close became one input per field read.
input("close", ohlcv.close);
// source("funding_rate", ...) became a funding input; it aligns to the close's grid.
input("funding", funding.rate_close, { description: "Funding rate, as of the bar" });
output("rsi", line, lower, { unit: "%", color: "#7c3aed", width: 2, description: "RSI (14)" });
output("smoothed", line, lower, { unit: "%", color: "#38bdf8", width: 1, description: "RSI smoothed by a 5-bar EMA" });
output("funding_pct", line, overlay, { unit: "%", color: "#f59e0b", description: "Funding as a percent, on the price pane" });
// print("alpha", ...) became a debug output: never drawn, readable in the legend and as a metric.
output("debug_alpha", none, lower, { description: "The EMA alpha in use" });

let rsi = new Rsi(14);
const ema = new Ema(5);
let showRaw: bool = true;
let value: f64 = NaN;
let smoothed: f64 = NaN;
let fundingRate: f64 = NaN;

export function init(): void {
  rsi = new Rsi(i32(p_period()));
  showRaw = p_show_raw() > 0.5;
}

export function state(): i32 {
  value = rsi.update(in_close());
  fundingRate = in_funding();
  smoothed = isNaN(value) ? NaN : ema.update(value);
  return isNaN(value) ? 0 : 1;
}

export function finalize(): void {
  out_rsi(showRaw ? value : NaN);
  out_smoothed(smoothed);
  out_funding_pct(fundingRate * 100.0);
  out_debug_alpha(2.0 / 6.0);
  emitRow();
}

export function reset(): void {
  rsi.reset();
  ema.reset();
  value = NaN;
  smoothed = NaN;
  fundingRate = NaN;
}

A 0 in show_raw writes NaN to the raw line, which draws nothing: that is what a boolean toggle on a plot ports to.

The declaration grammar

Instead of hand-writing wrun/metadata.json, declare params, inputs, and outputs as typed top-level statements of src/indicator.ts, imported from ./sdk/declare:

import { input, line, lower, ohlcv, output, overlay, param } from "./sdk/declare";

param("period", 14, { min: 2, max: 200, description: "Lookback window" });
input("close", ohlcv.close);
input("btc_close", ohlcv.close, { symbol: "BTCUSDT", exchange: "BINANCE_FUTURES" });
output("value", line, lower, { unit: "score" });

The build extracts the declarations statically (the code never runs at build time), derives the sheet, and generates the accessors from the same in-memory object, so the two cannot disagree. The grammar is static and literal-only:

  • param(name, default, options?) with options required, min, max, description.
  • input(name, source.field, options?) with options exchange, symbol, interval (MINUTE, FIVE_MINUTES, FIFTEEN_MINUTES, THIRTY_MINUTES, HOUR, FOUR_HOURS, DAY, WEEK), outcome, binding, tenor, side, token, missing ("carry", "nan", or "zero"; on the first input "nan"/"zero" densify the request grid), description. Sources are bare member references: ohlcv, trades, funding, oi, liquidations, implied_volatility, skew, token_supply, odds, time (metric composition inputs stay metadata-first and are refused by name).
  • output(name, plot?, panel?, options?) with plots line, bar, area, histogram, candle, shape, scatter, none (data-only), panels overlay, lower, and options description, unit, color, colors (the color_by palette, an array of string literals), width, opacity, line_style, color_by, shape_where, displacement_bars (an integer in -500..500; negative literals such as -26 are fine), width_by, and widths (the per-bar width ladder, an array of numeric literals; width_by and widths go together, like color_by and colors). output(...) returns a handle; bind it with a top-level const when a box or segment needs to name it.
  • range(upper, lower, options?) declares a sheet-level band between two rendered outputs with options color, colors, color_by, edge_width, edge_line_style, smooth (presentation-plane like fills, and ABI-neutral: declaring one never flips the sheet to "wrun-2"). Ranges never dedupe: repeat the declaration for several bands, the same pair included (the sheet schema imposes no uniqueness).
  • box(name, options) and segment(name, options) declare per-bar shapes over output HANDLES (Drawing objects). Box options: top, bottom (handles, required), from, to (bar offsets: integer literals in -500..500 or handles, default 0), when (a gate handle), panel ("overlay" or "lower"), color, borderColor, opacity, borderWidth. Segment options: yFrom, yTo (handles, required), from, to, when, panel, color, width, lineStyle. The derived sheet records output NAMES under the snake_case fields (x_from, x_to, border_color, border_width, y_from, y_to, line_style). ABI-neutral like range. A handle that binds no output(...) declaration, a string literal where a handle goes, a non-integer offset literal, or a panel outside the two names is a named build error.

The second runtime contract's vocabulary has declaration forms too, and deriving a sheet that uses any of them stamps abi_version: "wrun-2" automatically (scalar-only declarations keep the first contract):

  • Celled inputs: input(name, <class>.cells, options) with classes volume_profile, book, trade_volume_by_size. max_cells is REQUIRED; block_size (required on book) and max_depth are the book fetch facets; symbol + exchange pin together; description. Scalar-feed knobs (interval, side, ...) are refused on celled inputs.
  • String slots: string(name, { max_bytes, description? }). The declaration is named string, which shadows the type name in a file that imports it; import { string as slot } keeps the type (String functions).
  • Renderers: render.text(name, { y, text, color?, size? }), render.label(name, { x, y, text, color?, size? }), render.table(name, { rows, cols, cells, position? }), render.shape(name, { output, shape, where? }), render.stats_row(name, { output, title?, format?, polarity? }), render.bgcolor(name, { where, color?, color_by?, colors? }). Numeric references name declared outputs; text and table cells name declared string slots.
  • Drawings: draw.line(name, { x1, y1, x2, y2, color?, width?, line_style? }), draw.box(name, { left, top, right, bottom, color? }), draw.polyline(name, { points, color?, width?, line_style? }) where points is a flat ["x0", "y0", "x1", "y1", ...] list of output-name pairs, draw.label(name, { x, y, text, color? }).

Rules the extractor enforces, each as a named build error:

  • Names are string literals; defaults and option values are literals; the code never runs at build time.
  • Declarations are top-level statements of src/indicator.ts only; one anywhere else names the file and line.
  • Indexes follow declaration order: the first input(...) is slot 0 (the primary input), and reordering declarations reorders slots while the generated accessors keep your source name-attached.
  • Box and segment coordinates are output handles bound by a top-level const (let, var, and export const bind too; a handle may be bound below the shape that uses it); the sheet records the output's name, never the handle.

The derived sheet records generated_from: "declarations" plus a source_digest (sha256 of the source), serializes canonically (an unchanged source rewrites nothing), and is DERIVED state from then on: om wrun source set refuses with wrun_source_set_generated and the agent's metadata argument refuses with wrun_metadata_generated, both pointing at the declaration to edit instead. A sheet claiming generated provenance over a declaration-free source blocks the build naming both ways out (restore the declarations, or delete generated_from and source_digest to hand-edit again). A workspace with no declarations stays metadata-first: byte-identical behavior to a hand-written sheet, and the supported mode for languages without an extractor (Rust, Zig, a pre-built module).

The sheet dialect

wrun/metadata.json (the sheet) declares what the module reads and writes: params, inputs and their sources, outputs and their looks, and the ABI the module runs under. It is validated at author, build, install, and publish with the same schema, so a broken sheet fails early and names the field. Every top-level field in one sheet:

{
  "id": "context-skeleton",
  "name": "Context Skeleton",
  "description": "Every top-level field in one sheet.",
  "overlay": "offchart",
  "abi_version": "wrun-1",
  "warmup_bars": 3,
  "params": [
    { "name": "period", "default": 14, "required": false, "min": 2, "max": 200, "description": "Lookback bars" }
  ],
  "inputSources": {
    "close": { "source": "ohlcv", "field": "close" },
    "btc_close": { "source": "ohlcv", "field": "close", "symbol": "BTCUSDT", "exchange": "BINANCE_FUTURES" },
    "funding_rate": { "source": "funding", "field": "rate_close" }
  },
  "inputs": [
    { "index": 0, "name": "close" },
    { "index": 1, "name": "btc_close", "description": "Fixed BTC reference market" },
    { "index": 2, "name": "funding_rate" }
  ],
  "outputs": [
    { "index": 0, "name": "value", "plot": "line", "panel": "lower", "unit": "score" },
    { "index": 1, "name": "raw", "plot": "" },
    { "index": 2, "name": "lagging", "plot": "line", "displacement_bars": -26 }
  ]
}

Field by field:

  • id: the sheet's short name ([a-z0-9][a-z0-9._-]*). The installable package id comes from om-package.json's name (@scope/short-name); the metric ids are wrun/@scope/short-name/<output>.
  • name, description: display strings for listings and chart legends.
  • overlay: "onchart" or "offchart", a legacy default-placement hint; per-output panel is the modern control.
  • abi_version: "wrun-1" or "wrun-2"; ABSENT means "wrun-1". This field is the SOLE ABI authority (see below).
  • wasm_sha256: stamped by the build/export; installs and every run verify the module against it.
  • warmup_bars: a positive-integer hint for how many bars the module needs before its first ready row.
  • params: an ARRAY of {name, default, required?, min?, max?, options?, description?} objects with unique names. Each compute param is read in init() through its p_<name>() accessor; underneath, values reach the module positionally in declaration order. A param with a style binding is a style-only knob: no accessor, its slot stays zero-filled, and its default may be a string (Styling). Compute params need numeric defaults.
  • inputSources: keyed BY INPUT NAME; every input must have a matching entry and vice versa. Each entry is a feed source (source + field + optional pins), a metric composition reference, or the time source (Data sources lists every source, field, and knob).
  • inputs: {index, name, description?}, indexes contiguous from 0. Index 0 is the PRIMARY input: it sets the request grid (symbol, exchange, interval) every other input aligns to. A time source cannot be primary.
  • outputs: {index, name, description?, plot?, panel?, unit?, displacement_bars?, ...styling}, indexes contiguous from 0, unique names. plot: "" declares a data-only output: computed, never drawn (Styling has the full vocabulary, including fills at the top level). displacement_bars (integer, -500..500, default 0) is a DECLARED display offset: the value emitted at bar i is displayed at bar i + displacement_bars (negative = drawn earlier/behind, the lagging span of an Ichimoku; positive = drawn ahead). The runtime never shifts rows: om metric get / om metric series return the value at the bar it was computed on, and the field rides the sheet unchanged through metric catalogs, installs, the code-first build, and the render selection (per-row renderer kinds carry their value output's offset), so chart hosts and the parity comparator apply the shift themselves. om chart indicator preview does not yet honor it: an output with a nonzero displacement_bars is refused by name (wrun_preview_displacement_unsupported) because the preview wire carries no displacement field and the line would render on the bar it was computed on. Preview an undisplaced output instead. The styling vocabulary (the color_by/colors and width_by/widths per-bar ladders included) and the top-level fills, ranges, boxes, and segments arrays are on the Styling page; all four arrays are ABI-neutral and presentation-plane (chart hosts render them, scalar surfaces ignore them). boxes and segments have their own field list below.

Sheets on the second contract may additionally declare string_slots, renderers, and drawings (their own section below); under the first contract the three fields are refused, never ignored.

Boxes and segments (per-bar shapes)

Two top-level arrays legal on both contracts alike: sheet-only (no new wire records, the referenced outputs already stream) and evaluated on EVERY bar row by chart hosts, ignored by scalar surfaces. The semantics and worked examples are on the Styling and Drawing objects pages; the fields:

  • boxes (at most 16), each {name, top, bottom, x_from?, x_to?, when?, panel?, color?, border_color?, opacity?, border_width?}. top and bottom name declared outputs (data-only allowed); x_from / x_to are bar offsets, an integer in -500..500 or the name of an output whose truncated per-bar value is the offset (default 0); when names a gate output (the bar is skipped unless the value is finite and nonzero); panel is overlay or lower (default: the panel of top's output); opacity 0..1 (default 0.2); border_width 0..10 (default 1); color and border_color default to top's output color.
  • segments (at most 16), each {name, y_from, y_to, x_from?, x_to?, when?, panel?, color?, width?, line_style?}. y_from / y_to name declared outputs; offsets and when as on boxes; width 0.5..20 (default 1); line_style solid, dashed, or dotted (default solid); color defaults to y_from's output color and panel to its panel.
  • name follows the output grammar and is unique across outputs, boxes, segments, renderers, and drawings. Every refusal names the field (boxes.0.top, segments.1.x_to, ...).

ABI versions: the four contracts

abi_version in the sheet decides the contract the module is validated and run under; nothing else does (no manifest copy, no CLI flag, no inference from the module's imports). The first contract ("wrun-1") is FROZEN: published packages on it keep running bit-identically forever. Each later contract is ADDITIVE over the one before, the same four exports and scalar block plus one channel family, and freezes in turn: the third ("wrun-3") adds the handle-keyed draw channel, the strategy channel and the last-bar flag (Drawing objects, Strategy functions), the fourth ("wrun-4") the frame channel and series inputs (Drawing primitives). The second ("wrun-2") adds the cell and string channels (Quick reference), the renderer and drawing vocabulary (next section), and these sheet additions:

{
  "id": "orderflow-context",
  "abi_version": "wrun-2",
  "warmup_bars": 1,
  "params": [],
  "inputSources": {
    "close": { "source": "ohlcv", "field": "close" },
    "profile": { "source": "volume_profile" },
    "book": { "source": "book", "block_size": 10, "max_depth": 40 }
  },
  "inputs": [
    { "index": 0, "name": "close" },
    { "index": 1, "name": "profile", "cellType": "array", "max_cells": 256 },
    { "index": 2, "name": "book", "cellType": "array", "max_cells": 80 }
  ],
  "outputs": [{ "index": 0, "name": "imbalance", "plot": "line", "panel": "lower" }]
}
  • inputs[].cellType: "array" marks a celled input. It requires abi_version: "wrun-2" (refused under the first contract naming the way in) and requires max_cells beside it (a positive integer counting the class's TUPLES, the host-enforced cap; an oversize bar refuses the evaluation, never truncates). max_cells without cellType is refused as meaningless on a scalar input.
  • A celled input reads a celled source class (volume_profile, book, tape, trade_volume_by_size; the catalog and each class's knobs are in Data sources), and the pairing is checked both ways: a celled class under a scalar input is refused, and cellType on a scalar-feed source is refused at fetch. A celled input keeps its positional slot in the scalar argument block (the slot's value is NaN) and keeps an ordinary inputSources entry.
  • A celled class can never be the primary input (index 0): the primary must stay a fetched scalar series that defines the request grid. Celled sources may pin symbol + exchange together like any feed source, but never interval: cell blocks exact-join the primary grid row for row (alignment below), so there is no independent clock to pin.
  • generated_from: "declarations" and source_digest are provenance markers written by the code-first pipeline. The schema accepts and preserves them on any sheet; a sheet carrying them is treated as derived state (direct sheet edits are refused, see above).
  • A first-contract sheet over a module that imports the cell or string functions is refused at validation, install, and build; a second-contract sheet over a scalar-only module is legal and computes bit-identically.

Cell alignment is an exact join, never a fill: each primary-grid row gets the celled observation with the same bar open, a bar with no observation gets a PRESENT EMPTY block (the module sees 0 cells, not -1), and celled values are never carried forward the way sparse coarse scalars are (a replayed block would double-count volume). Celled inputs never abstain a row: warmup policy stays the module's state() decision.

Where celled packages run: alerts, om metric get / om metric series, and chart previews evaluate them like any other package (the daemon fetches volume_profile and book live). Backtests and screens REFUSE celled packages by name (wrun_celled_metric_unsupported): their replay and fan-out paths carry no cell blocks yet.

String slots, renderers, drawings

Outputs stay numbers; the second contract adds three top-level arrays that turn some of those numbers (and per-bar text) into chart decorations. All three are refused under the first contract, and every reference is checked at validation: numeric coordinates name declared OUTPUTS, text fields name declared STRING SLOTS, and the two namespaces never substitute for each other.

  • string_slots: {index, name, max_bytes, description?}, indexes contiguous from 0, unique names, at most 64 slots, max_bytes 1..4096. Slots are written per ready bar during finalize() through the generated senders; a slot not written that bar is ABSENT, distinct from a written empty string. Slots are not outputs and never become metrics.
  • renderers (at most 32 per package), each {kind, name, ...}:
    • text: {y, text, color?, size?}. One mark per ready row whose slot is present, at (bar time, y); optional color (string) and size (an integer pixel count, 6..64, refused outside) style the text.
    • label: {x, y, text, color?, size?}. ONE label at (x, y); x is a numeric output in epoch seconds; optional color/size as on text. Lifecycle: the LAST ready row whose slot is present and nonempty and whose x/y are finite wins; later rows with an absent or empty slot do not clear an earlier winner.
    • table: {rows, cols, cells, position?} with cells = exactly rows * cols string-slot names, row-major; caps rows <= 32, cols <= 8. Lifecycle: the LAST ready row where EVERY cell slot is present (empty strings included) wins; no such row, no table.
    • shape: {output, shape, where?, color?, width?} with shape one of circle, cross, triangle_up, triangle_down, diamond, arrow_up, arrow_down, flag, square; optional color (string) and width (positive number) style the mark on the chart. One mark per ready row where output is finite, gated (when where is declared) to rows where the gate is finite and nonzero.
    • stats_row: {output, title?, format?, polarity?}. The output as a stats strip, one entry per ready row.
    • bgcolor: {where, color?, color_by?, colors?}. One background tint per ready row where the where gate output is finite and nonzero. color_by (a data-only output) indexes colors per bar (floor; finite out-of-range clamps to entry 0; a non-finite ladder value, NaN or infinite alike, = no tint that bar, the static color never substitutes); color is the static tint when no ladder is declared; each ladder half without the other is refused by name.
  • drawings (at most 64 per package), kinds line (x1, y1, x2, y2), box (left, top, right, bottom), polyline (points, at most 64 {x, y} pairs), label (x, y, text, plus optional size, a positive number for the rendered text size), plus optional color / width / line_style per kind. Every coordinate names a numeric output; x-axis values are epoch SECONDS (the time source feeds them). Lifecycle is deterministic with no backward scan: only the LAST ready row is evaluated; every declared coordinate finite there (and, for label, its text slot present) means the object exists with createdBar = that row's index, while any NaN coordinate or absent slot there means NO object, regardless of earlier rows.

One more run-level cap: the expanded render selection (every selected entry, number, and string) must stay under 2 MiB per run, refused as wrun_render_result_too_large.

A worked example: a 2x2 session-stats table plus a box drawn around the session's price range. The coordinate outputs are data-only (plot: ""), the box's x coordinates come from the time source (epoch seconds), and the four table cells are rewritten every ready bar so the table always shows the last complete row:

{
  "id": "session-table",
  "name": "Session Range Table",
  "abi_version": "wrun-2",
  "warmup_bars": 1,
  "params": [],
  "inputSources": {
    "close": { "source": "ohlcv", "field": "close" },
    "bar_t": { "source": "time", "field": "bar_open_sec" }
  },
  "inputs": [
    { "index": 0, "name": "close" },
    { "index": 1, "name": "bar_t" }
  ],
  "outputs": [
    { "index": 0, "name": "range_pct", "plot": "line", "panel": "lower", "unit": "%" },
    { "index": 1, "name": "left", "plot": "" },
    { "index": 2, "name": "top", "plot": "" },
    { "index": 3, "name": "right", "plot": "" },
    { "index": 4, "name": "bottom", "plot": "" }
  ],
  "string_slots": [
    { "index": 0, "name": "close_label", "max_bytes": 16 },
    { "index": 1, "name": "close_text", "max_bytes": 32 },
    { "index": 2, "name": "range_label", "max_bytes": 16 },
    { "index": 3, "name": "range_text", "max_bytes": 32 }
  ],
  "renderers": [
    {
      "kind": "table",
      "name": "session_stats",
      "rows": 2,
      "cols": 2,
      "cells": ["close_label", "close_text", "range_label", "range_text"],
      "position": "top_right"
    }
  ],
  "drawings": [
    {
      "kind": "box",
      "name": "session_zone",
      "left": "left",
      "top": "top",
      "right": "right",
      "bottom": "bottom",
      "color": "#f59e0b"
    }
  ]
}
import { in_bar_t, in_close } from "./gen/inputs";
import { emitRow, out_bottom, out_left, out_range_pct, out_right, out_top } from "./gen/outputs";
import {
  sb_clear,
  sb_f64,
  sb_text,
  str_close_label,
  str_close_text_sb,
  str_range_label,
  str_range_text_sb,
} from "./gen/strings";

let close: f64 = NaN;
let high: f64 = NaN;
let low: f64 = NaN;
let left: f64 = NaN;
let right: f64 = NaN;

export function init(): void {}

export function state(): i32 {
  close = in_close();
  if (isNaN(high) || close > high) high = close;
  if (isNaN(low) || close < low) low = close;
  const t = in_bar_t();
  if (isNaN(left)) left = t;
  right = t;
  return low > 0.0 ? 1 : 0;
}

export function finalize(): void {
  const range = (100.0 * (high - low)) / low;
  out_range_pct(range);
  out_left(left);
  out_top(high);
  out_right(right);
  out_bottom(low);
  str_close_label("close");
  sb_clear();
  sb_f64(close, 2);
  str_close_text_sb();
  str_range_label("range");
  sb_clear();
  sb_f64(range, 2);
  sb_text("%");
  str_range_text_sb();
  emitRow();
}

export function reset(): void {
  close = NaN;
  high = NaN;
  low = NaN;
  left = NaN;
  right = NaN;
}

Because every coordinate output is finite on the last ready bar, the box exists with createdBar = that bar; a module that wants a drawing REMOVED emits NaN through its coordinate outputs on the newest bar. Renderers and drawings are decorations, never pane anchors: the chart lane renders them from the package's single overlay, and om metric get / om metric series on the same package keep returning the numeric outputs unchanged (the declarations never alter a metric value).

Pins: fixed markets, fixed intervals

A non-odds, non-time feed source may pin symbol and exchange TOGETHER so a secondary input reads a fixed reference market while the package follows the selector. The pair rule is SCHEMA-ENFORCED: a lone symbol or a lone exchange is refused with the issue on the missing half (symbols are venue-native strings, so half a pin names a market that does not exist). interval is an independent pin on any feed source, and it is legal: a coarser source contributes to a primary row only as-of its candle CLOSE (candle.ts + sourceSec <= min(now, row.ts + primarySec)), so a forming 4h candle never leaks its final value into the 1h rows under it, live or historical. Sparse coarse observations carry the latest CLOSED observation forward; equal-or-finer sources align by bar open, row for row.

  • missing (scalar alignment policy, "carry" | "nan" | "zero"): on a secondary source, absent or "carry" keeps the default above, the latest eligible value carried forward. missing: "nan" delivers NaN on primary bars that bring no observation of their own and never gates row readiness: a naturally sparse series (liquidations) stays sparse and the module decides what missing means. missing: "zero" is the same policy delivering 0 instead of NaN, the mapping most kScript ports apply to a sparse column. Closed coarse candles still carry as-of close: a coarse source is never "missing" between its closes. Refused on celled classes (a bar with no observation is a present empty block) and on the time source.
  • Primary densification: on the PRIMARY input (index 0), "nan" and "zero" change what the request grid IS. Instead of the rows the fetch returned, the package computes on a synthesized dense grid of bar-open timestamps at the primary's interval, in the phase the fetched rows sit on (epoch-aligned for crypto intraday bars, Monday opens for crypto WEEK bars; never an assumed epoch multiple), covering the last windowBars bars and ending at the bar containing now (the closed-bar cut and the forming-bar rules apply to the synthesized rows exactly as to fetched ones). Every fetched observation lands on its own grid bar; bars the fetch did not return deliver the fill (0 or NaN). An empty primary fetch refuses by name (wrun_densify_empty_fetch: no row, no phase to derive), and fetched rows that disagree on phase refuse too (wrun_densify_mixed_phase). Densification requires a fixed-UTC bar phase: a session-aligned grid whose UTC open shifts at a DST transition (CME DAY/WEEK bars) spans two phases across the transition and refuses as wrun_densify_mixed_phase by design; such grids are not supported. Secondaries align onto the synthesized grid under their own policy (carry abstains rows before its first observation, nan/zero fill). This is how a sparse series (liquidations) becomes a dense primary. "carry" on the primary is refused (there is no earlier grid bar to carry from), missing is refused on metric composition sources altogether (the inner package's rows are already a computed grid), and an absent policy keeps the fetched-rows grid byte-identical.
{
  "id": "dense-liquidations",
  "abi_version": "wrun-1",
  "warmup_bars": 20,
  "params": [],
  "inputSources": {
    "liqs": { "source": "liquidations", "field": "liquidations", "missing": "zero" },
    "close": { "source": "ohlcv", "field": "close" }
  },
  "inputs": [
    { "index": 0, "name": "liqs", "description": "Total liquidations, 0 on quiet bars, one row per bar" },
    { "index": 1, "name": "close", "description": "Close, carried onto the dense grid" }
  ],
  "outputs": [{ "index": 0, "name": "liq_ratio", "plot": "histogram", "panel": "lower" }]
}
{
  "id": "btc-4h-context",
  "abi_version": "wrun-1",
  "warmup_bars": 1,
  "params": [],
  "inputSources": {
    "close": { "source": "ohlcv", "field": "close" },
    "btc_4h": { "source": "ohlcv", "field": "close", "symbol": "BTCUSDT", "exchange": "BINANCE_FUTURES", "interval": "FOUR_HOURS" }
  },
  "inputs": [
    { "index": 0, "name": "close" },
    { "index": 1, "name": "btc_4h", "description": "BTC on its own 4h grid, as-of close" }
  ],
  "outputs": [{ "index": 0, "name": "ratio", "plot": "line", "panel": "lower" }]
}

Authoring policy the schema cannot check: keep the primary input (index 0) selector-following (a fully pinned package computes the same value for every selector symbol, which mislabels screens and legends); cross-symbol arithmetic is only dimensionally sane under the shared default USD quote. What a coarse pin costs: the fetch widens by two source intervals, and the value steps once per source candle. The chart reads its own market and interval for every input today, so a pinned package is a package for your machine (Multi-timeframe).

Odds sources: pinned or bindable

An odds source reads Polymarket probabilities. field defaults to close; exchange is implicit (and refused if written); the market key is the conditionId (0x + 64 hex), never the market slug. Three modes:

  • Selector-following: no symbol, no binding; the input inherits a conditionId selector.
  • Pinned: symbol carries the conditionId, outcome is YES or NO (NO only on the close field). om wrun source set repoints pinned odds inputs by market query or conditionId.
  • Bindable: binding: "required" INSTEAD of a symbol; the market arrives per use (sourceBindings on the alert, signal, query, or om metric get --bind input=0x...). Never both on one input; a bindable input's outcome comes from the binding (metadata outcome refused); screens refuse bindable packages (no per-row market exists).
{
  "id": "pm-mom",
  "abi_version": "wrun-1",
  "warmup_bars": 1,
  "params": [{ "name": "period", "default": 5, "min": 1, "max": 200 }],
  "inputSources": { "yes_odds": { "source": "odds", "binding": "required" } },
  "inputs": [{ "index": 0, "name": "yes_odds" }],
  "outputs": [{ "index": 0, "name": "momentum", "plot": "line", "panel": "lower" }]
}

The matching source, complete (Roc over the bound market's YES probability):

import { in_yes_odds } from "./gen/inputs";
import { emitRow, out_momentum } from "./gen/outputs";
import { p_period } from "./gen/params";
import { Roc } from "./sdk/ta";

let roc = new Roc(5);
let value: f64 = NaN;

export function init(): void {
  roc = new Roc(i32(p_period()));
}

export function state(): i32 {
  value = roc.update(in_yes_odds());
  return isNaN(value) ? 0 : 1;
}

export function finalize(): void {
  out_momentum(value);
  emitRow();
}

export function reset(): void {
  roc.reset();
  value = NaN;
}

Metric composition sources

{ "source": "metric", "metric": "wrun/@scope/name/output", "params": {...} } feeds another installed package's output in as an input (the referenced package must be installed). Composition inputs keep open-stamp alignment, so a non-primary metric source pinned COARSER than the primary grid is refused (the inner value would be read before its bar closed); the refusal names the input and both intervals. Do not pin symbol/exchange on metric sources. Composition is metadata-first only: the declaration grammar refuses it by name (Libraries).

Validation, in one list

The schema refuses, naming the path: non-contiguous or duplicate input and output indexes and names; duplicate param names; an input without a source entry or a source entry without an input; a time primary; a lone pin half; outcome/binding outside odds; a conditionId that is not 0x + 64 hex; missing tenor/side/token on sources that require them; a missing value outside carry/nan/zero, missing: "carry" on the primary input, or any missing on a celled source, a metric source, or the time source; a non-integer displacement_bars or one outside -500..500; width_by without widths or widths without width_by, a width_by or color_by naming no declared output or naming its own output, widths outside 1..10 entries of 0.5..20; equal or non-rendered ranges sides, a bgcolor colors/color_by ladder half without the other, a range color_by without colors (bare range colors is legal: the band sign palette), edge_width outside integers 1..10, an edge_line_style outside solid/dashed/dotted; a bgcolor where naming no declared output; a text/label renderer size outside integers 6..64; unknown fields per source; cellType without the second contract or without max_cells; max_cells without cellType; a celled source class under a scalar input, as the primary input, or with a field, an interval pin, or any scalar-feed knob; book without block_size, and block_size/max_depth anywhere but book; string_slots/renderers/drawings under the first contract; non-contiguous or duplicate string-slot indexes and names, max_bytes outside 1..4096, more than 64 slots; more than 32 renderers or 64 drawings; table cells whose length is not rows * cols, rows > 32, cols > 8; polylines with more than 64 points; renderer and drawing references to outputs or string slots that do not exist; style and fill references to outputs that do not exist; more than 16 boxes or 16 segments; a box or segment coordinate, offset, or when gate naming no declared output; a literal x_from/x_to that is not an integer or lies outside -500..500; a box or segment name already used by an output, renderer, drawing, box, or segment; box opacity outside 0..1 or border_width outside 0..10; segment width outside 0.5..20; a line_style outside solid/dashed/dotted or a panel outside overlay/lower on either. Unknown TOP-LEVEL sheet fields are warned about and ignored, never fatal, so newer sheets degrade gracefully on older readers. The messages themselves are in Common errors.