The Indicator kit on one screen, then the parts of it that are easiest to
forget: the four exports, the generated accessors (the p_ / in_ /
out_ families plus the cell and string families), every declaration
signature, the sources and their fields, the TA classes, and the
om wrun commands. The reference page of kScript (legacy) lists builtins
you call; this page lists what an Indicator declares, because an Indicator
says what it reads and writes and the host does the calling.
One screen
The four exports
| Export | Runs | Do this here | Never here |
|---|---|---|---|
init(): void | once per evaluation, before any bar | read params through p_<param>(), size buffers, construct TA objects | read an input (the in_ accessors trap outside state()) |
state(): i32 | once per bar, oldest first | read inputs through in_<input>() and the cell accessors, update module state, return 1 (row ready) or 0 (abstain) | write an output or a string slot |
finalize(): void | after each bar whose state() returned 1 | write every output through out_<output>(value), write string slots, then emitRow() LAST | read an input |
reset(): void | when the host replays the forming bar | reassign every module-level variable, call .reset() on every TA object | allocate |
Accessors (src/gen/, regenerated before every build)
| Accessor | Phase | Meaning |
|---|---|---|
p_<param>(): f64 | init() | the param's value: its default, or the setting the chart user chose |
in_<input>(): f64 | state() | this bar's scalar input; NaN on a celled input's slot |
out_<output>(value: f64): void | finalize() | writes one output; emitRow(): void commits the row |
in_<input>_cells(): i32 | state() | f64 cells in this bar's block: 0 for a present empty block, -1 when the bar carries no block |
in_<input>_read(ptr: i32): i32 | state() | copies the block into module memory at ptr; returns bytes written, 0, or -1 |
in_<input>_max_cells: i32 | constant | the declared max_cells, in source tuples |
in_<input>_capacity: i32 | constant | the f64 count to preallocate (max_cells x the class's tuple width) |
sb_clear(), sb_text(s), sb_int(n), sb_f64(x, decimals) | finalize() | build one UTF-8 line into a shared buffer, allocation-free |
str_<slot>(s: string), str_<slot>_sb() | finalize() | send a whole string, or the built line, to one slot |
Declarations (./sdk/declare, top-level statements of src/indicator.ts)
| Declaration | Signature |
|---|---|
| Param | param(name, default, { required?, min?, max?, description? }) |
| Input | input(name, <source>.<field>, { symbol?, exchange?, interval?, side?, tenor?, token?, outcome?, binding?, missing?, description? }) |
| Celled input | input(name, <class>.cells, { max_cells, block_size?, max_depth?, symbol?, exchange?, description? }) |
| Output | output(name, plot?, panel?, { description?, unit?, color?, colors?, width?, opacity?, line_style?, color_by?, shape_where?, displacement_bars?, width_by?, widths? }), returns an OutputHandle |
| Range | range(upper, lower, { color?, colors?, color_by?, edge_width?, edge_line_style?, smooth? }) |
| Box | box(name, { top, bottom, from?, to?, when?, panel?, color?, borderColor?, opacity?, borderWidth? }) |
| Segment | segment(name, { yFrom, yTo, from?, to?, when?, panel?, color?, width?, lineStyle? }) |
| String slot | string(name, { max_bytes, description? }) |
| 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? }) |
| 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? }), draw.label(name, { x, y, text, color? }) |
Vocabulary the declarations take:
| Slot | Values |
|---|---|
plot | line, bar, area, histogram, candle, shape, scatter, none (data-only: computed, never drawn) |
panel | overlay (the price pane), lower (its own pane) |
line_style / lineStyle / edge_line_style | solid, dashed, dotted |
shape (renderer) | circle, cross, triangle_up, triangle_down, diamond, arrow_up, arrow_down, flag, square |
missing | carry (default on a secondary), nan, zero; on the primary input nan and zero densify the grid |
side | BUY, SELL |
tenor | ONE_D, THREE_D, ONE_W, ONE_M, TWO_M, THREE_M, SIX_M, ONE_Y |
outcome | YES, NO (NO on the close field only) |
binding | required (the market arrives per use, never together with symbol) |
| Box and segment offsets | integer literals in -500..500, or an output handle whose per-bar value truncates to the offset |
| Colors | any string on outputs and segments; a box fill needs hex, rgb(), or hsl() |
Sources and fields
| Source | Fields | Knobs |
|---|---|---|
ohlcv | open, high, low, close, volume | pins only |
trades | open, high, low, close, volume | side REQUIRED |
funding | rate_open, rate_high, rate_low, rate_close, predicted_open, predicted_high, predicted_low, predicted_close | pins only |
oi | open, high, low, close | pins only |
liquidations | liquidations | side optional (absent = both sides summed); sparse, so a primary usually declares missing: "zero" |
implied_volatility | implied_volatility | tenor REQUIRED |
skew | skew | tenor REQUIRED |
token_supply | marketcap, first_marketcap, marketcap_dominance_percent, circulating_supply, total_supply, max_supply, total_value_locked, fully_diluted_valuation, cg_marketcap_rank, total_volume, usd_price | token REQUIRED |
odds | open, high, low, close (default), volume | conditionId as symbol plus outcome, or binding: "required"; exchange is implicit and refused |
time | bar_open_sec | no knobs, never the primary input |
metric (sheet only) | metric: "wrun/@scope/name/output", params | no pins; refused when pinned coarser than the primary |
Pins: symbol and exchange go together (a lone half is refused);
interval pins alone and is read as of the coarser candle's close. The
chart reads its own market for every input; your machine honors pins.
Celled classes (abi_version: "wrun-2")
| Class | Tuple | Width | Knobs | Served by the daemon |
|---|---|---|---|---|
volume_profile | [low, high, buy, sell] per price bucket | 4 f64 | optional symbol + exchange | yes |
book | [price, size, side] per level, side +1 bid / -1 ask, bids descending then asks ascending | 3 f64 | block_size REQUIRED (om block-sizes), optional max_depth, optional pin pair | yes |
tape | [offsetMs, price, size, side] per live print, side +1 BUY / -1 SELL | 4 f64 | min_size REQUIRED, history: "0" default, optional pin pair | live buffer only |
trade_volume_by_size | declared for cross-host parity | none | no: refused as wrun_cells_unavailable |
The TA classes (./sdk/ta)
One class per kScript (legacy) TA builtin, every one checked bar for bar
against the kScript engine. Construct in init(), .update(...) once per
bar in state() (it returns the primary value, NaN until warm),
.reset() from reset(). update(x) takes one value unless noted.
| Group | Classes | Per bar |
|---|---|---|
| Averages | Sma, Ema, Rma, Wma, Hma, Alma, Swma, Linreg | update(x); Vwma is update(x, volume) |
| Statistics and series | Sum, Median, Percentile, Variance, Stdev, Zscore, Change, Mom, Roc, Cum, Fixnan | update(x); Correlation is update(a, b) |
| Oscillators | Rsi, Cmo, Tsi, Macd | update(x) |
| Oscillators over OHLC | Cci, Wpr, Stoch, Stochastic | update(high, low, close); Mfi is update(high, low, close, volume); Obv is update(close, volume) |
| Ranges and bands | Tr, Atr | update(high, low, close); Bb is update(x), Keltner is update(x, high, low, close), Donchian is update(high, low) |
| Window extremes | Highest, Lowest, HighestBars, LowestBars | update(x) (pass the high or the low, the column kScript reads by default) |
| Trend systems | Adx, Ichimoku, Psar, Supertrend | update(high, low, close); Vwap is update(open, high, low, close, volume, tsMs), tsMs the bar's open time in milliseconds |
| Events | Rising, Falling, PivotHigh, PivotLow | update(x); ValueWhen is update(condition, x), BarsSince is update(condition), Cross is update(a, b): i32 (+1 up, -1 down, 0 otherwise) |
Multi-output classes return the primary line and carry the rest as
fields: Bb, Keltner, Donchian (basis, upper, lower); Macd
(macd, signal, hist); Stoch, Stochastic (k, d);
Supertrend (line, direction); Adx (adx, plusDi, minusDi);
Ichimoku (tenkan, kijun, senkouA, senkouB, chikou);
Highest, Lowest (bars, how many bars ago); HighestBars,
LowestBars (value, the matching extreme).
Every class allocates in its constructor and restores its just-constructed
state on .reset(). Two honest exceptions to bit-exactness: Ichimoku's
chikou is the current close (the engine reads a future bar), and Vwap
anchors other than none, "day" and a millisecond bucket are unproven.
The composite and every convention are in
TA library.
Commands
om wrun templates --format text
om wrun create @you/my-indicator ./my-indicator --template sma-codefirst
om wrun build ./my-indicator
om wrun validate ./my-indicator
om wrun install ./my-indicator --replace
om wrun list --format text
om wrun show @you/my-indicator
om metric get --metric wrun/@you/my-indicator/sma:period=20 --symbol BTCUSDT --exchange BINANCE_FUTURES
om metric series --metric wrun/@you/my-indicator/sma --params period=20 --symbol BTCUSDT --exchange BINANCE_FUTURES --bars 60
om chart indicator preview --type wrun/@you/my-indicator/sma --bars 300
om wrun source search polymarket "fed rate decision"
om watch publish ./my-indicator --dry-run
om install @you/my-indicator
om wrun upgrade @you/my-indicator --version 0.2.0
om wrun remove @you/my-indicator| Command | What it does |
|---|---|
om wrun templates | lists the nine starting points: sma-codefirst, sma-box, sma, vp-buy-share-codefirst, polymarket-odds, conviction-score, event-asset-divergence, escalation-risk, hud-terminal |
om wrun create <package> [dir] | scaffolds a workspace (--template, --force); om wrun scaffold <dir> --name is the same verb with the name as a flag |
om wrun build <dir> | derives the sheet from declarations, regenerates src/gen, runs bun install once and bun run build, checks the module against the contract, exports dist/package (--out, --wasm, --compile-command, --install, --replace, --no-install-deps) |
om wrun install <dir> | the build plus the local install under ~/.openmarket/packages |
om wrun export <dir> | the export step alone (--force, --install) |
om wrun validate <dir> | validates a workspace or an exported package without building |
om wrun list, om wrun show <id> | installed packages and their outputs; one package or metric id |
om wrun source search <provider> <query>, om wrun source set <name> | find a Polymarket conditionId; repoint a pinned odds input (refused on a derived sheet and on a bindable input) |
om wrun upgrade <package> | moves saved alert locks onto an installed version (--version) |
om wrun remove <package> | uninstalls (--version for one version); warns when alerts still reference the package |
om metric get, om metric series | the newest value; one [barOpenSec, value] pair per bar (--bars 1..500, default 30) |
om chart indicator preview | draws one locally installed output on your active chart, published or not |
om watch publish <dir> | publishes a package directory to the registry (--dry-run prints what would ship) |
om install, om search --kind wrun-indicator, om packages delete, om access | consumers, discovery, deleting a package you publish, private-package grants |
Caps
16 boxes, 16 segments, 32 renderers, 64 drawings, 64 polyline points, 64 string slots of at most 4096 bytes, 64 KiB of strings per row, 8 MiB per run, 2 MiB of expanded render result, offsets in -500..500, 4 MiB of module memory. Every number, with the refusal it produces, is in Limits.
The four exports, exactly
The module contract is exact and the build refuses deviations statically, naming the found and required signature:
init(): voidruns once per evaluation, before any bar. Read params here throughp_<param>().state(): i32runs once per bar. Read that bar's inputs throughin_<input>(). Return1when a row is ready,0during warm-up. Modules on the second ABI read celled inputs here too: the cell accessors trap outsidestate().finalize(): voidruns after each ready bar: write every output throughout_<output>(value), write any string slots, then commit the row withemitRow()LAST.reset(): voidclears ALL module state: reassign every module-level variable and call.reset()on every TA object. The runtime calls it for live same-bar replay, so a stale field here corrupts re-evaluated bars.
Redesigned signatures (init(args: Array<f64>), a finalize that returns
the value) DO compile under AssemblyScript; the build's static ABI check
refuses the module afterwards, so keep the contract and let values flow
through the accessors. A row whose state() returned 0 has no
finalize() call, no drawn point, and no metric value
(Execution model).
Generated accessors
The runtime contract is positional; the accessors are how your source stays name-attached. One function per declared name, regenerated from the sheet before every build:
p_<param>(): f64(params, read ininit)in_<input>(): f64(inputs, read instate)out_<output>(value: f64): voidplusemitRow(): void(outputs, written infinalize)
A declared name becomes its accessor by lowercasing it and collapsing
every run of characters outside [a-z0-9_] to one _: param fast.len
becomes p_fast_len(), input btc-close becomes in_btc_close(), output
BTC-Ratio becomes out_btc_ratio(). Param and input names are lowercase
in the sheet ([a-z0-9][a-z0-9._-]*); output names may carry capitals
([A-Za-z0-9][A-Za-z0-9._-]*) and lose them in the accessor. Two names in
one family that escape identically are refused at generation, and
duplicate param names are refused at the schema. Every accessor family
gets the same treatment: a string slot summary sends through
str_summary() and str_summary_sb(), a celled input profile reads
through in_profile_cells() and in_profile_read().
A sheet whose names need escaping, and the source that reads them:
{
"id": "ref-accessors",
"abi_version": "wrun-1",
"warmup_bars": 26,
"params": [
{ "name": "fast.len", "default": 12, "min": 1, "max": 200 },
{ "name": "slow.len", "default": 26, "min": 2, "max": 400 }
],
"inputSources": {
"close": { "source": "ohlcv", "field": "close" },
"btc-close": { "source": "ohlcv", "field": "close", "symbol": "BTCUSDT", "exchange": "BINANCE_FUTURES" }
},
"inputs": [
{ "index": 0, "name": "close" },
{ "index": 1, "name": "btc-close", "description": "Fixed BTC reference market" }
],
"outputs": [
{ "index": 0, "name": "spread", "plot": "line", "panel": "lower" },
{ "index": 1, "name": "BTC-Ratio", "plot": "line", "panel": "lower" }
]
}import { in_btc_close, in_close } from "./gen/inputs";
import { emitRow, out_btc_ratio, out_spread } from "./gen/outputs";
import { p_fast_len, p_slow_len } from "./gen/params";
import { Ema } from "./sdk/ta";
let fast = new Ema(12);
let slow = new Ema(26);
let spread: f64 = NaN;
let ratio: f64 = NaN;
export function init(): void {
fast = new Ema(i32(p_fast_len()));
slow = new Ema(i32(p_slow_len()));
}
export function state(): i32 {
const close = in_close();
const btc = in_btc_close();
spread = fast.update(close) - slow.update(close);
ratio = btc > 0.0 ? close / btc : NaN;
return isNaN(spread) || isNaN(ratio) ? 0 : 1;
}
export function finalize(): void {
out_spread(spread);
out_btc_ratio(ratio);
emitRow();
}
export function reset(): void {
fast.reset();
slow.reset();
spread = NaN;
ratio = NaN;
}Editing the sheet moves the SLOT inside each accessor while your source keeps the NAME, so reordering params or inputs never changes what the module computes; a renamed declaration renames its accessor, and the compiler then points at every stale import.
Raw positional slot literals (getFloat(0), getInt(0), setOutput(0, ...), wrun_arg_f64(0), wrun_output_f64(0, ...)) fail a scaffold build
BEFORE the compiler runs, in every file under src/ except src/sdk and
src/gen, because slots silently rebind when the sheet changes. Variable
indexes stay legal: src/sdk/sdk.ts wraps the raw host imports for
genuinely dynamic access. Builds that bypass the scaffold (--wasm,
--compile-command) skip the lint and are exported with positionalAbi: true: their params and inputs bind by metadata POSITION, so never reorder
such a package's metadata entries.
Declarations, in one place
The signatures in the table above are the whole code-first grammar; the 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, which sets the request grid every other input aligns to), 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. color_by,width_by, andshape_wherename a DIFFERENT declared output; an output cannot color, widen, or gate itself.colorsneeds at least two entries besidecolor_by;widthsandwidth_bygo together.- A celled input (
<class>.cells) requiresmax_cells; scalar-feed knobs (interval,side,tenor, ...) are refused on it;bookrequiresblock_size.
Declaring a string slot, a renderer, a drawing, or a celled input stamps
the derived sheet abi_version: "wrun-2"; scalar-only declarations keep
deriving the first ABI. range, box, and segment are ABI-neutral:
they never flip the sheet. The derived sheet records generated_from: "declarations" plus a source_digest, and is DERIVED state from then on:
om wrun source set refuses it with wrun_source_set_generated, and the
agent's metadata argument refuses with wrun_metadata_generated. A
workspace with no declarations stays metadata-first, the only mode for
languages without an extractor. The field-by-field sheet dialect, pins,
odds modes, and metric composition are in
Script definition; the styling
ladders, fills, and style knobs (hand-written sheets only) are in
Styling.
The cell channel (wrun-2)
Modules that declare abi_version: "wrun-2" may read celled inputs: a
variable-length block of f64 cells per bar (a volume profile's per-price
rows, a book snapshot's levels) beside the scalar argument block. A celled
input is declared in the sheet (cellType: "array" + max_cells) or in
the source (input(name, <class>.cells, { max_cells })), reads a celled
source class (Data sources), and
receives cells in fixed-width TUPLES per class: volume_profile =
[low, high, buy, sell] (4 f64s), book = [price, size, side] (3
f64s). max_cells counts tuples.
The scaffold generates one accessor family per celled input (no scalar
accessor: the input's slot in the scalar block holds NaN), callable during
state() only:
in_<input>_cells(): i32: f64 cells in this bar's block (tuples x tuple width);0for a present, empty block;-1when the bar carries no block.in_<input>_read(ptr: i32): i32: copies the block into the module's exported memory atptr; returns bytes written,0for a zero-cell block,-1for a missing block.in_<input>_max_cells: i32: the declaredmax_cells(source tuples).in_<input>_capacity: i32: the f64 count to preallocate (max_cellsx the class's tuple width); generated when the class's width is known.
Cells are IEEE 754 f64, little-endian, 8 bytes each, contiguous in block
order. max_cells is a contract, not a hint: preallocate
in_<input>_capacity f64s (a StaticArray<f64> sized by it allocates
once at module start), and a bar whose block exceeds the cap refuses the
WHOLE evaluation by name (wrun_cell_block_too_large; a block is never
truncated). A call outside state() traps by name, an out-of-bounds ptr
is refused naming the sizes, and a read of an input not declared
cellType: "array" is refused naming the declared set. Underneath the
accessors sit two host imports in the wrun namespace, wrun_arg_len(index)
and wrun_arg_bytes(index, ptr); positional literals on them are a
scaffold build error exactly like getFloat(0), so scaffold source goes
through the accessors and only --wasm / --compile-command builds call
the imports directly. A second-ABI module that reads no celled input is
legal: such packages may be scalar-only, and scalar behavior is
bit-identical across every contract.
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 (a replayed block would double-count
volume). Celled inputs never abstain a row: warm-up stays the module's
state() decision. A celled class can never be the primary input, may pin
symbol + exchange together, and never pins interval.
Summing a bar's volume profile (total volume = buy + sell in every
[low, high, buy, sell] tuple):
{
"id": "ref-profile-sum",
"abi_version": "wrun-2",
"warmup_bars": 1,
"params": [],
"inputSources": {
"close": { "source": "ohlcv", "field": "close" },
"profile": { "source": "volume_profile" }
},
"inputs": [
{ "index": 0, "name": "close" },
{ "index": 1, "name": "profile", "cellType": "array", "max_cells": 512 }
],
"outputs": [{ "index": 0, "name": "total", "plot": "line", "panel": "lower" }]
}import { in_close, in_profile_capacity, in_profile_cells, in_profile_read } from "./gen/inputs";
import { emitRow, out_total } from "./gen/outputs";
const cells = new StaticArray<f64>(in_profile_capacity);
let total: f64 = NaN;
export function init(): void {}
export function state(): i32 {
in_close(); // the scalar block still carries every scalar input
const n = in_profile_cells();
if (n < 0) return 0; // this bar carries no block
total = 0;
if (n > 0) {
in_profile_read(i32(changetype<usize>(cells)));
for (let i = 0; i + 3 < n; i += 4) {
total += cells[i + 2] + cells[i + 3]; // buy + sell per [low, high, buy, sell] tuple
}
}
return 1;
}
export function finalize(): void {
out_total(total);
emitRow();
}
export function reset(): void {
total = NaN;
}The first ABI is frozen: its import allowlist never grows, and a wrun-1
package that imports the cell functions is refused naming the way in
(declare abi_version: "wrun-2"). Which celled classes this daemon's data
plane serves, and the named refusals for the rest, are in
Data sources. Where celled packages
run: alerts, om metric get / om metric series, and chart previews
evaluate them; backtests and screens refuse them by name
(wrun_celled_metric_unsupported).
The string channel (wrun-2)
Second-ABI sheets may declare string_slots: numbered, named, byte-capped
text channels written once per ready bar during finalize(). Slots are
NOT outputs: outputs stays numeric-only and every output is still a
metric; strings exist so renderers and drawing labels can carry text, and
they never become metrics.
When the sheet declares slots (or the source declares string(...)), the
scaffold generates src/gen/strings.ts:
sb_clear(),sb_text(s),sb_int(value),sb_f64(value, decimals)build one UTF-8 line allocation-free into a shared buffer sized to the largest declaredmax_bytes(allocated once at module start, so per-bar string work allocates nothing).str_<slot>(s: string)encodes and sendssto that slot;str_<slot>_sb()sends the built line. Both pass the REQUIRED byte count, so a line over the slot'smax_bytesrefuses host-side by name; strings are never truncated.
A slot not written that bar is ABSENT, which is distinct from a written
empty string: render.text draws every present slot (empty included),
render.label keeps the last present NONEMPTY one, and render.table
waits for a row where every cell is present. Writing the same slot twice
in one finalize() replaces its bytes. The channel is finalize()-only:
the underlying import (wrun_output_str(slot, ptr, len), wrun
namespace) traps by phase anywhere else, and positional literals on it are
the same scaffold build error as the cell imports (use the generated
senders). Host-enforced caps, each a named refusal: max_bytes per slot
(at most 4096), 64 slots per package, 64 KiB of string bytes per row,
8 MiB per run. Bytes must be valid UTF-8; a broken sequence refuses the
evaluation instead of landing as a replacement character.
An RSI with two slots: a readout the label renderer keeps on the newest bar, and a zone word written only on bars that are in a zone, so the text mark stays absent everywhere else:
import { input, line, lower, none, ohlcv, output, param, render, string, time } from "./sdk/declare";
import { in_bar_t, in_close } from "./gen/inputs";
import { emitRow, out_rsi, out_tag_x } from "./gen/outputs";
import { p_period } from "./gen/params";
import { sb_clear, sb_f64, sb_int, sb_text, str_readout_sb, str_zone } from "./gen/strings";
import { Rsi } from "./sdk/ta";
param("period", 14, { min: 2, max: 200 });
input("close", ohlcv.close);
input("bar_t", time.bar_open_sec);
output("rsi", line, lower, { color: "#38bdf8" });
output("tag_x", none, lower, { description: "Bar time in epoch seconds, the label's x" });
string("readout", { max_bytes: 32 });
string("zone", { max_bytes: 16 });
// One label, riding the newest bar whose readout slot was written.
render.label("rsi_tag", { x: "tag_x", y: "rsi", text: "readout", color: "#38bdf8", size: 11 });
// One text mark per bar whose zone slot was written; quiet bars leave it absent.
render.text("rsi_zone", { y: "rsi", text: "zone", color: "#f59e0b", size: 10 });
let rsi = new Rsi(14);
let period: i32 = 14;
let value: f64 = NaN;
let barTime: f64 = NaN;
export function init(): void {
period = i32(p_period());
rsi = new Rsi(period);
}
export function state(): i32 {
barTime = in_bar_t();
value = rsi.update(in_close());
return isNaN(value) ? 0 : 1;
}
export function finalize(): void {
out_rsi(value);
out_tag_x(barTime);
sb_clear();
sb_text("RSI ");
sb_int(period);
sb_text(": ");
sb_f64(value, 1);
str_readout_sb(); // "RSI 14: 63.2", at most 32 bytes
if (value >= 70.0) str_zone("overbought");
else if (value <= 30.0) str_zone("oversold");
emitRow();
}
export function reset(): void {
rsi.reset();
value = NaN;
barTime = NaN;
}Renderers and drawings are selected after the run under one lifecycle both hosts implement (Plotting and Drawing objects); the worked table example in Script definition writes four slots per bar through this module surface.
Language cheat
The file is AssemblyScript: TypeScript syntax over fixed-width numbers,
compiled to a module with no filesystem, no network, and no allocation
after init(). What the kScript (legacy) reference lists as data types,
math, strings, control flow, and user functions maps like this:
| Need | Form |
|---|---|
| Numbers | f64 for every param, input, and output; i32 for counts, periods, and loop bounds; bool for flags. Casts are explicit: i32(p_period()), f64(count). |
| Missing value | NaN, tested with isNaN(x); write NaN to an output for "nothing here", or return 0 from state() to abstain the row. Infinity in an output is refused by name. |
| Windows and history | a StaticArray<f64> sized once (from a param's max) and written as a ring buffer; there is no close[1], keep yesterday's value in a module-level variable. |
| Collections | StaticArray<T> and Array<T> allocated at module start or in init(), never in state(); Map<K, V> for keyed state, sized up front. |
| Math | Math.abs, Math.max, Math.min, Math.sqrt, Math.pow, Math.exp, Math.log, Math.floor, Math.ceil, Math.round, Math.trunc, Math.sign, the trigonometric set, Math.PI, Math.E, all over f64. |
| Strings | only in string slots; build with sb_text / sb_int / sb_f64, never + on a string per bar (that allocates). |
| Control flow | if / else, for, while, break, continue, switch on integers; a number is not a truth value (if (value > 0.0), not if (value)). |
| Functions | function name(x: f64, n: i32): f64 { ... } at module scope, typed params and return; closures cannot capture locals, so a reducer is a named function over a buffer. |
| Types | class Zone { top: f64 = NaN; bottom: f64 = NaN; alive: bool = false; }, constructed in init(); a type alias for readability. |
| Time | input("bar_t", time.bar_open_sec) gives epoch seconds; sessions are integer math on it (UTC). |
| Colors | never computed: declared per output, box, segment, or renderer (hex, rgb(), hsl(), or a named color where a fill is not involved), or chosen per bar through a color_by ladder. |
A rolling highest-high that shows the shapes above (a buffer sized once, a typed helper function, explicit casts):
import { input, line, ohlcv, output, overlay, param } from "./sdk/declare";
import { in_high } from "./gen/inputs";
import { emitRow, out_highest } from "./gen/outputs";
import { p_bars } from "./gen/params";
param("bars", 20, { min: 1, max: 500, description: "Lookback in bars" });
input("high", ohlcv.high);
output("highest", line, overlay, { color: "#38bdf8" });
const MAX_BARS: i32 = 500; // the param's max: the buffer is sized once, never per bar
const highs = new StaticArray<f64>(MAX_BARS);
let n: i32 = 20;
let cursor: i32 = 0;
let count: i32 = 0;
let highest: f64 = NaN;
// A plain function: typed params, a typed return, no closure over locals.
function maxOf(values: StaticArray<f64>, len: i32): f64 {
let best = -Infinity;
for (let i = 0; i < len; i++) {
if (values[i] > best) best = values[i];
}
return best;
}
export function init(): void {
n = i32(p_bars());
}
export function state(): i32 {
highs[cursor] = in_high();
cursor = (cursor + 1) % n;
if (count < n) count += 1;
if (count < n) return 0;
highest = maxOf(highs, n);
return 1;
}
export function finalize(): void {
out_highest(highest);
emitRow();
}
export function reset(): void {
cursor = 0;
count = 0;
highest = NaN;
}Sheet fields, by name
Where each top-level field of wrun/metadata.json is explained:
| Field | Meaning | Page |
|---|---|---|
id, name, description, overlay | the sheet's short name, display strings, the legacy placement hint | Script definition |
abi_version | wrun-1 (absent means this) or wrun-2; the sole ABI authority | Script definition |
wasm_sha256, warmup_bars | the build stamp installs verify; the warm-up hint the fetch planner reads | Script definition |
params[] | {name, default, required?, min?, max?, description?}; a style binding makes a style-only knob | Styling |
inputSources{}, inputs[] | sources keyed by input name; {index, name, description?, cellType?, max_cells?} | Data sources |
outputs[] | {index, name, plot?, panel?, unit?, displacement_bars?, ...styling} | Plotting |
fills[], ranges[] | shaded band between two rendered outputs; banded range with edges and a ladder | Styling |
boxes[], segments[] | per-bar shapes over outputs (16 each) | Drawing objects |
string_slots[], renderers[], drawings[] | the second ABI's text and decoration vocabulary | Script definition |
generated_from, source_digest | provenance of a sheet derived from declarations | this page |
The full validation list, every refusal by path, is at the end of Script definition; the messages you will actually meet, with their fixes, are in Common errors.