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(...) argument | Indicator |
|---|---|
title | the 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 |
showPriceAxis | nothing 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) |
maxBarsBack | nothing: 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 type | Indicator |
|---|---|
number, int, float, slider | param(name, default, { min, max, description }); cast with i32(...) for a bar count |
boolean | param(name, 1, { min: 0, max: 1 }), tested as p_flag() > 0.5 |
select, multiSelect, string, text, color, color[], source, timeframe, session, symbol | not in Indicators yet as params; pins are declarations, colors are per-output options (Typed inputs) |
constraints.step, group | no 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).
| kScript | Indicator |
|---|---|
source("ohlcv", currentSymbol, currentExchange) then .close | input("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", ...), .buy | input("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 optionsrequired,min,max,description.input(name, source.field, options?)with optionsexchange,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 plotsline,bar,area,histogram,candle,shape,scatter,none(data-only), panelsoverlay,lower, and optionsdescription,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-26are fine),width_by, andwidths(the per-bar width ladder, an array of numeric literals;width_byandwidthsgo together, likecolor_byandcolors).output(...)returns a handle; bind it with a top-levelconstwhen aboxorsegmentneeds to name it.range(upper, lower, options?)declares a sheet-level band between two rendered outputs with optionscolor,colors,color_by,edge_width,edge_line_style,smooth(presentation-plane likefills, 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)andsegment(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 likerange. A handle that binds nooutput(...)declaration, a string literal where a handle goes, a non-integer offset literal, or apaneloutside 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 classesvolume_profile,book,trade_volume_by_size.max_cellsis REQUIRED;block_size(required onbook) andmax_depthare the book fetch facets;symbol+exchangepin together;description. Scalar-feed knobs (interval,side, ...) are refused on celled inputs. - String slots:
string(name, { max_bytes, description? }). The declaration is namedstring, 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;textand tablecellsname 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? })wherepointsis 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.tsonly; 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, andexport constbind 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 fromom-package.json'sname(@scope/short-name); the metric ids arewrun/@scope/short-name/<output>.name,description: display strings for listings and chart legends.overlay:"onchart"or"offchart", a legacy default-placement hint; per-outputpanelis 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 ininit()through itsp_<name>()accessor; underneath, values reach the module positionally in declaration order. A param with astylebinding 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 thetimesource (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. Atimesource 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, includingfillsat the top level).displacement_bars(integer, -500..500, default 0) is a DECLARED display offset: the value emitted at bar i is displayed at bari + 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 seriesreturn 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 previewdoes not yet honor it: an output with a nonzerodisplacement_barsis 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 (thecolor_by/colorsandwidth_by/widthsper-bar ladders included) and the top-levelfills,ranges,boxes, andsegmentsarrays are on the Styling page; all four arrays are ABI-neutral and presentation-plane (chart hosts render them, scalar surfaces ignore them).boxesandsegmentshave 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?}.topandbottomname declared outputs (data-only allowed);x_from/x_toare bar offsets, an integer in -500..500 or the name of an output whose truncated per-bar value is the offset (default0);whennames a gate output (the bar is skipped unless the value is finite and nonzero);panelisoverlayorlower(default: the panel oftop's output);opacity0..1 (default 0.2);border_width0..10 (default 1);colorandborder_colordefault totop'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_toname declared outputs; offsets andwhenas on boxes;width0.5..20 (default 1);line_stylesolid,dashed, ordotted(defaultsolid);colordefaults toy_from's output color andpanelto its panel.namefollows 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 requiresabi_version: "wrun-2"(refused under the first contract naming the way in) and requiresmax_cellsbeside it (a positive integer counting the class's TUPLES, the host-enforced cap; an oversize bar refuses the evaluation, never truncates).max_cellswithoutcellTypeis 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, andcellTypeon a scalar-feed source is refused at fetch. A celled input keeps its positional slot in the scalar argument block (the slot's value isNaN) and keeps an ordinaryinputSourcesentry. - 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+exchangetogether like any feed source, but neverinterval: cell blocks exact-join the primary grid row for row (alignment below), so there is no independent clock to pin. generated_from: "declarations"andsource_digestare 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_bytes1..4096. Slots are written per ready bar duringfinalize()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); optionalcolor(string) andsize(an integer pixel count, 6..64, refused outside) style the text.label:{x, y, text, color?, size?}. ONE label at (x,y);xis a numeric output in epoch seconds; optionalcolor/sizeas ontext. Lifecycle: the LAST ready row whose slot is present and nonempty and whosex/yare finite wins; later rows with an absent or empty slot do not clear an earlier winner.table:{rows, cols, cells, position?}withcells= exactlyrows * colsstring-slot names, row-major; capsrows<= 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?}withshapeone ofcircle,cross,triangle_up,triangle_down,diamond,arrow_up,arrow_down,flag,square; optionalcolor(string) andwidth(positive number) style the mark on the chart. One mark per ready row whereoutputis finite, gated (whenwhereis 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 thewheregate output is finite and nonzero.color_by(a data-only output) indexescolorsper bar (floor; finite out-of-range clamps to entry 0; a non-finite ladder value,NaNor infinite alike, = no tint that bar, the staticcolornever substitutes);coloris the static tint when no ladder is declared; each ladder half without the other is refused by name.
drawings(at most 64 per package), kindsline(x1, y1, x2, y2),box(left, top, right, bottom),polyline(points, at most 64{x, y}pairs),label(x, y, text, plus optionalsize, a positive number for the rendered text size), plus optionalcolor/width/line_styleper kind. Every coordinate names a numeric output; x-axis values are epoch SECONDS (thetimesource feeds them). Lifecycle is deterministic with no backward scan: only the LAST ready row is evaluated; every declared coordinate finite there (and, forlabel, its text slot present) means the object exists withcreatedBar= that row's index, while anyNaNcoordinate 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"deliversNaNon 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 ofNaN, 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 thetimesource.- 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 lastwindowBarsbars and ending at the bar containingnow(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 orNaN). 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 aswrun_densify_mixed_phaseby design; such grids are not supported. Secondaries align onto the synthesized grid under their own policy (carryabstains rows before its first observation,nan/zerofill). 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),missingis refused onmetriccomposition 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, nobinding; the input inherits a conditionId selector. - Pinned:
symbolcarries the conditionId,outcomeisYESorNO(NOonly on theclosefield).om wrun source setrepoints pinned odds inputs by market query or conditionId. - Bindable:
binding: "required"INSTEAD of a symbol; the market arrives per use (sourceBindingson the alert, signal, query, orom metric get --bind input=0x...). Never both on one input; a bindable input's outcome comes from the binding (metadataoutcomerefused); 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.