---
title: "Quick reference"
description: "The wrun kit on one screen, then the parts of it that are easiest to forget: the two hooks, the generated accessors (the p_ / in_ / out_ families plus the cell…"
order: 129
section: "reference"
---

<!-- source: docs/indicators/reference/quick-reference.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# Quick reference

The wrun kit on one screen, then the parts of it that are easiest to
forget: the two hooks, the generated accessors (the `p_` / `in_` /
`out_` families plus the cell and string families and the bar's own
fields), every declaration signature, the fifteen setting kinds and the
block kit, the sources the chart serves and their fields, the TA classes,
and the kit modules. This page lists what a wrun indicator
declares, because an indicator says what it reads and writes and the
chart does the calling.

## One screen

### The hooks

A file is its declarations at the top, then module state, then one or two
plain functions (no `export`, nothing to import):

| Hook | Runs | Do this here | Never here |
| --- | --- | --- | --- |
| `onStart(): void` (optional) | once per evaluation, before any bar | read params through `p_<param>()`, size buffers, construct TA objects | read an input (every `in_` reader and `bar.*()` return NaN before the first bar) or write an output (a write here never reaches a row) |
| `onBar(): void` (required) | once per bar, oldest first | read inputs through `in_<input>()`, `bar.*()` and the cell readers, update module state, write outputs through `out_<output>(value)`, string slots, frames, handles and orders; the row is emitted when it returns | allocate (size buffers at module start or in `onStart()`); `emitRow()`: the build sends the row once, after `onBar()` returns, and a call of your own does nothing |

Every bar emits a row: an output `onBar()` does not write is NaN on that
bar and draws nothing there, so warm-up is a `return` before the writes,
or a NaN written as it is. There is no fourth function to write: the chart
replays the forming bar from a snapshot of the module's memory.

### Accessors (generated from your declarations each time the editor compiles, in scope with nothing to import)

| Accessor | Phase | Meaning |
| --- | --- | --- |
| `p_<param>(): f64` | `onStart()` on | the param's value: its default, or the setting the chart user chose |
| `pb_<param>(): bool`, `p_<range>_lo()` / `_hi()`, `p_<list>(): f64[]`, `p_<session>_start()` / `_end()` / `_tz()`, `p_<param>_unit()`, `pt_<param>(): string` | `onStart()` on | a typed setting's readers: a toggle, a range's ends, a list's filled slots, a session's parts, a unit menu's code, a text setting's words ([Setting kinds](../settings/kinds.md)) |
| `in_<input>(): f64` | `onBar()` | this bar's scalar input; NaN on a celled input's slot |
| `bar.open()`, `bar.high()`, `bar.low()`, `bar.close()`, `bar.volume()`, `bar.time()`, `bar.isLast()`, `bar.count()` | `onBar()` | the chart's own candle, its open in epoch seconds, whether this is the newest bar, and how many bars the run holds; no `input` line needed, reading a field adds its input to the sheet |
| `out_<output>(value: f64): void` | `onBar()` | writes one output; the row is emitted when `onBar()` returns, NaN in every output it did not write |
| `in_<input>_cells(): i32` | `onBar()` | f64 cells in this bar's block: `0` for a present empty block, `-1` when the bar carries no block (and before the first bar) |
| `in_<input>_view(): StaticArray<f64>` | `onBar()` | the build's own buffer holding this bar's block, no copy; only the first `in_<input>_cells()` values belong to this bar |
| `in_<input>_read(ptr: i32): i32` | `onBar()` | copies the block into module memory at `ptr`, for code that owns its own buffer; 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 of the build's buffer (`max_cells` x the class's tuple width) |
| `sb_clear()`, `sb_text(s)`, `sb_int(n)`, `sb_f64(x, decimals)` | `onBar()` | build one UTF-8 line into a shared buffer, allocation-free |
| `str_<slot>(s: string)`, `str_<slot>_sb()` | `onBar()` | send a whole string, or the built line, to one slot |

### Declarations (top-level statements of your file)

| Declaration | Signature |
| --- | --- |
| Param | `param(name, default, { required?, min?, max?, description? })`, a number field |
| Typed setting | `param.int`, `param.number`, `param.bool`, `param.choice`, `param.color`, `param.time`, `param.price`, `param.range`, `param.multi`, `param.list`, `param.source`, `param.timeframe`, `param.symbol`, `param.session` `(name, default, { required?, min?, max?, description?, label?, step?, group?, row?, hint?, when?, hide?, slider?, unit?, unit_default?, confirm? })`, `tz?` on a session; `market.tick_size()`, `market.price_precision()`, `market.kind()`, `market.point_value()`, `market.zone()`, `market.quote_is_usd()` |
| Settings layout | `page(title)`, `section(title, { toggle?, collapsed?, when? })`, `divider()`, `note(text)`, `presets({ Name: { param: value } })`, `legend({ title })` |
| Setting bindings | `"@<param>"` as `color`, a `colors` entry or `line_style` on an output or renderer (a `param.color`, a line-style `param.choice`), as `interval` or `symbol` on an input (a `param.timeframe`, a `param.symbol`) |
| Input | `input(name, <source>.<field>, { symbol?, exchange?, interval?, bars?, view?, views?, offset?, side?, tenor?, delta?, fund?, asset?, publisher?, series?, venue?, token?, outcome?, missing?, description? })` |
| Celled input | `input(name, <class>.cells, { max_cells, interval?, bars?, symbol?, exchange?, block_size?, max_depth?, venue?, expiries?, description? })`; `max_cells` may be left out on `candles` |
| Output | `output(name, plot?, panel?, { description?, unit?, color?, colors?, width?, opacity?, line_style?, glow?, corner?, color_by?, shape_where?, displacement_bars?, width_by?, widths?, label?, format?, legend?, visible?, price_line?, axis_label?, tooltip?, hover?, badges?, hint? })`, returns an `OutputHandle` |
| Hover card | `hover(handle, [block.value(label, output, { format?, delta?, tooltip?, hint? }), block.spark(label, output, { bars?, color? }), block.gauge(label, output, { min, max, format? }), block.pill(label, slot, { color_by?, colors? }), block.rows(label?, [[label, output or slot, format?]]), block.meter(label, output, { min, max, marks? }), block.chips(slot or ladder)])`; the same list inline as an output's `hover` |
| Range | `range(upper, lower, { color?, colors?, color_by?, edge_width?, edge_line_style?, smooth?, gradient?, gradientMode?, legend?, label?, z? })`; `edge_width: 0` keeps the band and drops the edge lines |
| Fill | `fill(upper, lower, { color?, opacity?, opacity_by?, color_by?, colors?, color_packed_by?, z? })`, the interior only between two drawn outputs on one pane; `opacity_by` an output whose value is each bar's opacity |
| Pane | `pane(name, { title?, place?, height?, scale?, invert?, padding?, min?, max?, format?, decimals?, signed?, unit? })`, at most 4; an output names it with `pane` |
| Box | `box(name, { top, bottom, from?, to?, when?, panel?, color?, borderColor?, opacity?, borderWidth?, borderStyle?, z?, colorBy?, colors?, colorPackedBy?, borderColorBy?, borderColors?, borderColorPackedBy? })` |
| 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?, style?, align?, valign?, label_position?, background_color?, background_color_by?, background_colors?, border_color?, corner_radius?, font_weight?, font_family?, emblem_shape?, emblem_color?, size_by?, color_by?, colors?, color_packed_by?, panel?, tooltip?, hover?, badges? })`, `render.label(name, { x, y, text, color?, size?, style?, position?, offset?, align?, valign?, background_color?, border_color?, corner_radius?, padding?, font_weight?, font_family?, emblem_shape?, emblem_color?, size_by?, tooltip?, hover?, badges? })` (a corner label takes `position` and `offset` and no point), `render.table(name, { rows, cols, cells, position?, ...look, styles? })` ([Styled tables](../presentation/cards-frames-panels.md#styled-tables)), `render.shape(name, { output, shape, where?, color?, color_by?, colors?, width?, location?, glow?, fill?, fill_opacity?, char?, font_family?, tooltip? })`, `render.stats_row(name, { output, title?, format?, polarity?, color?, colors?, color_by?, color_packed_by?, priority?, visible? })`, `render.bgcolor(name, { where, color?, color_by?, colors?, width?, line_style? })`, `render.barcolor(name, { where, color?, color_by?, colors? })` |
| Legend and HUD | `render.legend(name, { text?, value?, format?, color?, color_by?, colors? })`, `render.hud(name, { position, title?, columns?, tiles: [tile.value(...), tile.spark(...), tile.gauge(...), tile.pill(...), tile.rows(...), tile.meter(...), tile.chips(...), tile.rings(...)], look?, width?, offset?, z?, chrome?, texture?, text_glow?, title_case?, label_case?, accent_color?, accent_color_by?, accent_colors?, background_color?, background_opacity?, background_gradient?, gradient_direction?, border_color?, border_width?, border_style?, corner_radius?, padding?, shadow?, opacity?, text_color?, title_text_color?, font_family?, safe_area?, mobile? })` |
| Drawings | `draw.line(name, { x1, y1, x2, y2, color?, width?, line_style?, extend?, arrow?, glow?, glow_color?, sticky_right?, axis_label?, opacity?, tooltip? })`, `draw.box(name, { left, top, right, bottom, color?, border_color?, opacity?, border_width?, border_style?, corner_radius?, extend?, shape?, gradient?, gradient_direction?, glow?, glow_color?, text?, text_color?, font_size?, font_weight?, font_family?, align?, valign?, padding?, tooltip? })`, `draw.polyline(name, { points, color?, width?, line_style?, opacity?, fill_color?, closed?, smooth?, arrow?, glow?, glow_color?, tooltip? })`, `draw.label(name, { x, y, text, color?, style?, align?, valign?, background_color?, border_color?, border_width?, corner_radius?, font_weight?, font_family?, padding?, max_width?, angle?, glow?, glow_color?, emblem_shape?, emblem_color?, sticky_right?, axis_label?, opacity?, tooltip? })` |
| Widgets | `draw.card(name, { title, anchor?, offset?, z?, state_by?, rows, headline?, rule?, rule_color?, show_state?, state_colors?, ...look })`, `draw.feed(name, { frame, anchor?, offset?, z?, title?, time_format?, ...look })`, `draw.meter(name, { label, fraction, ramp, text?, anchor?, offset?, z?, title?, bar_height?, track_color?, ...look })`, `draw.ladder(name, { frame, side, divider?, title?, width_frac?, offset?, opacity?, color?, labels?, format?, decimals?, signed?, unit?, text_color?, font_size?, font_family?, font_weight?, divider_color?, panel? })`; the look is `chrome`, `stripe`, `controls`, `accent_color`, `title_text_color`, `title_font_size`, `title_font_weight`, `background_color`, `background_opacity`, `background_gradient`, `gradient_direction`, `border_color`, `border_width`, `border_style`, `corner_radius`, `padding`, `width`, `opacity`, `font_size`, `font_family`, `font_weight`, `text_color`, `label_text_color`, `above_drawings`, `safe_area`, `panel` |
| Frames and panels | `frame(name, { max_bytes? })`; `plot.levels({ name, frame, dock, width_frac?, poc?, labels?, color?, span?, ...style })`; `panel.bars` / `line` / `scatter` / `histogram` / `pie` / `heatmap` / `table` / `tiles({ name, title, x, place, frame, series?, ...style })`; `plot.matrix({ name, frame, dock?, columns?, ...style })`; `out.inset(name, { dock, height_px?, shape?, color?, colors?, color_by?, opacity? })` |
| Price canvases | `plot.heatmap({ name, cells | grid, value?, price_low?, price_step?, ...style })`, `out.grid(name, { rows })`, `plot.footprint({ name, cells, ...style })`, `plot.tpo({ name, cells?, period?, letter_minutes?, ...style })`, `plot.profile({ name, cells, span?, session?, start?, end?, ...style })` |
| Handles | `handles.line`, `handles.box`, `handles.label`, `handles.polyline({ anchor?, safe_area?, ...the drawing's keys })`, the defaults for handles the module creates in `onBar()` |
| Chart | `chart.contrast_guard(false)` (keep the author's colours on a light chart), `chart.stack_handles(true)` (keep clear of another indicator's anchored handles), `chart.up_color()`, `chart.down_color()`, `chart.grid_color()`, `chart.bg_color()` (the chart's colours as packed numbers, read in `onStart()`) |
| Alert | `alert(name, { when, message?, text?, every_bar?, description? })`, `when` an output handle bound by a top-level `const`, `text` a string slot ([Alerts](../functions/alerts.md)) |

Vocabulary the declarations take:

| Slot | Values |
| --- | --- |
| `plot` | `line`, `bar`, `area` (filled down to zero), `histogram`, `candle` (four in a row draw one candle series: open, high, low, close), `shape`, `scatter` (one mark per value), `none` (data-only: computed, never drawn) |
| `panel` | `overlay` (the price pane), `lower` (a pane below the chart) |
| `line_style` / `lineStyle` / `edge_line_style` | `solid`, `dashed`, `dotted` |
| `format` (outputs, panes, panels, levels, cards, ladders, canvases, blocks, `{{name:format}}`) | `price` (the chart's price digits), `%`, `si`, `int`, `0`, `0.0`, `0.00`, `0.000`, `usd` (`$1.2M`), `auto` (six significant digits with thousands separators); a card row or headline also takes `pct`; `decimals` (0..8), `signed` and `unit` (up to 8 characters on the new surfaces; unbounded on an output, as it always was) refine a declared format |
| `style` (labels) | `plain`, `price_label`, `pill`, `callout`, `badge`, `box`, `knockout`, `emblem`, on `render.text`, `render.label`, `draw.label` and `handles.label`; a drawing or handle label takes `box` for the tag look, never `price_label`, and a corner label takes neither `price_label` nor `callout` |
| `position` (tables, HUD, corner labels) and `anchor` (cards, feeds, meters, anchored handles) | `top_left`, `top_center`, `top_right`, `middle_left`, `middle_center`, `middle_right`, `bottom_left`, `bottom_center`, `bottom_right` |
| `font_family`, `font_weight`, `valign` | `ui`, `mono`, `serif`, `rounded` (system stacks, nothing downloads); `normal`, `medium`, `bold`; `top`, `middle`, `bottom` |
| `gradient_direction`, `corner_radius`, `opacity` | `vertical`, `horizontal`; 0..32 px; 0..1, multiplying every colour's own alpha |
| `location` (marks) | `absolute`, `above_bar`, `below_bar`, `top`, `bottom` |
| `chrome` | a panel: `box`, `grid`, `none`; a card, feed or meter: `state`, `plain`; a HUD frame: `card`, `none`, `brackets`, `rules`, `tag`, `title_bar`, `window`, `cover` |
| `look` (HUD) | `default`, `glass`, `stage`, `signal`, `terminal`, `broadsheet`, `chart_desk`, `dial`, `grid`, `cockpit`, `phosphor`, `classic`; a word declared beside it wins |
| `param.timeframe` default | `chart`, `1m`, `5m`, `15m`, `30m`, `1h`, `4h`, `1d`, `1w` |
| `param.source` default | `ohlcv.open`, `.high`, `.low`, `.close`, `.volume`, and the blends `.hl2`, `.hlc3`, `.ohlc4`, `.hlcc4` |
| `market.kind()` | `p_market_kind()`: 1 crypto, 2 stock or ETF, 3 forex, 4 metal, 5 index, 6 economic series, 0 unknown |
| `market.quote_is_usd()` | `p_market_quote_is_usd()`: 1 US dollars, 2 another currency or coin, 0 unknown |
| `market.zone()` | `p_market_zone()`: the market's zone as its `tz` index below (0 UTC, also unknown) |
| `unit` | `price`, `ticks`, `%`, `atr` (codes 0, 1, 2, 3 in `p_<param>_unit()`) |
| `tz` (a session) | `UTC`, `America/New_York`, `America/Chicago`, `Europe/London`, `Europe/Berlin`, `Asia/Tokyo`, `Asia/Hong_Kong`, `Asia/Singapore`, `Australia/Sydney`, `Asia/Kolkata`, `America/Los_Angeles`, `America/Toronto`, `America/Mexico_City`, `America/Sao_Paulo`, `America/Argentina/Buenos_Aires`, `Europe/Paris`, `Europe/Amsterdam`, `Europe/Zurich`, `Europe/Madrid`, `Europe/Rome`, `Europe/Stockholm`, `Europe/Oslo`, `Europe/Copenhagen`, `Europe/Warsaw`, `Europe/Helsinki`, `Europe/Athens`, `Europe/Istanbul`, `Europe/Moscow`, `Asia/Jerusalem`, `Asia/Riyadh`, `Asia/Dubai`, `Africa/Johannesburg`, `Asia/Karachi`, `Asia/Bangkok`, `Asia/Jakarta`, `Asia/Ho_Chi_Minh`, `Asia/Kuala_Lumpur`, `Asia/Shanghai`, `Asia/Taipei`, `Asia/Manila`, `Asia/Seoul`, `Pacific/Auckland` (42 zones; each with its offset and daylight rule on [Sessions and units](../settings/sessions-and-units.md)) |
| `shape` (renderer) | `circle`, `cross`, `triangle_up`, `triangle_down`, `diamond`, `arrow_up`, `arrow_down`, `flag`, `square` |
| `missing` | `carry` (the default: the latest value carries forward), `nan`, `zero`: what a bar with no observation of its own reads |
| `side` | `BUY`, `SELL` |
| `tenor` | `ONE_W`, `ONE_M`, `TWO_M`, `THREE_M`, `SIX_M`; required on `implied_volatility` and `skew` |
| `delta` | `5`, `15`, `25` (the default), `35`; `skew` only |
| `fund` | an ETF ticker (`IBIT`, `FBTC`, `ETHA`, ...) or `all` (the sum over every fund listed for the chart's coin); required on `etf_flow`, `etf_holdings` and `etf_premium`, and `etf_premium` takes one ticker, never `all` |
| `venue` | on `options_chain`: `auto` (the default), `deribit`, `cme`, `binance`, `okx`, `bybit`, `bullish`, `derive`; on `options_oi` and `options_volume`: `deribit` (the default), `binance` |
| `publisher`, `series` | the publisher's upper-case id (`FRED`, `US_TREASURY`, `ECB`, ...) and that publisher's series id (`DGS10`); `economic` only, both required |
| `asset` | an upper-case ticker (`BTC`); `treasury_balance` only, the chart's coin when absent |
| `token` | the token's display name (`Bitcoin`, `Ethereum`, `Solana`), never its ticker; `token_supply` only, required |
| `outcome` | `YES`, `NO` (`NO` on the `close` field only) |
| `interval` (a pin) | `MINUTE`, `FIVE_MINUTES`, `FIFTEEN_MINUTES`, `THIRTY_MINUTES`, `HOUR`, `FOUR_HOURS`, `DAY`, `WEEK` (or `1m`, `5m`, `15m`, `30m`, `1h`, `4h`, `1d`, `1w`); chart only, the custom timeframes `TWO_MINUTES`, `THREE_MINUTES`, `TEN_MINUTES`, `FORTY_FIVE_MINUTES`, `TWO_HOURS`, `SIX_HOURS`, `EIGHT_HOURS`, `TWELVE_HOURS`, `THREE_DAYS` (or `2m`, `3m`, `10m`, `45m`, `2h`, `6h`, `8h`, `12h`, `3d`) |
| `view` (with an interval pin) | `confirmed` (the default), `forming`, `is_new_period`; `views` lists the extra ones as `"forming,is_new_period"`; `offset: N` writes the `offset` view for you |
| `offset` (with an interval pin) | `1` to `500`: the pinned candle N candles before the confirmed one, `NaN` until N + 1 have closed |
| `bars` (an interval pin or `candles`) | `1` to `5000`: how many of the pin's own candles to reach back from the newest; it only deepens the fetch |
| Box and segment offsets | literals in -500..500, whole or fractional (a fraction places the edge inside its bar), or an output handle whose per-bar value truncates to the offset |
| Colors | any string on outputs, segments, renderers and drawings; a box fill needs hex, `rgb()`, or `hsl()`; every style word this reference lists on panels, levels, cards, feeds, meters, ladders, HUDs and canvases takes `#rrggbb`, `#rrggbbaa` or a theme token; the seven tokens `theme.up`, `theme.down`, `theme.text`, `theme.muted`, `theme.bg`, `theme.grid`, `theme.accent` pass on every colour key and frame colour (a colour param's default and presets excepted) and follow the chart's theme at paint time |

### Settings

Fifteen kinds, each a top-level `param.<kind>(name, default, options?)`:
the control the dialog draws for it, and the reader `onStart()` calls (it
answers from then on) ([Setting kinds](../settings/kinds.md)).

| Kind | The dialog draws | `onStart()` reads |
| --- | --- | --- |
| `param.int("length", 20, { min: 2, max: 500 })` | a number field that steps by 1 | `p_length()`, a whole number |
| `param.number("mult", 2.0, { min: 0.5, max: 5, step: 0.1 })` | a number field that steps by `step` | `p_mult()` |
| `param.bool("show_bands", true)` | an on/off toggle | `pb_show_bands()`, a `bool` |
| `param.choice("kind", ["Simple", "Exponential"], "Simple")` | a menu of the labels | `p_kind()`, the picked index (0 for the first label) |
| `param.color("basis_color", "#2962ff")` | a color picker | `p_basis_color()`, the color packed into one number; most files bind it to an output with `color: "@basis_color"` instead |
| `param.time("since", "2024-01-01 00:00")` | a date and time field with **Pick**: the next click on the chart sets it | `p_since()`, epoch seconds |
| `param.price("floor", 0.0)` | a number field with **Pick**: the next click on the chart sets the price | `p_floor()` |
| `param.range("band_pct", [0.5, 2.0], { min: 0, max: 10 })` | one slider with two handles | `p_band_pct_lo()` and `p_band_pct_hi()` |
| `param.multi("days", ["Mon", "Tue", "Wed"], ["Mon", "Wed"])` | a chip per option up to four options, a multi-select menu past that | `p_days()`, a bitmask (bit 0 is the first option); `multiHas(mask, i)` tests one |
| `param.list("lookbacks", [5.0, 20.0], { max: 4 })` | an editable list of numbers with **Add**, up to `max` long | `p_lookbacks()`, an `f64[]` of the filled slots |
| `param.source("src", ohlcv.close)` | a menu of `open`, `high`, `low`, `close`, `hl2`, `hlc3`, `ohlc4`, `volume`, `hlcc4` | nothing: it declares the input too, and `in_src()` reads the picked field per bar |
| `param.timeframe("htf", "chart")` | a menu of `chart`, `1m`, `5m`, `15m`, `30m`, `1h`, `4h`, `1d`, `1w` | nothing: `{ interval: "@htf" }` on an input pins that input to the pick |
| `param.symbol("pair", "BINANCE_FUTURES:ETHUSDT")` | a market button that opens the symbol search | nothing: `{ symbol: "@pair" }` on an input pins that input to the pick |
| `param.session("rth", "09:30-16:00", { tz: "America/New_York" })` | a day strip with the window shaded, a start and an end on the 24-hour clock, and a zone menu | `p_rth_start()` and `p_rth_end()` (minutes from midnight), `p_rth_tz()` (the zone's index) |
| `param.text("label", "Session average", { max_bytes: 64 })` | a text field (a text area for `param.text_area`) | `pt_label()`, the words as a `string` |

Every kind takes one options object, every key optional
([Options on a setting](../settings/options.md)). The words that lay the
dialog out are on [Pages, sections, dividers, notes](../settings/layout.md), the
named sets of values on [Presets](../settings/presets.md), the per-output
rows the dialog adds by itself on [The Style page](../settings/style-page.md),
the session zones and the unit codes on
[Sessions and units](../settings/sessions-and-units.md), where a pick
applies and the 128-setting cap on
[Picks, lanes, the cap](../settings/picks-and-lanes.md), and every refusal
on [What the build checks](../settings/checks.md).

### Presentation

One kit of seven blocks with two homes: the card the cursor opens
(`block.<kind>`, on a line, its legend entry, a HUD tile, a text or label
mark) and a HUD card at one of the nine anchors (`tile.<kind>`). Every
block reads the newest row; outputs and slots are named by a bound handle
or by their name as a string ([Blocks and tiles](../presentation/hud-and-hover-cards.md#blocks-and-tiles)).

| Block or tile | Renders in | Options |
| --- | --- | --- |
| `value(label, output)` | a hover card as `block.value`, a HUD card as `tile.value`: the output's newest value under its label, a signed delta from the `delta` output beside it | `format`, `delta`, `tooltip`, `hint`, `color` or `color_by` + `colors`, `headline` (one per card, spanning every column), `font_size` |
| `spark(label, output)` | both: a sparkline of the output's last bars (64 at most) | `bars`, `color` or `color_by` + `colors`, `draw` (`line`, `area`, `dots`, `bars`, `text`), `height` (16..120) |
| `gauge(label, output)` | both: a dial between `min` and `max` | `min` and `max` (required), `format`, `color` or `color_by` + `colors`, `draw` (`arc`, `ring`, `dial`, `ticks`, `bar`, `blocks`, `text`) |
| `pill(label, slot)` | both: the slot's words as a chip, colored by a ladder | `color`, `color_by`, `colors`, `draw` (`capsule`, `dot`, `led`, `text`), `headline`, `font_size` |
| `rows(label?, [[label, output or slot, format?], ...], options?)` | both: rows of label and value | a `format` per row; `color` or `color_by` + `colors`, `leader` (`none`, `dots`) |
| `meter(label, output)` | both: a bar between `min` and `max` with marks | `min` and `max` (required), `marks`, `color` or `color_by` + `colors`, `draw` (`bar`, `split`, `blocks`, `segments`, `scale`, `text`), `height` |
| `chips(slot or ladder output)` | both: the slot's words, or the ladder's labels, as chips | none |
| `rings(label?, [[label, output, { min, max, color? }], ...])` | both: 1..3 concentric rings, outermost first, each a share of its own range, with a legend beside | `height` (16..120) |

The hover card is `hover: [...]` on an output, a `render.text` or a
`render.label`, or `hover(handle, [...])` at the top level
([Hover cards](../presentation/hud-and-hover-cards.md#hover-cards)); the HUD card is
`render.hud(name, { position, title?, columns?, tiles })`
([HUD cards](../presentation/hud-and-hover-cards.md#hud-cards)). The third home, the legend, takes
words rather than blocks: `legend({ title })` after the indicator's name and
`render.legend(name, { text?, value?, format?, color?, color_by?, colors? })`
per entry, with `label`, `format` and `legend: false` on an output shaping
its own entry ([Legend](../presentation/legend.md)). The `tooltip`
templates, the label `style` words and `badges` are on
[Labels, tooltips, badges](../presentation/labels-and-tooltips.md); `glow`,
`corner`, `opacity`, `width`, `line_style`, a range's gradient and the
`color_by` and `width_by` ladders are on [Styling](../presentation/styling.md).

### Sources the chart serves

| Source | Fields | Knobs and units |
| --- | --- | --- |
| `ohlcv` | `open`, `high`, `low`, `close`, `volume` | the chart's own candles; a secondary input may pin another market or a coarser interval |
| `trades` | `volume` | `side` REQUIRED: that side's volume per bar, in the base asset; `currency` `"USD"` reads it in dollars, `"Coin"` in coins (one lane per unit) |
| `funding` | `rate_close` | in percent, normalized to a one-hour rate; may pin a coarser interval |
| `oi` | `open`, `high`, `low`, `close` | in USD; may pin a coarser interval |
| `liquidations` | `liquidations` | `side` optional (absent = both sides summed); in USD; sparse, so an indicator usually declares `missing: "nan"` or `"zero"` |
| `long_short_ratio` | `total_account`, `top_trader_account`, `top_trader_position` | plain ratios of longs over shorts (a value at or below 0 reads `NaN`); the chart must be 5 minutes or coarser |
| `implied_volatility` | `implied_volatility` | `tenor` REQUIRED; Deribit's summary for the chart's coin |
| `skew` | `skew` | `tenor` REQUIRED, `delta` optional (25 when absent); Deribit's skew for the chart's coin |
| `volatility_index` | `open`, `high`, `low`, `close` | Deribit's volatility index (DVOL) for the chart's coin, in index points; BTC and ETH |
| `options_oi` | `puts`, `calls` | `venue` optional; open interest in the venue's units (contracts on Deribit, the base coin on Binance); BTC and ETH |
| `options_volume` | `puts`, `calls` | `venue` optional; volume in contracts; BTC and ETH |
| `etf_flow` | `flow_usd` | `fund` REQUIRED; the chart's coin must be BTC, ETH or SOL; daily (on an intraday chart the day's value lands on the first bar of its day); served in the browser, and an alert on an indicator that reads it is refused |
| `etf_holdings` | `holdings` | `fund` REQUIRED (a ticker or `all`); in coins of the fund's underlying; a coin with listed spot ETFs |
| `etf_premium` | `premium_rate` | `fund` REQUIRED (one ticker); in percent; daily, read as the newest value at or before the bar's day |
| `ethena_positions` | `collateral` | Ethena's collateral in the chart's coin; BTC and ETH |
| `bitfinex_funding` | `funding_size`, `credit_size`, `active_credit_size`, `margin_rate` | the three sizes in the funding currency, `margin_rate` an APR in percent; the chart's coin |
| `treasury_balance` | `balance` | `asset` optional; Binance's own balance in coins, published monthly |
| `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; daily; a one-minute chart is refused |
| `economic` | `value` | `publisher` and `series` REQUIRED; the publisher's unit; daily or slower |
| `odds` | `open`, `high`, `low`, `close` (default), `volume` | the market's condition id (`0x` plus 64 hex characters) as `symbol`, plus `outcome`; `exchange` is implied and refused; the chart has no market picker, so `binding` is refused |
| `time` | `bar_open_sec`, `trade_date`, `session` | the bar's open, its exchange trade date (epoch seconds at 00:00 UTC) and its session (`1` regular, `2` pre-market, `3` after-hours, `0` closed); no knobs, never the primary input |

Ten feeds share the `etf_flow` rule (served in the browser, and an alert
on an indicator that reads one is refused): `long_short_ratio`,
`volatility_index`, `options_oi`, `options_volume`, `etf_holdings`,
`etf_premium`, `ethena_positions`, `bitfinex_funding`, `treasury_balance`
and `economic`. The kit also declares the `tape` cells, which the chart
does not serve: a file that reads them is refused by name when it runs
("celled source class 'tape' is not served by the browser lane yet").

Pins: `symbol` and `exchange` go together (a lone half is refused), in the
chart's own ids (`{ symbol: "ETHUSDT", exchange: "BINANCE_FUTURES" }`).
The chart checks every pin before it fetches anything. A secondary `ohlcv`
input may pin another market, read bar by bar at the chart's interval, and
an interval coarser than the chart's that is a whole multiple of it, read
as of the coarser candle's close; `funding` and `oi` may pin a coarser
interval on the chart's own market. Everything else is refused by name: a
pin on the primary input, a market pin on any other source, a finer
interval, and an interval on any other feed (`trades`, `liquidations`,
`implied_volatility`, `skew`, `etf_holdings`, `economic`, ...), on `time`
or on a celled input. Two celled inputs are the exception: `intrabar`
and `candles` require an `interval` and may pin another market.
[Multi-timeframe](../core-concepts/multi-timeframe.md) has the views.

### Celled classes (`abi_version: "wrun-2"`)

| Class | Tuple | Width | Knobs | On the chart |
| --- | --- | --- | --- | --- |
| `volume_profile` | `[low, high, buy, sell]` per price bucket, ascending by price | 4 f64 | `ticks_per_bar` (1 to 500 of the market's buckets merged into one), `currency` (`"USD"`, or `"Coin"` by default) | served, history and live, on the chart's own market |
| `book` | `[price, size, side]` per level, side `+1` bid / `-1` ask | 3 f64 | `block_size` REQUIRED by the declaration | served, history and live: the chart's own order book at its own grouping, at most 500 levels a side, whatever `block_size` and `max_depth` say; bids come best first, then asks from the farthest to the best, so branch on `side`, never on position |
| `intrabar` | `[offset_ms, open, high, low, close, volume]` per closed finer bar, ascending | 6 f64 | `interval` REQUIRED (finer than the chart's and dividing it evenly, `1m` at the finest); `max_cells` at least the finer bars in one chart bar; `symbol` + `exchange` for another market | served on the chart's own market or a pinned one |
| `candles` | `[offset_ms, open, high, low, close, volume]` per closed candle of the stream's interval, oldest first | 6 f64 | `interval` REQUIRED (any word); `bars` 1 to 5000; `max_cells` filled when left out; `symbol` + `exchange` for another market; `view: "forming"` (the live candle on the live bar) or `"forming_open"` (on every bar the open of the candle holding it, `[offset_ms, open, NaN, NaN, NaN, NaN]`), each without `bars` | served: the backlog on the first bar, then each candle on the bar it closes with |
| `trade_volume_by_size` | `[bucket, buy_usd, sell_usd, buy_count, sell_count]` per USD trade-size bucket that traded (each fill at its own size), ascending; `bucket` 1 (under 1K) to 7 (10M and up) | 5 f64 | none (`max_cells` 7 holds a full bar) | served on the chart's own market; a bar with no trades is an empty block |
| `options_chain` | `[strike, expiry_ms, side, oi, gamma, delta, mark_iv, underlying, multiplier, vega]` per listed contract, side `+1` call / `-1` put | 10 f64 | `venue` (`"auto"` default, `"deribit"`, `"cme"`, `"binance"`, `"okx"`, `"bybit"`, `"bullish"`, `"derive"`), `expiries` (`"all"` default or `"nearest:N"`) | served on the LAST row only (history rows are empty blocks), a snapshot refreshed about every 30 seconds |
| `tape` | | | | not served: refused by name |

### The TA classes (`./sdk/ta`)

One class per calculation, each fed one bar at a time. Construct in
`onStart()`, `.update(...)` once per bar in `onBar()` (it returns the
primary value, `NaN` until warm). `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 for a highest high, the low for a lowest low) |
| 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](../functions/ta-library.md).

### Kit modules (`./sdk/...`)

Ten kits beside the TA classes, every name in scope with nothing to
import. Construct their classes in `onStart()`; each page lists every
export with its rules.

| Kit | Main exports | Page |
| --- | --- | --- |
| `./sdk/ta-plus` | 26 more classes: `Dev`, `PercentRank`, `Bbw`, `Kcw`, `AccDist`, `Wad`, `Wvad`, `Nvi`, `Pvi`, `Pvt`, `RunningMax`, `RunningMin`, `Mode`, `Range`, `Cog`, `Iii`, `Dema`, `Tema`, `Zlema`, `Trix`, `Ultimate`, `Vortex`, `Aroon`, `Choppiness`, `Efficiency`, `Kama` | [Extra indicators](../functions/extra-indicators.md) |
| `./sdk/stats` | `History` (`push(v)`, `ago(n)`, `max()`, `min()`, `mean()`, `sum()`); `stats.sum`, `mean`, `variance`, `stdev`, `min`, `max`, `argmin`, `argmax`, `slope`, `covariance`, `correlation`, `zscore`, `sortAscending`, `median`, `percentile` over a `StaticArray<f64>` and a count | [Stats, history and lists](../functions/stats-history-lists.md) |
| `./sdk/fmt` | `TextBuilder` (`text`, `char`, `int`, `f64`, `auto`, `price`, `pct`, `signed`, `compact`, `time`, `duration`, `spaces`); the generated `sb_*` calls wrap one (`sb_price`, `sb_pct`, `sb_signed`, `sb_compact`, `sb_time`, `sb_duration` and the rest) | [Strings and text](../functions/text-formatting.md) |
| `./sdk/clock` | `Clock` (`update(t)`, `hour`, `minute`, `weekday`, `dayOfMonth`, `month`, `year`, `isNewDay`, `isNewWeek`, `isNewMonth`, `index`, `intervalSec`, `barCloseSec`); `Session` (`update(t)`, `isIn`, `isFirst`, `closed`, `key`) | [Clock and sessions kit](../functions/time-and-sessions-kit.md) |
| `./sdk/resample` | `Resampler` (`update`, `closed`, `newBucket`, `last`, `forming`, `source`, `confirmed`, `refused`); `ClosedWindow` (`push`, `mean`, `wma`, `stdev`, `highest`, `lowest`, `sum`, `vwma`, `back`); `Smoothed` (`commit`, `value`, `peek`); `tf.*`, `field.*` | [Higher-timeframe kit](../functions/higher-timeframe-kit.md#from-the-chart-bars-resampler) |
| `./sdk/candles` | `Periods` over a `candles` stream (`load`, `confirmed(n)`, `developing`, `isNew`, `startSec`, `endSec`, `complete`), each period a `PeriodCandle` (`open`, `high`, `low`, `close`, `volume`, `vwap()`); `CandleList` (`load`, `count`, `openSec(i)`, `open(i)` ... `volume(i)`); `period.*` | [Higher-timeframe kit](../functions/higher-timeframe-kit.md#calendar-periods-and-candle-lists) |
| `./sdk/color` | `ink.*` (twenty packed colors), `fromHex`, `alpha`, `mix`, `lighten`, `darken` for handle colors; `ColorScale`, `Thresholds` for a `color_by` index | [Colors](../functions/colors-kit.md) |
| `./sdk/orderflow` | `delta`, `deltaPct`, `Cvd`, `VolumeProfile` (`poc`, `vah`, `val`), `BookImbalance`, `Absorption`, `LiquidationBurst` | [Order flow](../functions/order-flow-kit.md) |
| `./sdk/levels` | `PeriodLevels`, `SessionLevels`, `PivotPoints`, `roundLevels`, `SupportResistance` | [Levels kit](../functions/levels-kit.md) |
| `./sdk/structure` | `Swings`, `MarketStructure`, `FairValueGaps`, `OrderBlocks`, `Divergence`, `candles.*` | [Market structure kit](../functions/market-structure-kit.md) |

### Caps

16 boxes, 16 segments, 64 renderers, 64 drawings (8 cards among them, and
32 cards, feeds and meters per pane), 64 polyline points, 16 declared
alerts, 64 string slots of at most 4096 bytes, 8 frames of at most 96
KiB, 4 docked profiles, 8 panels, 4 heatmaps, 4 footprints, letter and
time-anchored profiles together, 4 matrices, 4 panes, 64 KiB of strings
per row, 2 MiB of strings and frames per run, 8 MiB of expanded render
result, 2,000 drawings per run, offsets in -500..500, 4 MiB of module
memory, 20 seconds per run. Every number, with the refusal it produces, is
in [Limits](limits.md).

## The four-function form

The module the chart runs exports four functions, `init`, `state`,
`finalize` and `reset`, with exact signatures that Run checks before
anything reaches the chart. A file with `onBar()` gets them from the
build; a file may still export the four itself, and it builds the same way
([Execution model](../core-concepts/execution-model.md#the-four-function-form)).
How the hooks map onto them:

| Export | The build's version, around a file with `onBar()` | In a file that exports it |
| --- | --- | --- |
| `init(): void` | reads every param once, then calls `onStart()` when the file declares one | runs once before any bar; the only place `p_<param>()` reads |
| `state(): i32` | reads this bar's inputs and returns `1` on every bar | runs once per bar; the only place `in_<input>()` and the cell accessors read; `0` abstains the row |
| `finalize(): void` | writes NaN to every output, calls `onBar()`, then emits the row | runs after each bar whose `state()` returned `1`: writes outputs and string slots, then `emitRow()` LAST |
| `reset(): void` | calls `onReset()` when the file declares one; otherwise nothing is reset | required: reassigns every module-level variable, `.reset()` on every TA object |

One `export` of `init`, `state`, `finalize` or `reset` makes the whole
file the four-function form. The chart replays the live forming bar from a
snapshot of the module's memory and globals taken after the last closed
bar, so every replay of the forming bar starts from the state that bar
left.

## Generated accessors

The runtime contract is positional; the accessors are how your source
stays name-attached. One function per declared name, regenerated from your
declarations each time the editor compiles:

- `p_<param>(): f64` (params, readable from `onStart()` on), plus
  `pb_<param>(): bool` on a toggle, `p_<range>_lo()` / `_hi()`,
  `p_<list>(): f64[]`, `p_<session>_start()` / `_end()` / `_tz()` and
  `p_<param>_unit()` on the composite settings, and `pt_<param>(): string`
  on a text setting
- `in_<input>(): f64` (inputs, read in `onBar()`); the chart's own candle
  and bar time also read as `bar.open()`, `bar.high()`, `bar.low()`,
  `bar.close()`, `bar.volume()` and `bar.time()` with no declaration
- `out_<output>(value: f64): void` (outputs, written in `onBar()`; the row
  is emitted when it returns)

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()`. Any name may be written with
capitals. Param, input, string-slot and frame names are stored lowercase
(`param("fastLen", 12)` is the setting `fastlen`), and so is every option
that names one (`"@lineColor"`, `when: "showBands"`, a preset's keys) and
the name inside a `{{fastLen}}` placeholder of a legend title, tooltip or
HUD title; output names keep their capitals
(`[A-Za-z0-9][A-Za-z0-9._-]*`). The accessor answers to the spelling your
file uses: `p_fastLen()` and `p_fastlen()` are the same reader,
`out_BTC_Ratio()` and `out_btc_ratio()` the same writer. Two names in one
family that differ only by case (box, segment and alert names excepted:
they keep their spelling), or that escape identically, are refused, and so
are duplicate param names. 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()`.

Declarations whose names need escaping, and the source that reads them
(the `close` line stays first because the first input sets the request
grid, and `bar.close()` reads through it; the `btc-close` input pins
BTCUSDT on Binance Futures, which the chart reads bar by bar at its own
interval):

```typescript sample=ref-accessors
param("fast.len", 12, { min: 1, max: 200 });
param("slow.len", 26, { min: 2, max: 400 });
input("close", ohlcv.close);
input("btc-close", ohlcv.close, { symbol: "BTCUSDT", exchange: "BINANCE_FUTURES", description: "Fixed BTC reference market" });
output("spread", line, lower);
output("BTC-Ratio", line, lower);

let fast = new Ema(12);
let slow = new Ema(26);

function onStart(): void {
  fast = new Ema(i32(p_fast_len()));
  slow = new Ema(i32(p_slow_len()));
}

function onBar(): void {
  const close = bar.close();
  const btc = in_btc_close();
  const spread = fast.update(close) - slow.update(close);
  const ratio = btc > 0.0 ? close / btc : NaN;
  if (isNaN(spread) || isNaN(ratio)) return;
  out_spread(spread);
  out_btc_ratio(ratio);
}
```

Reordering declarations 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 reader or writer.

Raw positional slot literals (`getFloat(0)`, `getInt(0)`, `setOutput(0, ...)`, `wrun_arg_f64(0)`, `wrun_output_f64(0, ...)`) are refused before
the compiler runs ("Raw slot literal 0 passed to getFloat(): raw positional
slots rebind silently when the sheet changes"), because slots silently
rebind when declarations change. Variable indexes stay legal: `./sdk/sdk`
wraps the raw host imports for genuinely dynamic access.

## Declarations, in one place

The signatures in the table above are the whole grammar; the rules the
editor enforces, each as a named error in the Console:

- Names are string literals; defaults and option values are literals; the
  code never runs when the editor reads the declarations.
- Names may carry capitals (`fastLen`). Params, inputs, string slots and
  frames are stored lowercase, and so is a `{{fastLen}}` placeholder that
  names one; outputs keep their spelling. Two names of one family that
  differ only by case are refused, naming both (boxes, segments and alerts
  keep their spelling and may differ by case).
- Declarations are top-level statements of your file; one anywhere else is
  refused with its line. An indicator is one file.
- Indexes follow declaration order: the first `input(...)` is slot 0, the
  primary input. The rows are always the chart's own bars and every input
  aligns to them; reordering declarations reorders slots while the
  generated accessors keep your source name-attached. A candle field read
  through `bar.*()` with no `input(...)` line is appended after the
  declared inputs, and a file with no `input(...)` line gets `close` as
  slot 0 whether or not it reads `bar.close()`; a declared input that
  reads exactly that feed serves the field instead.
- Box and segment coordinates are output handles bound by a top-level
  `const` (`let`, `var`, and `export const` bind too; a handle may be
  bound below the shape that uses it); the sheet records the output's
  name, never the handle.
- `color_by`, `width_by`, and `shape_where` name a DIFFERENT declared
  output; an output cannot color, widen, or gate itself. `colors` needs
  at least two entries beside `color_by`; `widths` and `width_by` go
  together.
- A celled input (`<class>.cells`) requires `max_cells`; scalar-feed knobs
  (`side`, `tenor`, ...) are refused on it; `book` requires `block_size`.

Run derives the contract from what you declare: a string slot, a
renderer, a drawing, or a celled input stamps the derived sheet
`abi_version: "wrun-2"`; a handle, `strategy(...)` or `bar.isLast()`
stamps `"wrun-3"`; a frame, a level profile or a panel stamps `"wrun-4"`;
a text setting (`param.text`) stamps `"wrun-5"`; a `bar.count()` read
stamps `"wrun-6"`;
scalar-only declarations keep the first contract. `range`, `box`,
`segment` and `alert` are contract-neutral: they never flip the sheet. The
derived sheet records `generated_from: "declarations"` plus a
`source_digest`, and it is derived state: to change it, edit the
declaration. The editor has no hand-written sheet to fall back to, so a
file that declares nothing is refused ("This indicator declares nothing.
Add param(...), input(...), and output(...) statements ..."). A file with
`onBar()` needs at least one `output(...)`; it needs no `input(...)`
line, because the chart's own candle reads through `bar.*()`. The sheet's
fields are on [Declarations and the sheet](declarations.md#the-sheet-run-derives), its pins on
[Multi-timeframe](../core-concepts/multi-timeframe.md) and [Multi-source](../core-concepts/multi-source.md), odds modes on
[Data sources](../core-concepts/data-sources.md#beyond-the-charts-venue); the styling ladders are in [Styling](../presentation/styling.md).

## The cell channel (`wrun-2`)

Modules on the second contract or later 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 source, `input(name, <class>.cells, { max_cells })`
(the derived sheet records it as `cellType: "array"` with `max_cells`),
reads a celled source class ([Data sources](../core-concepts/data-sources.md)),
and receives cells in fixed-width TUPLES per class: `volume_profile` =
`[low, high, buy, sell]` (4 f64s), `book` = `[price, size, side]` (3
f64s), `intrabar` = `[offset_ms, open, high, low, close, volume]` (6
f64s), `trade_volume_by_size` = `[bucket, buy_usd, sell_usd, buy_count,
sell_count]` (5 f64s), `options_chain` = 10 f64s per contract.
`max_cells` counts tuples.

The editor generates one accessor family per celled input (no scalar
accessor: the input's slot in the scalar block holds NaN), read in
`onBar()`:

- `in_<input>_cells(): i32`: f64 cells in this bar's block (tuples x tuple
  width); `0` for a present, empty block; `-1` when the bar carries no
  block, and before the first bar.
- `in_<input>_view(): StaticArray<f64>`: the build's own buffer, holding
  this bar's block with no copy. Only the first `in_<input>_cells()`
  values belong to this bar: on an absent or empty bar the buffer still
  holds the previous block, so bound every loop by the count.
- `in_<input>_read(ptr: i32): i32`: copies the block into the module's
  exported memory at `ptr`, for code that owns its own buffer; returns
  bytes written, `0` for a zero-cell block, `-1` for a missing block.
- `in_<input>_max_cells: i32`: the declared `max_cells` (source tuples).
- `in_<input>_capacity: i32`: the f64 count of the build's buffer
  (`max_cells` x 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: the build allocates one
buffer of `max_cells` tuples per celled input when the module starts
(`in_<input>_capacity` f64s), reads each bar's block into it once, and a
bar whose block exceeds the cap refuses the WHOLE run by name ("... has
1200 cells; max_cells is 1000, so the evaluation is refused (a block is
never truncated)"). An out-of-bounds `ptr` is refused naming the sizes,
and a read of an input not declared as celled 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 refused exactly like `getFloat(0)`, so
source goes through the accessors. A second-contract 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 chart bar 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: on a bar with no block, leave
the output unwritten or `return` from `onBar()`. A celled class can never
be the primary input (so a scalar `input(...)` line stays in front of it) and
follows the chart's own market (a market pin on it is refused); only
`intrabar` takes an `interval`, the finer bars it folds.

Summing a bar's volume profile (total volume = buy + sell in every
`[low, high, buy, sell]` tuple):

```typescript sample=ref-profile-sum
input("close", ohlcv.close);
// One tuple per price level: an hour of BTCUSDT carries several hundred, so leave room.
input("profile", volume_profile.cells, { max_cells: 8192 });
output("total", line, lower);

function onBar(): void {
  const n = in_profile_cells();
  if (n < 0) return; // this bar carries no block
  let total = 0.0;
  if (n > 0) {
    const cells = in_profile_view();
    for (let i = 0; i + 3 < n; i += 4) {
      total += cells[i + 2] + cells[i + 3]; // buy + sell per [low, high, buy, sell] tuple
    }
  }
  out_total(total);
}
```

The first contract is frozen: its import allowlist never grows, and a
module on it that imports the cell functions is refused naming the way in
(a celled input moves the derived sheet to `wrun-2`). Which celled classes
the chart serves is in the table above; an alert on an indicator that
reads a source the alerts engine cannot evaluate is refused when you save
it ([Alerts](../functions/alerts.md)).

## The string channel (`wrun-2`)

From the second contract on, a file may declare string slots: numbered,
named, byte-capped text channels written from `onBar()`. Slots are NOT
outputs: outputs stay numeric; strings exist so renderers and drawing
labels can carry text, and they are never outputs.

When the file declares `string(...)`, the editor generates the string
family, in scope with nothing to import:

- `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 declared `max_bytes` (allocated once at module start, so per-bar
  string work allocates nothing).
- `str_<slot>(s: string)` encodes and sends `s` to that slot;
  `str_<slot>_sb()` sends the built line. Both pass the REQUIRED byte
  count, so a line over the slot's `max_bytes` refuses by name; strings
  are never truncated.

A slot named `debug` is also the editor's debug log: each non-empty line
prints in the Console as `<ISO bar time> <text>`, the newest 400 lines
kept.

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 `onBar()` replaces its bytes. The channel belongs to the row being
committed, the bar `onBar()` runs on: the underlying import
(`wrun_output_str(slot, ptr, len)`, `wrun` namespace) traps by phase
anywhere else, and positional literals on it are refused like the cell
imports (use the generated senders). Caps the chart
enforces, each a named refusal: `max_bytes` per slot (at most 4096), 64
slots per indicator, 64 KiB of string bytes per row, 2 MiB of strings and
frames together per run (a live session counts as one run). Bytes must be
valid UTF-8; a broken sequence refuses the run 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:

```typescript sample=ref-string-channel
param("period", 14, { min: 2, max: 200 });
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;

function onStart(): void {
  period = i32(p_period());
  rsi = new Rsi(period);
}

function onBar(): void {
  const value = rsi.update(bar.close());
  if (isNaN(value)) return;
  out_rsi(value);
  out_tag_x(bar.time());
  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");
}
```

Renderers and drawings are selected after the run
([Plotting](../presentation/plotting.md) and
[Drawing objects](../presentation/drawing-objects.md)); the worked table
example in [Declarations and the sheet](declarations.md#string-slots-renderers-drawings) 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 `onStart()`. Data types, math, strings, control flow, and user
functions take these forms:

| 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` from `onBar()` before the writes: an output left unwritten is NaN on that bar. `Infinity` in an output is refused by name. |
| Windows and history | `History` from `./sdk/stats`: `push(x)` once per bar, then `ago(1)` for the previous bar's value and `max()`, `min()`, `mean()`, `sum()` over the window ([Stats, history and lists](../functions/stats-history-lists.md#history-xn-from-pine)). There is no `close[1]`. By hand: yesterday's value in a module-level variable, or a `StaticArray<f64>` sized once (from a param's `max`) and written as a ring buffer. |
| Collections | `StaticArray<T>` and `Array<T>` allocated at module start or in `onStart()`, never in `onBar()`; `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 `onStart()`; a `type` alias for readability. |
| Time | `bar.time()` gives the bar's open in epoch seconds. Feed it to a `Clock` (the hour, the weekday, new-day tests, in a named zone) or a `Session` (an `"0930-1600"` window on the weekdays you list) from `./sdk/clock` ([Clock and sessions kit](../functions/time-and-sessions-kit.md)). By hand, 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 a `theme.` token the chart resolves at paint time), or chosen per bar through a `color_by` ladder. A handle's colour is a packed number from `./sdk/color`, where `theme.UP` and its six siblings are the same tokens. |

A rolling highest-high that shows the shapes above (a buffer sized once, a
typed helper function, explicit casts; the `high` line is kept as the
first input, which sets the request grid, and `bar.high()` reads through
it):

```typescript sample=ref-skeleton
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;

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

function onStart(): void {
  n = i32(p_bars());
}

function onBar(): void {
  highs[cursor] = bar.high();
  cursor = (cursor + 1) % n;
  if (count < n) count += 1;
  if (count < n) return;
  out_highest(maxOf(highs, n));
}
```

## The sheet Run derives

Run turns your declarations into a sheet, the metadata a published
indicator carries; you never write it. Where each of its top-level fields
is explained:

| Field | Meaning | Page |
| --- | --- | --- |
| `id`, `name`, `description` | the sheet's short name and display strings | [Declarations and the sheet](declarations.md#the-sheet-run-derives) |
| `abi_version` | `wrun-1` (absent means this) through `wrun-5`, derived from what the declarations use | [Declarations and the sheet](declarations.md#abi-versions-the-five-contracts) |
| `wasm_sha256` | the compiled module's digest, stamped by Run | [Declarations and the sheet](declarations.md#the-sheet-run-derives) |
| `params[]` | `{name, default, required?, min?, max?, description?}` | [Declarations and the sheet](declarations.md#the-sheet-run-derives) |
| `inputSources{}`, `inputs[]` | sources keyed by input name; `{index, name, description?, cellType?, max_cells?}` | [Data sources](../core-concepts/data-sources.md) |
| `outputs[]` | `{index, name, plot?, panel?, unit?, displacement_bars?, ...styling}` | [Plotting](../presentation/plotting.md) |
| `ranges[]` | a band between two outputs, with edges, a ladder or a gradient | [Styling](../presentation/styling.md) |
| `boxes[]`, `segments[]` | per-bar shapes over outputs (16 each) | [Drawing objects](../presentation/drawing-objects.md) |
| `string_slots[]`, `renderers[]`, `drawings[]` | the second contract's text and decoration vocabulary | [Declarations and the sheet](declarations.md#string-slots-renderers-drawings) |
| `handles`, `strategy` | the third contract's drawing handles and strategy settings | [Drawing objects](../presentation/drawing-objects.md), [Writing strategies](../strategies/writing-strategies.md) |
| `frames[]`, `levels[]`, `panels[]`, `matrices[]` | the fourth contract's snapshots, docked profiles, panels and strike matrices | [Cards, frames and panels](../presentation/cards-frames-panels.md#frames-panels-and-compact-widgets) |
| `heatmaps[]`, `profiles[]` | the canvases drawn from cells or a grid of outputs, on any contract | [Price canvases](../presentation/price-canvases.md) |
| `panes[]`, `fills[]`, `contrast_guard`, `stack_handles` | named lower panes, interior fills between two lines, and the two chart flags | [Styling](../presentation/styling.md) |
| `alerts[]` | the declared signals the chart's alert dialog offers | [Alerts](../functions/alerts.md) |
| `generated_from`, `source_digest` | provenance of the sheet derived from declarations | this page |

The full validation list, every refusal by path, is at the end of
[Declarations and the sheet](declarations.md#validation-in-one-list); the messages you
will actually meet, with their fixes, are in
[Common errors](../faq/common-errors.md).
