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_, 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_ / _end() / _tz(), p_, 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) |
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_ | onBar() | writes one output; the row is emitted when onBar() returns, NaN in every output it did not write |
in_ | 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_ | onBar() | the build's own buffer holding this bar's block, no copy; only the first in_ values belong to this bar |
in_ | onBar() | copies the block into module memory at ptr, for code that owns its own buffer; returns bytes written, 0, or -1 |
in_ | constant | the declared max_cells, in source tuples |
in_ | 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., market., market.kind(), market., market.zone(), market. |
| 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>. |
| 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_, returns an OutputHandle |
| Hover card | hover(handle, [block.; the same list inline as an output's hover |
| Range | range(upper, lower, { color?, colors?, color_by?, edge_width?, edge_; edge_width: 0 keeps the band and drops the edge lines |
| Fill | fill(upper, lower, { color?, opacity?, opacity_by?, color_by?, colors?, color_, 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?, border |
| Segment | segment(name, { yFrom, yTo, from?, to?, when?, panel?, color?, width?, lineStyle? }) |
| String slot | string(name, { max_bytes, description? }) |
| Renderers | render., render. (a corner label takes position and offset and no point), render. (Styled tables), render., render., render., render. |
| Legend and HUD | render., render. |
| 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_, draw., draw. |
| 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., draw.; the look is chrome, stripe, controls, accent_color, title_, title_font_size, title_, background_, background_, background_, gradient_, border_color, border_width, border_style, corner_radius, padding, width, opacity, font_size, font_family, font_weight, text_color, label_, 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 |
| Handles | handles.line, handles.box, handles.label, handles., the defaults for handles the module creates in onBar() |
| Chart | chart. (keep the author's colours on a light chart), chart. (keep clear of another indicator's anchored handles), chart., chart., chart., chart. (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) |
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_, 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. | p_: 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_) |
tz (a session) | UTC, America/, America/Chicago, Europe/London, Europe/Berlin, Asia/Tokyo, Asia/Hong_Kong, Asia/Singapore, Australia/, Asia/Kolkata, America/, America/Toronto, America/, America/, America/, Europe/Paris, Europe/, Europe/Zurich, Europe/Madrid, Europe/Rome, Europe/, Europe/Oslo, Europe/, Europe/Warsaw, Europe/Helsinki, Europe/Athens, Europe/Istanbul, Europe/Moscow, Asia/Jerusalem, Asia/Riyadh, Asia/Dubai, Africa/, Asia/Karachi, Asia/Bangkok, Asia/Jakarta, Asia/, Asia/, Asia/Shanghai, Asia/Taipei, Asia/Manila, Asia/Seoul, Pacific/ (42 zones; each with its offset and daylight rule on Sessions and units) |
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_ 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_ 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_, 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,; 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).
| Kind | The dialog draws | onStart() reads |
|---|---|---|
param. | a number field that steps by 1 | p_length(), a whole number |
param. | a number field that steps by step | p_mult() |
param. | an on/off toggle | pb_show_bands(), a bool |
param. | a menu of the labels | p_kind(), the picked index (0 for the first label) |
param. | 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. | a date and time field with Pick: the next click on the chart sets it | p_since(), epoch seconds |
param. | a number field with Pick: the next click on the chart sets the price | p_floor() |
param. | one slider with two handles | p_band_pct_lo() and p_band_pct_hi() |
param. | 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. | an editable list of numbers with Add, up to max long | p_lookbacks(), an f64[] of the filled slots |
param. | 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. | 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. | a market button that opens the symbol search | nothing: { symbol: "@pair" } on an input pins that input to the pick |
param. | 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. | 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). The words that lay the dialog out are on Pages, sections, dividers, notes, the named sets of values on Presets, the per-output rows the dialog adds by itself on The Style page, the session zones and the unit codes on Sessions and units, where a pick applies and the 128-setting cap on Picks, lanes, the cap, and every refusal on What the build checks.
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).
| 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); the HUD card is
render.hud(name, { position, title?, columns?, tiles })
(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). The tooltip
templates, the label style words and badges are on
Labels, tooltips, badges; glow,
corner, opacity, width, line_style, a range's gradient and the
color_by and width_by ladders are on Styling.
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_ | total_account, top_, top_ | plain ratios of longs over shorts (a value at or below 0 reads NaN); the chart must be 5 minutes or coarser |
implied_ | implied_ | 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_ | 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_ | collateral | Ethena's collateral in the chart's coin; BTC and ETH |
bitfinex_ | funding_size, credit_size, active_, margin_rate | the three sizes in the funding currency, margin_rate an APR in percent; the chart's coin |
treasury_ | balance | asset optional; Binance's own balance in coins, published monthly |
token_supply | marketcap, first_marketcap, marketcap_, circulating_, total_supply, max_supply, total_, fully_, cg_, 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 has the views.
Celled classes (abi_version: "wrun-2")
| Class | Tuple | Width | Knobs | On the chart |
|---|---|---|---|---|
volume_ | [low, high, buy, sell] per price bucket, ascending by price | 4 f64 | ticks_ (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_ 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_ and max_ say; bids come best first, then asks from the farthest to the best, so branch on side, never on position |
intrabar | [offset_ per closed finer bar, ascending | 6 f64 | interval REQUIRED (finer than the chart's and dividing it evenly, 1m at the finest); max_ 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_ per closed candle of the stream's interval, oldest first | 6 f64 | interval REQUIRED (any word); bars 1 to 5000; max_ filled when left out; symbol + exchange for another market; view: "forming" (the live candle on the live bar) or "forming_ (on every bar the open of the candle holding it, [offset_), each without bars | served: the backlog on the first bar, then each candle on the bar it closes with |
trade_ | [bucket, buy_ 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_ 7 holds a full bar) | served on the chart's own market; a bar with no trades is an empty block |
options_ | [strike, expiry_ 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(, BarsSince is update(, 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.
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 |
./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 Static and a count | Stats, history and lists |
./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 |
./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 |
./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 |
./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 |
./sdk/color | ink.* (twenty packed colors), fromHex, alpha, mix, lighten, darken for handle colors; ColorScale, Thresholds for a color_by index | Colors |
./sdk/orderflow | delta, deltaPct, Cvd, VolumeProfile (poc, vah, val), BookImbalance, Absorption, Liquidation | Order flow |
./sdk/levels | PeriodLevels, SessionLevels, PivotPoints, roundLevels, Support | Levels kit |
./sdk/structure | Swings, MarketStructure, FairValueGaps, OrderBlocks, Divergence, candles.* | Market structure kit |
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.
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).
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 fromonStart()on), pluspb_<param>(): boolon a toggle,p_<range>_lo()/_hi(),p_<list>(): f64[],p_<session>_start()/_end()/_tz()andp_<param>_unit()on the composite settings, andpt_<param>(): stringon a text settingin_<input>(): f64(inputs, read inonBar()); the chart's own candle and bar time also read asbar.open(),bar.high(),bar.low(),bar.close(),bar.volume()andbar.time()with no declarationout_<output>(value: f64): void(outputs, written inonBar(); 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):
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 throughbar.*()with noinput(...)line is appended after the declared inputs, and a file with noinput(...)line getscloseas slot 0 whether or not it readsbar.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, andexport constbind too; a handle may be bound below the shape that uses it); the sheet records the output's name, never the handle. color_by,width_by, andshape_wherename a DIFFERENT declared output; an output cannot color, widen, or gate itself.colorsneeds at least two entries besidecolor_by;widthsandwidth_bygo together.- A celled input (
<class>.cells) requiresmax_cells; scalar-feed knobs (side,tenor, ...) are refused on it;bookrequiresblock_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, its pins on
Multi-timeframe and Multi-source, odds modes on
Data sources; the styling ladders are in Styling.
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),
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);0for a present, empty block;-1when 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 firstin_<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 atptr, for code that owns its own buffer; returns bytes written,0for a zero-cell block,-1for a missing block.in_<input>_max_cells: i32: the declaredmax_cells(source tuples).in_<input>_capacity: i32: the f64 count of the build's buffer (max_cellsx the class's tuple width); generated when the class's width is known.
Cells are IEEE 754 f64, little-endian, 8 bytes each, contiguous in block
order. max_cells is a contract, not a hint: 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):
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).
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 declaredmax_bytes(allocated once at module start, so per-bar string work allocates nothing).str_<slot>(s: string)encodes and sendssto that slot;str_<slot>_sb()sends the built line. Both pass the REQUIRED byte count, so a line over the slot'smax_bytesrefuses 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:
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 and Drawing objects); the worked table example in Declarations and the sheet 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). There is no close[1]. By hand: yesterday's value in a module-level variable, or a Static 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). 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):
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 |
abi_version | wrun-1 (absent means this) through wrun-5, derived from what the declarations use | Declarations and the sheet |
wasm_sha256 | the compiled module's digest, stamped by Run | Declarations and the sheet |
params[] | {name, default, required?, min?, max?, description?} | Declarations and the sheet |
inputSources{}, inputs[] | sources keyed by input name; {index, name, description?, cellType?, max_cells?} | Data sources |
outputs[] | {index, name, plot?, panel?, unit?, displacement_ | Plotting |
ranges[] | a band between two outputs, with edges, a ladder or a gradient | Styling |
boxes[], segments[] | per-bar shapes over outputs (16 each) | Drawing objects |
string_slots[], renderers[], drawings[] | the second contract's text and decoration vocabulary | Declarations and the sheet |
handles, strategy | the third contract's drawing handles and strategy settings | Drawing objects, Writing strategies |
frames[], levels[], panels[], matrices[] | the fourth contract's snapshots, docked profiles, panels and strike matrices | Cards, frames and panels |
heatmaps[], profiles[] | the canvases drawn from cells or a grid of outputs, on any contract | Price canvases |
panes[], fills[], contrast_guard, stack_handles | named lower panes, interior fills between two lines, and the two chart flags | Styling |
alerts[] | the declared signals the chart's alert dialog offers | Alerts |
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; the messages you will actually meet, with their fixes, are in Common errors.