Declarations and the sheet

View as MarkdownOpen the editor

A wrun indicator declares what it reads, writes and draws as top-level statements at the top of its file, and Run derives the sheet from them: the description of the indicator the chart draws from and a published indicator carries.

This page is the grammar of those statements, the rules the editor enforces, the sheet's fields and provenance, and the runtime contract the sheet records. What each declaration does on the chart is on the page that owns it, and every signature with every option is one table on Quick reference.

param

param(name, default, { required?, min?, max?, description? }) is a number field, read from onStart() on through p_<name>(). param.<kind>(name, default, options?) is a typed setting (int, number, bool, choice, color, time, price, range, multi, list, source, timeframe, symbol, session, text), each naming the control the dialog draws, its literal default and its reader: Setting kinds. param.text (one line) and param.text_area (several) take words, read once in onStart() through pt_<name>() (Text settings). The options, and which kind takes each: Options on a setting. market.tick_size(), market.price_precision(), market.kind(), market.point_value(), market.zone(), market.quote_is_usd(), chart.interval_sec(), chart.bg_color(), chart.fg_color(), chart.up_color(), chart.down_color() and chart.grid_color() declare hidden settings the host writes before onStart(), 0 where it cannot know one (Chart context).

input

input(name, source.field, options?) declares one per-bar number, read in onBar() through in_<name>(); the chart's own candle needs no line (bar.close() and the other bar.*() readers), and the first input is the bar grid. The sources, their fields and knobs: Data sources. The pins: symbol with exchange for another market (Multi-source); interval with view, views, offset and bars for a coarser leg (Multi-timeframe); missing for what a bar with no observation reads. input(name, <class>.cells, { max_cells, ... }) declares a celled input, a block of tuples per bar. No input reads another indicator's outputs.

output

output(name, plot?, panel?, options?) declares a per-bar number, written in onBar() with out_<name>(value); an output not written that bar stays NaN and draws nothing there. plot is line, bar, area, histogram, candle, shape, scatter or none (data-only: computed, never drawn); panel is overlay (the price pane) or lower (a pane below it), per output. The options are style keys and presentation keys: Plotting, Styling. The styling round's words ride the same literal: the plot-kind looks (step, smooth, gradient, split with up_color, down_color, split_level, split_fill and split_base, fill_color, fill_opacity, fill_gradient, gradient_mode, fill_color_by plus fill_colors, fill_color_packed_by, base, grading, stack, candle_style, border_colors, wick_colors, border_width, shape, location, char, font_family, fill, role, align, pill_style, font_size, each refused by name off the plot kinds that take it), z (-10..10, the paint order), pane (a declared pane's name or "lower"), color_packed_by (a packed rgba output per bar), displacement_bars_by ({ param, scale?, offset? }), and the presentation keys decimals (0..8) and signed (both need format, whose words are price, %, si, int, 0, 0.0, 0.00, 0.000, usd and auto; unit prints beside one) and axis_name; every one reaches the sheet only (Plotting). Four more ride the same way: show_price_display (the output's price-axis tag under display({ price_display: "per_output" })), show_price_display_by (an output whose value switches that tag per run), omit_if_empty (no legend row or tag when no bar drew) and barcolor: true on a none output (its value picks the candle tint from colors, or color_packed_by names it) (Styling). output(...) returns a handle; bind it with a top-level const when a box, segment, hover or alert needs to name it.

string

string(name, { max_bytes, description? }) declares a text slot, written per bar in onBar() through str_<name>(text) or the sb_* builder and str_<name>_sb(); a slot not written that bar is absent. Slots feed renderers, drawings, blocks and the Console, never a plot (Strings and text). A slot named exactly debug is the indicator's debug log in the editor's Console (Debugging).

render and draw

render.text, render.label, render.table, render.shape, render.stats_row, render.bgcolor and render.barcolor place text, marks and tints over outputs and slots; render.legend adds a legend entry and render.hud a card. draw.line, draw.box, draw.polyline and draw.label with a name as the first argument each declare one drawing, placed from the newest bar. Numeric references name outputs; text and table cells name string slots. render.table also takes the look keys and per-cell styles (Styled tables), and render.shape takes width, the mark's size in pixels (Mark size). Renderers: Plotting; legend and cards: Legend, HUD and hover cards; drawings and handles: Drawing objects.

Since the styling round, render.text and render.label take style from eight words (plain, price_label, pill, callout, badge, box, knockout, emblem) and the look keys: align and valign on plain text; label_position, background_color, border_color and corner_radius on a tag (a tag key on plain text, or align on a tag, is refused); font_weight, font_family, emblem_shape, emblem_color and size_by on both; a text renderer's background_color_by plus background_colors, color_by plus colors or color_packed_by, and panel; a label's padding. A render.label pins to a corner with position (one of the nine anchors) and offset ([x, y], each -200..200 px) instead of x and y (declaring both is refused; price_label and callout are refused there). render.shape adds location (absolute, above_bar, below_bar, top, bottom), glow, char (one character, with and only with shape: "char"), font_family, fill, fill_opacity and tooltip; render.stats_row adds color, colors, color_by, color_packed_by and priority (1..3); render.bgcolor adds width (0.5..10, a vertical line per gated bar) and line_style; render.hud takes its surface, type and look words. Every draw.* declaration takes its handle kind's look words in snake_case: extend, arrow, glow, glow_color, sticky_right, axis_label, opacity and tooltip on a line; border_color, border_width, border_style, corner_radius, shape, gradient, gradient_direction, text (a string slot), text_color, font_size, font_weight, font_family, align, valign and padding on a box, whose color is the fill; fill_color, closed and smooth on a polyline; style, background_color, max_width, angle, emblem_shape and emblem_color on a label. Every one is a presentation key, stripped before the compiler, and since the styling round every render.text, render.label, render.shape and draw.* declaration is erased whole (Plotting, Drawing objects).

box, segment and range

box(name, { top, bottom, ... }) and segment(name, { yFrom, yTo, ... }) declare per-bar shapes over output HANDLES; range(upper, lower, options?) declares a band between two rendered outputs (ranges never dedupe: repeat the declaration for several bands); its edge_width is 0..10 (0 for no edge lines) and it also takes legend (false drops the band's legend row), label, z, fill (false draws the two edge lines with no interior), show_price_display (the band's tags) and omit_if_empty (no legend row or tag when no bar had both edges). fill(a, b, options?) shades the interior between two drawn outputs on one pane with no edge lines (color, opacity 0..1 with default 0.25, color_by plus colors, color_packed_by, z; only a top-level fill( statement is read, a .fill( method on a handle is something else), and pane(name, options?) declares a pane of the indicator's own, at most four (title, place "below" or "price", height_frac 0.05..0.6, scale "linear", "log", "percent" or "indexed", invert, padding [top, bottom], min, max, format, decimals, signed, unit), joined by an output's pane: "<name>". A box also takes borderStyle, z and the colour ladders colorBy plus colors, colorPackedBy, borderColorBy plus borderColors and borderColorPackedBy (output handles for the By keys; a ladder half without the other is refused). All of these are erased before the compiler. What the chart draws: Drawing objects, Styling.

alert

alert(name, { when, message?, description?, text?, every_bar?, title? }) declares a signal the chart's alert dialog offers: when is the handle of the output whose false-to-true edge fires it (a data-only none output works, so no 0/1 line has to be drawn), message the fire text with placeholders such as {{symbol}}, {{close}} and {{<output>}} (a declared output's value on the fired bar), description the picker's sub-line, both at most 200 characters. text names a string slot: the words the file writes into it on the fired bar are the message, sent as one plain line of at most 200 characters. every_bar: true fires on every bar when holds, once per bar at most; the default fires on the false-to-true edge only (Alerts). title is the words the dialog, an armed alert and its notification show for the signal, any characters, 1 to 120 of them; the name keeps letters, digits, ., _ and -. The declaration adds nothing to the module. A worked signal: Alerts.

Layout words

page(title), section(title, { toggle?, collapsed?, when? }), divider() and note(text) lay the settings dialog out in source order; presets({ Name: { param: value } }) ships named settings sets; legend({ title }) sets the legend's title template. Sheet-only, compiled to nothing: Pages, sections, dividers, notes, Presets, Legend. display({ axis?, price_display?, overlay?, mount_order? }), at most once, sets the indicator's look on the chart as a whole: axis: false gives it no price axis of its own (an indicator on the price pane rides the chart's), price_display: "per_output" tags each plot on the price axis by its own show_price_display, overlay: "offchart" homes it in its own pane below the chart, and mount_order: "first_value" lists its plots in the order they first draw (Styling).

Other words

hover(handle, [block.*]) is an output's hover card (HUD and hover cards); handles.* sets handle defaults (Drawing objects); frame, panel.*, plot.levels, draw.ladder, draw.feed, draw.meter and out.inset declare JSON snapshots and the panels and widgets drawn from them (Cards, frames and panels); strategy({ ... }) places orders through the engine's broker (Strategies overview). plot.heatmap, plot.footprint, plot.tpo, plot.profile and plot.matrix declare the price canvases and out.grid a block of data-only outputs a heatmap reads (Price canvases); chart.contrast_guard(false) and chart.stack_handles(true) are top-level flags with one boolean literal each (a binding is refused): the first keeps the author's colours as written on a light chart, the second stacks the indicator's corner-anchored handle groups below other indicators' groups (Colors).

A file that uses them

Two settings, two inputs, three outputs and the debug slot, with the hooks that read and write them (the bar loop in full: Execution model):

// One output in its own pane (lower), labelled with a unit.
param("period", 14, { min: 2, max: 200, description: "RSI Period" });
// An on/off setting is a 0/1 param.
param("show_raw", 1, { min: 0, max: 1, description: "Write the raw RSI as well as the smoothed one" });
// The first input is the bar grid the funding input is aligned to; bar.close() reads through it.
input("close", ohlcv.close);
// A funding input, aligned to the close's rows; the chart serves it as a percent.
input("funding", funding.rate_close, { description: "Funding rate in percent, 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 in percent, on the price pane" });
// The debug log: a string slot named debug, printed in the editor's Console.
string("debug", { max_bytes: 32 });

let rsi = new Rsi(14);
const ema = new Ema(5);
let showRaw: bool = true;

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

function onBar(): void {
  const value = rsi.update(bar.close());
  const fundingRate = in_funding();
  if (isNaN(value)) return;
  const smoothed = ema.update(value);
  out_rsi(showRaw ? value : NaN);
  out_smoothed(smoothed);
  out_funding_pct(fundingRate);
  sb_clear();
  sb_text("rsi ");
  sb_f64(value, 2);
  str_debug_sb();
}

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

The declaration grammar

A declaration is a top-level statement of your file: a call to one of the words above with a string-literal name, a literal default and a literal options object, nothing imported. The build reads them without running the code, derives the sheet, and generates the readers and writers (p_*, pb_*, in_*, out_*, str_*, sb_*, fb_*) from the same object, so the two cannot disagree. A file with onBar() and no output(...) is refused at Run. A file that exports init, state, finalize or reset is the four-function form; both forms derive the same sheet (The four-function form).

wrun
param("period", 14, { min: 2, max: 200, description: "Lookback window" });
param.bool("show_raw", true, { label: "Raw line" });
input("close", ohlcv.close);
input("btc_close", ohlcv.close, { symbol: "BTCUSDT", exchange: "BINANCE_FUTURES" });
output("value", line, lower, { unit: "score", label: "Score", format: "0.00" });

Bindings

"@<param>" as the value of color, an entry of colors or line_style on an output or a text, label or legend renderer binds that spot to a param.color (for line_style, a param.choice over solid, dashed, dotted); as interval or symbol on an input it binds the pin to a param.timeframe or a param.symbol; a param.source declares the input it reads. The build writes the setting's default in the reference's place before the compiler runs and records the binding on the sheet (style_targets, interval_param, symbol_param, field_param), so the module's bytes never depend on a setting (The Style page, Picks, lanes, the cap). The same reference works on a range, a fill, a box and a segment (their colours, palette entries, gradient stops and line or border styles), on an output's other colour words (fill_color, fill_colors, fill_gradient, gradient, up_color, down_color, border_colors, wick_colors), and on the style keys of a plot.levels profile, where a word key takes a param.choice over that key's words, a number key a param.number or param.int whose range lies inside the key's, and a boolean key a param.bool. A data-only output refuses a paint binding (a barcolor output, which paints the candles, takes one), and so do a draw.* object, a handles.* default, a panel series and a tile. Every colour reference also takes "@<param>/<alpha>", a panel series colour excepted: the setting's default at that alpha (a decimal from 0 to 1, replacing the colour's own) lands in the reference's place, as rgba(r, g, b, a) where the colour takes rgba() (an output's color and colors, a renderer's colours but a stats row's, a legend entry, a range's colours and gradient, a fill's color, a box's color and borderColor, a segment's color, a mini-chart grid's colours) and as #rrggbbaa where it takes a colour word, and the binding records the alpha, so a recolour keeps the translucency. ticks_per_bar: "@<param>" on a volume_profile.cells input binds the profile's bucket size to a param.int within 1..500: the sheet carries its default and ticks_per_bar_param, and the chart fetches again when it changes.

Sheet-only keys

An output's presentation keys, a renderer's style, tooltip, hover and badges, the plot-kind looks, z and pane, an input's ticks_per_bar, currency and a profile's missing, and the whole of hover, range, fill, pane, box, segment, alert, display, the chart flags, every plot.* canvas, the layout words, render.legend, render.hud and (since the styling round) every render.text, render.label, render.shape and draw.* declaration reach the sheet only: the build strips them before the compiler runs, so a declaration with them compiles to the same bytes as one without. A const bound to a string(...) or output(...) is a handle for naming, dropped the same way.

Rules every declaration follows

Each is a named build error on the declaration's line:

  • Names are string literals; defaults and option values are literals; the code never runs at build time.
  • Names may carry capitals. Params, inputs, string slots and frames are stored lowercase, with every option and {{placeholder}} that names one; outputs keep their spelling; each accessor also answers to the file's spelling. Two names of one family that differ only by case are refused; boxes, segments and alerts keep their spelling, so Zone and zone are two boxes.
  • Declarations are top-level statements; 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 accessors stay name-attached. A candle field read through bar.<field>() is served by a declared input of exactly that feed, else appended after the declared inputs under its own name, in the order open, high, low, close, volume, bar_t; a file with no input(...) line gets close as slot 0.
  • Box and segment coordinates, a section's toggle and when, a hover target and an alert's when are output handles bound by a top-level const (let, var and export const bind too); the sheet records the output's name, never the handle. A string literal where a handle goes, or an unbound handle, is refused.
  • color_by, width_by and shape_where name a DIFFERENT declared output; colors goes with color_by and widths with width_by, each half without the other refused.
  • A typed setting's options belong to its kind; when names a declared param.bool; a "@<param>" reference names a setting of the kind the spot needs; a preset sets sheet params only. A setting cannot take a name the chart keeps (symbol, exchange, interval, transformations, ticksPerBar, currency, runMode, devViewerTier and the overlay's own keys), start with __style__, or collide with a name another setting derives (band_lo, rth_tz, offset_unit, market_tick_size).
  • The expanded sheet holds at most 128 params (a range counts two, a session three, a list its max + 1, a unit list one more); every other cap is on Limits.
  • Names share one namespace across outputs, boxes, segments, renderers, drawings and alerts.
  • A celled input requires max_cells (candles gets the larger of bars and 2 when it is left out) and book requires block_size; scalar-feed knobs are refused on a celled input.

The sheet Run derives

When you press Run, the editor derives the sheet from the declarations, checks it against the schema, and compiles; a refusal prints in the Console on the declaration's line, and Run stops there. There is no hand-written sheet. Read it at the Console prompt (sheet) and in Copy for LLM; a published indicator carries it. The sheets on this page omit the provenance fields every derived sheet carries (generated_from, source_digest, and wasm_sha256 once the module compiles). These declarations, in a tab titled "Context Skeleton":

wrun
param("period", 14, { min: 2, max: 200, description: "Lookback bars" });
input("close", ohlcv.close);
input("btc_close", ohlcv.close, { symbol: "BTCUSDT", exchange: "BINANCE_FUTURES", description: "Fixed BTC reference market" });
input("funding_rate", funding.rate_close);
output("value", line, lower, { unit: "score" });
output("raw", none);
output("lagging", line, lower, { displacement_bars: -26 });

derive every top-level field a scalar sheet has:

json
{
  "id": "context-skeleton",
  "name": "Context Skeleton",
  "params": [
    {
      "name": "period",
      "default": 14,
      "min": 2,
      "max": 200,
      "description": "Lookback bars"
    }
  ],
  "inputSources": {
    "close": { "source": "ohlcv", "field": "close" },
    "btc_close": {
      "source": "ohlcv",
      "field": "close",
      "exchange": "BINANCE_FUTURES",
      "symbol": "BTCUSDT"
    },
    "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",
      "panel": "lower",
      "displacement_bars": -26
    }
  ]
}
FieldWhat Run writes
id, namefrom the editor tab's title (Context Skeleton derives context-skeleton and Context Skeleton); name is shown in lists, and publishing names the package @yourname/<name> (Publishing)
abi_versionthe runtime contract, picked from the declarations you use; ABSENT means "wrun-1" (below)
generated_from, source_digest"declarations", and the sha256 of the source the sheet came from
wasm_sha256the compiled module's sha256, stamped when Run compiles it
params{name, default, required?, min?, max?, description?} with unique names, in declaration order; values reach the module positionally in that order
inputSourceskeyed BY INPUT NAME: the source, the field and the options the declaration set, an interval always as the long word (FOUR_HOURS)
inputs{index, name, description?}, indexes contiguous from 0 in declaration order; index 0 is the PRIMARY input, which follows the chart's market and interval (a pin on it is refused; an odds input is the exception); a time source cannot be primary
outputs{index, name, description?, plot?, panel?, unit?, displacement_bars?, ...style, ...presentation}, indexes contiguous from 0, unique names; plot: "" is a data-only output (declared none); displacement_bars draws the value written at bar i at bar i + displacement_bars, the module never shifting a row

The chart reads no warm-up field: the bars where onBar() writes nothing are the warm-up. The sheet is DERIVED state, serialized canonically (an unchanged source rewrites nothing): edit the declaration, never the sheet.

Typed settings on the sheet

A typed setting adds its type (int, number, boolean, select, color, time, price, multi, symbol, text) and the dialog's keys (label, step, group, row, hint, when, hide, slider, confirm; options and option_labels on a select, multi_bits on a multi, unit and unit_default beside a unit menu); a composite expands to its members (range / range_end, list / list_index / list_end, session / session_end, unit_for); a host-applied setting carries host and a symbol its symbol_default; a binding records style_targets (and style_values on a line-style choice); a text setting carries max_bytes, multiline and its words as text_default (its default is 0); market.* and chart.* carry host_fill and hidden. None of these change what reaches the module: one number per sheet param, a text setting's number being its words' byte count, with the words on the text channel. The declarations below, in a tab titled "Typed Skeleton":

wrun
page("Signal");
param.int("period", 14, { min: 2, max: 200, label: "Length" });
param.choice("kind", ["Simple", "Exponential"], "Simple");
const show = param.bool("show_band", true);
section("Band", { toggle: show });
param.range("band_pct", [0.5, 2.0], { min: 0, max: 10, step: 0.1, label: "Band width %" });
param.color("band_color", "#94a3b8");
input("close", ohlcv.close);
output("value", line, overlay, { label: "Average", format: "price" });
output("band_hi", line, overlay, { color: "@band_color", label: "Band", legend: false });
presets({ Tight: { band_pct_lo: 0.2, band_pct_hi: 1 } });

derive six sheet params from five settings (the range is two), the color's default written onto the output it paints plus the binding on the param, the layout, and the preset:

json
{
  "id": "typed-skeleton",
  "name": "Typed Skeleton",
  "params": [
    {
      "name": "period",
      "default": 14,
      "min": 2,
      "max": 200,
      "type": "int",
      "step": 1,
      "label": "Length"
    },
    {
      "name": "kind",
      "default": 0,
      "type": "select",
      "options": [0, 1],
      "option_labels": ["Simple", "Exponential"]
    },
    {
      "name": "show_band",
      "default": 1,
      "min": 0,
      "max": 1,
      "type": "boolean"
    },
    {
      "name": "band_pct_lo",
      "default": 0.5,
      "min": 0,
      "max": 10,
      "type": "number",
      "step": 0.1,
      "label": "Band width %",
      "range": "band_pct",
      "range_end": "lo"
    },
    {
      "name": "band_pct_hi",
      "default": 2,
      "min": 0,
      "max": 10,
      "type": "number",
      "step": 0.1,
      "label": "Band width %",
      "range": "band_pct",
      "range_end": "hi"
    },
    {
      "name": "band_color",
      "default": 9975030760,
      "type": "color",
      "style_targets": [{ "output": "band_hi", "property": "color" }]
    }
  ],
  "inputSources": { "close": { "source": "ohlcv", "field": "close" } },
  "inputs": [{ "index": 0, "name": "close" }],
  "outputs": [
    {
      "index": 0,
      "name": "value",
      "plot": "line",
      "panel": "overlay",
      "label": "Average",
      "format": "price"
    },
    {
      "index": 1,
      "name": "band_hi",
      "plot": "line",
      "panel": "overlay",
      "color": "#94a3b8",
      "label": "Band",
      "legend": false
    }
  ],
  "layout": [
    { "kind": "page", "title": "Signal" },
    { "kind": "param", "name": "period" },
    { "kind": "param", "name": "kind" },
    { "kind": "param", "name": "show_band" },
    { "kind": "section", "title": "Band", "toggle": "show_band" },
    { "kind": "param", "name": "band_pct_lo" },
    { "kind": "param", "name": "band_color" }
  ],
  "presets": [
    { "name": "Tight", "values": { "band_pct_lo": 0.2, "band_pct_hi": 1 } }
  ]
}

Underneath, the module still receives six numbers in this order: the length, the choice's index, 1 or 0 for the toggle, the two ends of the range, and the packed color.

Pins on the sheet

A pin lands on the input's inputSources entry as declared; one the user can change carries the setting's default plus interval_param or symbol_param, a profile's bucket size ticks_per_bar_param, and a param.source input field_param. A volume profile declared missing: "empty" carries the word too, and so does a trades input's currency. The chart checks every pin before anything is fetched and refuses one it cannot serve by name. What each source may pin, and what a leg, a view, an offset or bars reads: Multi-timeframe; another market and the missing policy: Multi-source.

Liquidations first, read as 0 on quiet bars, with the close carried beside them (the schema refuses missing: "carry" on the first input): input("liqs", liquidations.liquidations, { missing: "zero", description: "Total liquidations, 0 on quiet bars" }), input("close", ohlcv.close, { description: "Close, carried" }) and output("liq_ratio", histogram, lower), in a tab titled "Dense Liquidations", derive:

json
{
  "id": "dense-liquidations",
  "name": "Dense Liquidations",
  "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"
    },
    { "index": 1, "name": "close", "description": "Close, carried" }
  ],
  "outputs": [
    { "index": 0, "name": "liq_ratio", "plot": "histogram", "panel": "lower" }
  ]
}

A fixed BTC reference on its own 4h candles, as of close: input("btc_4h", ohlcv.close, { symbol: "BTCUSDT", exchange: "BINANCE_FUTURES", interval: "4h", description: "BTC on its own 4h grid, as of close" }) beside input("close", ohlcv.close) and output("ratio", line, lower), in a tab titled "BTC 4h Context", derive the sheet below, the interval spelled FOUR_HOURS. On a 4h chart the pin equals the chart's interval and reads BTC row by row; on a coarser chart it is finer than the chart, and Run refuses it.

json
{
  "id": "btc-4h-context",
  "name": "BTC 4h Context",
  "params": [],
  "inputSources": {
    "close": { "source": "ohlcv", "field": "close" },
    "btc_4h": {
      "source": "ohlcv",
      "field": "close",
      "exchange": "BINANCE_FUTURES",
      "symbol": "BTCUSDT",
      "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" }]
}

An odds input names its own Polymarket market by condition id (0x + 64 hex) in symbol and may be the first input (Data sources). This sample pins the placeholder market the chart's prediction starters ship, so Run refuses it until you paste your market's condition id into symbol:

param("period", 5, { min: 1, max: 200, description: "Momentum lookback, bars" });
// The market is its condition id (0x + 64 hex): paste yours over this placeholder.
input("yes_odds", odds.close, { symbol: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", outcome: "YES", description: "Polymarket YES probability" });
output("momentum", line, lower, { description: "Rate of change of the YES probability" });

let roc = new Roc(5);

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

function onBar(): void {
  out_momentum(roc.update(in_yes_odds()));
}

Boxes and segments (per-bar shapes)

Two top-level arrays, derived from box(...) and segment(...), legal on every contract: they add nothing to the module, and the chart evaluates them on EVERY bar row. The sheet records output NAMES under snake_case fields: boxes (at most 16), each {name, top, bottom, x_from?, x_to?, when?, panel?, color?, border_color?, opacity?, border_width?, border_style?, z?, color_by?, colors?, color_packed_by?, border_color_by?, border_colors?, border_color_packed_by?} (the _by keys name declared outputs whose per-bar value picks an entry of colors or border_colors, 1..64 colours each; the _packed_by keys an output carrying a packed colour per bar), and segments (at most 16), each {name, y_from, y_to, x_from?, x_to?, when?, panel?, color?, width?, line_style?}. The coordinates name declared outputs (data-only allowed); x_from / x_to are bar offsets, a literal (whole or fractional: a fraction places the edge inside its bar, at that share of the bar's interval) or the name of an output whose truncated per-bar value is the offset; when names a gate output (the bar is skipped unless its value is finite and nonzero). Every refusal names the field (boxes.0.top). The defaults and what the chart draws: Drawing objects.

Declared alerts

alerts (at most 64), derived from alert(name, { when, message?, description?, text?, every_bar?, title? }), each {name, when, message?, description?, text?, every_bar?, title?}: when names a declared output (any plot, data-only "" included), and the alert fires on the bar that output turns from false to true (true is finite and nonzero; a NaN row is false and re-arms it), or on every bar it holds when every_bar is true; text records the slot's name. Legal on every contract; nothing new reaches the module. The golden-cross declarations on Alerts, in a tab titled "Golden Cross", derive:

json
{
  "id": "golden-cross",
  "name": "Golden Cross",
  "params": [
    {
      "name": "fast",
      "default": 21,
      "min": 1,
      "max": 200,
      "description": "Fast EMA length"
    },
    {
      "name": "slow",
      "default": 55,
      "min": 2,
      "max": 400,
      "description": "Slow EMA length"
    }
  ],
  "inputSources": { "close": { "source": "ohlcv", "field": "close" } },
  "inputs": [{ "index": 0, "name": "close" }],
  "outputs": [
    {
      "index": 0,
      "name": "fast",
      "description": "Fast EMA of the close",
      "plot": "line",
      "panel": "overlay",
      "color": "#2563eb"
    },
    {
      "index": 1,
      "name": "slow",
      "description": "Slow EMA of the close",
      "plot": "line",
      "panel": "overlay",
      "color": "#f97316"
    },
    { "index": 2, "name": "golden_cross", "plot": "" }
  ],
  "alerts": [
    {
      "name": "golden_cross_up",
      "when": "golden_cross",
      "message": "{{symbol}} golden cross at {{close}}",
      "description": "Fast EMA crosses above slow EMA"
    }
  ]
}

Settings layout, presets, presentation

Four more top-level keys, written only when the file declares them and read by the chart alone: layout, presets, legend_title and presentation. Legal on every contract; none reaches the module.

KeyShape
layoutthe dialog in source order: {kind: "page", title}, {kind: "section", title, toggle?, collapsed?, when?}, {kind: "divider"}, {kind: "note", text}, {kind: "param", name} (a composite once, under its first member's name; a hidden market.* param takes no entry); absent when the file uses no layout word and no group option, and the dialog keeps one flat list
presets`[{name, values: {param: number
legend_titlethe legend({ title }) template
presentation{legend?: [...], hud?: [...]} from render.legend and render.hud, each entry with the keys of its declaration, the tiles in the block shape an output's hover uses

A presentation.hud entry carries its surface, type and look words as declared (width, offset, z, background_color, corner_radius, font_family, look, accent_color, chrome, safe_area, mobile and the rest). Two more top-level arrays and two flags travel the same way: panes (from pane(...), each {name, title?, place?, height_frac?, scale?, invert?, padding?, min?, max?, format?, decimals?, signed?, unit?}, at most 4), fills (from fill(...), each {between: [a, b], color?, opacity?, color_by?, colors?, color_packed_by?, z?}), contrast_guard (boolean, default true) and stack_handles (boolean, default false).

What the dialog draws from each: Pages, sections, dividers, notes, Presets, Legend, HUD and hover cards; the Style page is derived from the outputs, with no key of its own (The Style page).

ABI versions: the five contracts

Run writes abi_version from the declarations you use, and the module is validated and run under that contract and no other. The first contract is FROZEN: published indicators on it keep running bit-identically. Each later contract is ADDITIVE over the one before, the same entry points and scalar block plus one channel family, and freezes in turn. range, box, segment and alert are contract-neutral: declaring one never flips the sheet; so are fill, pane, the chart flags and the cell-fed canvases (plot.heatmap, plot.footprint, plot.tpo, plot.profile), which ride whichever contract their input already needs.

ContractStamped byAdds
"wrun-1" (the field absent)scalar-only declarationsparams, inputs, outputs
"wrun-2"a celled input, a string slot, a renderer or a declared drawingthe cell and string channels, the renderer and drawing vocabulary (below)
"wrun-3"a handle, strategy(...) or a bar.isLast() callthe handle-keyed draw channel, the strategy channel, the last-bar flag (Drawing objects, Strategies overview)
"wrun-4"a frame (frame(...), panel.*, plot.levels, plot.matrix, a draw.ladder or draw.feed HUD)the frame channel (Cards, frames and panels)
"wrun-5"a text setting (param.text, param.text_area)the text channel (wrun_param_len, wrun_param_bytes): a text setting's words, handed over before the first bar (Text settings)
"wrun-6"a bar.count() readthe bar count (wrun_bar_count): how many bars the run holds, so a bar can tell whether a later bar exists (Execution model)

The second contract's sheet additions, as Run derives them from input("profile", volume_profile.cells, { max_cells: 256 }) and input("book", book.cells, { max_cells: 1000, block_size: 10 }) in a tab titled "Orderflow Context":

json
{
  "id": "orderflow-context",
  "name": "Orderflow Context",
  "abi_version": "wrun-2",
  "params": [],
  "inputSources": {
    "close": { "source": "ohlcv", "field": "close" },
    "profile": { "source": "volume_profile" },
    "book": { "source": "book", "block_size": 10 }
  },
  "inputs": [
    { "index": 0, "name": "close" },
    { "index": 1, "name": "profile", "cellType": "array", "max_cells": 256 },
    { "index": 2, "name": "book", "cellType": "array", "max_cells": 1000 }
  ],
  "outputs": [
    { "index": 0, "name": "imbalance", "plot": "line", "panel": "lower" }
  ]
}

inputs[].cellType: "array" marks a celled input, and max_cells beside it counts the class's TUPLES (the cap the chart enforces: an oversize bar refuses the run, never truncates); block_size and max_depth are recorded, and the chart serves the book it has stored as it is. A celled input keeps its positional slot in the scalar block (reading NaN), can never be the primary input, and joins the chart's rows exactly: a bar with no observation is a PRESENT EMPTY block (0 cells, not -1), never a carried value and never a withheld row. The classes and each block's rules: Data sources, Multi-timeframe.

String slots, renderers, drawings

Outputs stay numbers; the second contract adds three top-level arrays, derived from string(...), render.* and draw.*, that turn numbers and per-bar text into chart decorations. Numeric coordinates name declared OUTPUTS, text fields name declared STRING SLOTS, and the two namespaces never substitute for each other.

ArrayEntries
string_slots (at most 64){index, name, max_bytes, description?}, indexes contiguous from 0, unique names; a slot not written that bar is ABSENT, distinct from a written empty string
renderers (at most 64){kind, name, ...} with the keys of the matching render.* declaration, kind one of text, label, table, shape, stats_row, bgcolor, barcolor
stats_strip (beside a stats_row renderer){grading?, label_side?, theme?}, the strip's look in its words (Plotting); a param.choice paints a key it holds through style_targets and style_values
drawings (at most 64){kind, name, ...} with the coordinate outputs of the matching draw.* declaration, kind one of line, box, polyline, label; x values are epoch SECONDS

What each renderer draws and which row wins: Plotting; a declared drawing is evaluated on the LAST row only: Drawing objects. The expanded render selection must stay under 2 MiB per run, refused as wrun_render_result_too_large. A 2x2 session-stats table plus a box around the session's price range. The box's top and bottom are drawn as lines on the price pane: a package with a drawn overlay output is homed there, and a declared drawing follows the pane of its y output (top for a box), so the box frames the candles while range_pct keeps its own pane below. Data-only outputs never decide a pane, so the two x coordinates, bar times, stay none. The four cells are rewritten every bar so the table shows the last complete row:

output("range_pct", line, lower, { unit: "%", description: "Session range as a percent of its low" });
// The box's coordinates, read by name. Its top and bottom are drawn on the price pane:
// a drawn overlay output homes the package there, and the box follows its `top` output's
// pane, so it frames the candles rather than the percent line. The two times stay data-only.
output("left", none);
output("top", line, overlay, { color: "#f59e0b", width: 1, description: "Session high so far, the box top" });
output("right", none);
output("bottom", line, overlay, { color: "#f59e0b", width: 1, description: "Session low so far, the box bottom" });
// The table's four cells: byte-capped string slots rewritten every bar.
string("close_label", { max_bytes: 16 });
string("close_text", { max_bytes: 32 });
string("range_label", { max_bytes: 16 });
string("range_text", { max_bytes: 32 });
render.table("session_stats", { rows: 2, cols: 2, cells: ["close_label", "close_text", "range_label", "range_text"], position: "top_right" });
draw.box("session_zone", { left: "left", top: "top", right: "right", bottom: "bottom", color: "#f59e0b" });

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

function onBar(): void {
  const close = bar.close();
  if (isNaN(high) || close > high) high = close;
  if (isNaN(low) || close < low) low = close;
  const t = bar.time();
  if (isNaN(left)) left = t;
  right = t;
  if (isNaN(low) || low <= 0.0) return;
  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();
}

Because every coordinate output is finite on the last bar, the box exists with createdBar = that bar; a module that wants a drawing REMOVED writes NaN to a coordinate output on the newest bar. Decorations never change the numbers the outputs carry.

Validation, in one list

When Run checks the derived sheet, the schema refuses, naming the path (the Console puts a param, input, output or string-slot refusal on the declaration it came from). The caps, each with its message, are on Limits; the messages with their fixes are in Common errors. Beyond the caps and the rules above, the schema refuses:

FamilyRefused
Paramsduplicate names; a type outside the nine words; a composite with a member missing or doubled; hide without when; unit_default outside unit; a multi default past its bits; a when or unit_for naming no param; a style_targets output that is not drawn; unset_at_default on a target of a setting that is not a param.color, or on a non-colour target; a stats_strip target the strip does not hold, or painted by anything but a choice over the key's words; a preset or presentation name used twice
Inputsnon-contiguous or duplicate indexes and names; an input without a source entry, or the reverse; a time primary; a lone pin half; outcome or binding outside odds; a condition id that is not 0x + 64 hex; a required knob missing (tenor, side, token, fund, publisher, series) or a knob on a source that does not take it; a delta other than 5, 15, 25 or 35; fund: "all" on etf_premium; a missing value outside carry, nan and zero, missing: "carry" on the primary input, or any missing on a celled or time source; unknown fields per source
Celled inputscellType without the second contract or without max_cells; max_cells without cellType; a celled class under a scalar input, as the primary input, or with a field or an interval pin (intrabar and candles excepted); block_size or max_depth anywhere but book
Outputs and rangesnon-contiguous or duplicate indexes and names; a format outside the ten, decimals or signed without format; a line_style or edge_line_style outside solid, dashed and dotted; a block whose output or slot does not exist; an output badging itself; a style reference to an output that does not exist; range sides equal or not rendered; a range color_by without colors (bare colors is legal: the band sign palette)
Boxes, segments, alertsa coordinate, offset or when gate naming no declared output; a panel outside overlay and lower
String slots, renderers, drawingsany of the three under the first contract; non-contiguous or duplicate slot indexes and names; a reference to an output or string slot that does not exist; a bgcolor, barcolor or shape ladder half without the other; a bgcolor or barcolor where naming no declared output; a text or label style outside the eight, a tag key on plain text or align on a tag, a corner label with x and y beside offset; a HUD position outside the nine anchors or its columns outside 1 and 2, a HUD look, chrome or tile draw outside its list
Styling wordsa theme. word outside the seven; a unit past 8 characters beside a format on a pane, panel, level, card, ladder or canvas (an output's unit stays unbounded); a corner_radius outside 0..32, a font_weight or font_family outside their words; a look word on a plot kind that does not take it (step on a bar, gradient beside color_by, split without both colours, stack beside base, char without shape: "char"); a pane naming no declared pane, a declared pane with nothing drawn in it or more than 4, a fill whose sides sit on two panes, a z outside -10..10; a box or fill ladder half without the other, or a packed key beside its _by key