Quick reference

View as MarkdownOpen the editor

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):

HookRunsDo this hereNever here
onStart(): void (optional)once per evaluation, before any barread params through p_<param>(), size buffers, construct TA objectsread 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 firstread 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 returnsallocate (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)

AccessorPhaseMeaning
p_<param>(): f64onStart() onthe 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>(): stringonStart() ona 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>(): f64onBar()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): voidonBar()writes one output; the row is emitted when onBar() returns, NaN in every output it did not write
in_<input>_cells(): i32onBar()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): i32onBar()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: i32constantthe declared max_cells, in source tuples
in_<input>_capacity: i32constantthe 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)

DeclarationSignature
Paramparam(name, default, { required?, min?, max?, description? }), a number field
Typed settingparam.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 layoutpage(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)
Inputinput(name, <source>.<field>, { symbol?, exchange?, interval?, bars?, view?, views?, offset?, side?, tenor?, delta?, fund?, asset?, publisher?, series?, venue?, token?, outcome?, missing?, description? })
Celled inputinput(name, <class>.cells, { max_cells, interval?, bars?, symbol?, exchange?, block_size?, max_depth?, venue?, expiries?, description? }); max_cells may be left out on candles
Outputoutput(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 cardhover(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
Rangerange(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
Fillfill(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
Panepane(name, { title?, place?, height?, scale?, invert?, padding?, min?, max?, format?, decimals?, signed?, unit? }), at most 4; an output names it with pane
Boxbox(name, { top, bottom, from?, to?, when?, panel?, color?, borderColor?, opacity?, borderWidth?, borderStyle?, z?, colorBy?, colors?, colorPackedBy?, borderColorBy?, borderColors?, borderColorPackedBy? })
Segmentsegment(name, { yFrom, yTo, from?, to?, when?, panel?, color?, width?, lineStyle? })
String slotstring(name, { max_bytes, description? })
Renderersrender.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), 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 HUDrender.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? })
Drawingsdraw.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? })
Widgetsdraw.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 panelsframe(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
Handleshandles.line, handles.box, handles.label, handles.polyline({ anchor?, safe_area?, ...the drawing's keys }), the defaults for handles the module creates in onBar()
Chartchart.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())
Alertalert(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:

SlotValues
plotline, 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)
paneloverlay (the price pane), lower (a pane below the chart)
line_style / lineStyle / edge_line_stylesolid, 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, valignui, mono, serif, rounded (system stacks, nothing downloads); normal, medium, bold; top, middle, bottom
gradient_direction, corner_radius, opacityvertical, horizontal; 0..32 px; 0..1, multiplying every colour's own alpha
location (marks)absolute, above_bar, below_bar, top, bottom
chromea 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 defaultchart, 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w
param.source defaultohlcv.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)
unitprice, 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)
shape (renderer)circle, cross, triangle_up, triangle_down, diamond, arrow_up, arrow_down, flag, square
missingcarry (the default: the latest value carries forward), nan, zero: what a bar with no observation of its own reads
sideBUY, SELL
tenorONE_W, ONE_M, TWO_M, THREE_M, SIX_M; required on implied_volatility and skew
delta5, 15, 25 (the default), 35; skew only
fundan 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
venueon options_chain: auto (the default), deribit, cme, binance, okx, bybit, bullish, derive; on options_oi and options_volume: deribit (the default), binance
publisher, seriesthe publisher's upper-case id (FRED, US_TREASURY, ECB, ...) and that publisher's series id (DGS10); economic only, both required
assetan upper-case ticker (BTC); treasury_balance only, the chart's coin when absent
tokenthe token's display name (Bitcoin, Ethereum, Solana), never its ticker; token_supply only, required
outcomeYES, 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 offsetsliterals 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
Colorsany 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).

KindThe dialog drawsonStart() reads
param.int("length", 20, { min: 2, max: 500 })a number field that steps by 1p_length(), a whole number
param.number("mult", 2.0, { min: 0.5, max: 5, step: 0.1 })a number field that steps by stepp_mult()
param.bool("show_bands", true)an on/off togglepb_show_bands(), a bool
param.choice("kind", ["Simple", "Exponential"], "Simple")a menu of the labelsp_kind(), the picked index (0 for the first label)
param.color("basis_color", "#2962ff")a color pickerp_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 itp_since(), epoch seconds
param.price("floor", 0.0)a number field with Pick: the next click on the chart sets the pricep_floor()
param.range("band_pct", [0.5, 2.0], { min: 0, max: 10 })one slider with two handlesp_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 thatp_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 longp_lookbacks(), an f64[] of the filled slots
param.source("src", ohlcv.close)a menu of open, high, low, close, hl2, hlc3, ohlc4, volume, hlcc4nothing: 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, 1wnothing: { interval: "@htf" } on an input pins that input to the pick
param.symbol("pair", "BINANCE_FUTURES:ETHUSDT")a market button that opens the symbol searchnothing: { 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 menup_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). 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 tileRenders inOptions
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 itformat, 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 maxmin 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 laddercolor, color_by, colors, draw (capsule, dot, led, text), headline, font_size
rows(label?, [[label, output or slot, format?], ...], options?)both: rows of label and valuea format per row; color or color_by + colors, leader (none, dots)
meter(label, output)both: a bar between min and max with marksmin 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 chipsnone
rings(label?, [[label, output, { min, max, color? }], ...])both: 1..3 concentric rings, outermost first, each a share of its own range, with a legend besideheight (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

SourceFieldsKnobs and units
ohlcvopen, high, low, close, volumethe chart's own candles; a secondary input may pin another market or a coarser interval
tradesvolumeside REQUIRED: that side's volume per bar, in the base asset; currency "USD" reads it in dollars, "Coin" in coins (one lane per unit)
fundingrate_closein percent, normalized to a one-hour rate; may pin a coarser interval
oiopen, high, low, closein USD; may pin a coarser interval
liquidationsliquidationsside optional (absent = both sides summed); in USD; sparse, so an indicator usually declares missing: "nan" or "zero"
long_short_ratiototal_account, top_trader_account, top_trader_positionplain ratios of longs over shorts (a value at or below 0 reads NaN); the chart must be 5 minutes or coarser
implied_volatilityimplied_volatilitytenor REQUIRED; Deribit's summary for the chart's coin
skewskewtenor REQUIRED, delta optional (25 when absent); Deribit's skew for the chart's coin
volatility_indexopen, high, low, closeDeribit's volatility index (DVOL) for the chart's coin, in index points; BTC and ETH
options_oiputs, callsvenue optional; open interest in the venue's units (contracts on Deribit, the base coin on Binance); BTC and ETH
options_volumeputs, callsvenue optional; volume in contracts; BTC and ETH
etf_flowflow_usdfund 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_holdingsholdingsfund REQUIRED (a ticker or all); in coins of the fund's underlying; a coin with listed spot ETFs
etf_premiumpremium_ratefund REQUIRED (one ticker); in percent; daily, read as the newest value at or before the bar's day
ethena_positionscollateralEthena's collateral in the chart's coin; BTC and ETH
bitfinex_fundingfunding_size, credit_size, active_credit_size, margin_ratethe three sizes in the funding currency, margin_rate an APR in percent; the chart's coin
treasury_balancebalanceasset optional; Binance's own balance in coins, published monthly
token_supplymarketcap, first_marketcap, marketcap_dominance_percent, circulating_supply, total_supply, max_supply, total_value_locked, fully_diluted_valuation, cg_marketcap_rank, total_volume, usd_pricetoken REQUIRED; daily; a one-minute chart is refused
economicvaluepublisher and series REQUIRED; the publisher's unit; daily or slower
oddsopen, high, low, close (default), volumethe 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
timebar_open_sec, trade_date, sessionthe 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")

ClassTupleWidthKnobsOn the chart
volume_profile[low, high, buy, sell] per price bucket, ascending by price4 f64ticks_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 ask3 f64block_size REQUIRED by the declarationserved, 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, ascending6 f64interval 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 marketserved 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 first6 f64interval 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 barsserved: 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 f64none (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 put10 f64venue ("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
tapenot 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.

GroupClassesPer bar
AveragesSma, Ema, Rma, Wma, Hma, Alma, Swma, Linregupdate(x); Vwma is update(x, volume)
Statistics and seriesSum, Median, Percentile, Variance, Stdev, Zscore, Change, Mom, Roc, Cum, Fixnanupdate(x); Correlation is update(a, b)
OscillatorsRsi, Cmo, Tsi, Macdupdate(x)
Oscillators over OHLCCci, Wpr, Stoch, Stochasticupdate(high, low, close); Mfi is update(high, low, close, volume); Obv is update(close, volume)
Ranges and bandsTr, Atrupdate(high, low, close); Bb is update(x), Keltner is update(x, high, low, close), Donchian is update(high, low)
Window extremesHighest, Lowest, HighestBars, LowestBarsupdate(x) (pass the high for a highest high, the low for a lowest low)
Trend systemsAdx, Ichimoku, Psar, Supertrendupdate(high, low, close); Vwap is update(open, high, low, close, volume, tsMs), tsMs the bar's open time in milliseconds
EventsRising, Falling, PivotHigh, PivotLowupdate(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.

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.

KitMain exportsPage
./sdk/ta-plus26 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, KamaExtra indicators
./sdk/statsHistory (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 countStats, history and lists
./sdk/fmtTextBuilder (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/clockClock (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/resampleResampler (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/candlesPeriods 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/colorink.* (twenty packed colors), fromHex, alpha, mix, lighten, darken for handle colors; ColorScale, Thresholds for a color_by indexColors
./sdk/orderflowdelta, deltaPct, Cvd, VolumeProfile (poc, vah, val), BookImbalance, Absorption, LiquidationBurstOrder flow
./sdk/levelsPeriodLevels, SessionLevels, PivotPoints, roundLevels, SupportResistanceLevels kit
./sdk/structureSwings, 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:

ExportThe build's version, around a file with onBar()In a file that exports it
init(): voidreads every param once, then calls onStart() when the file declares oneruns once before any bar; the only place p_<param>() reads
state(): i32reads this bar's inputs and returns 1 on every barruns once per bar; the only place in_<input>() and the cell accessors read; 0 abstains the row
finalize(): voidwrites NaN to every output, calls onBar(), then emits the rowruns after each bar whose state() returned 1: writes outputs and string slots, then emitRow() LAST
reset(): voidcalls onReset() when the file declares one; otherwise nothing is resetrequired: 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):

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, 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); 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):

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);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

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 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:

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");
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

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:

NeedForm
Numbersf64 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 valueNaN, 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 historyHistory 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 StaticArray<f64> sized once (from a param's max) and written as a ring buffer.
CollectionsStaticArray<T> and Array<T> allocated at module start or in onStart(), never in onBar(); Map<K, V> for keyed state, sized up front.
MathMath.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.
Stringsonly in string slots; build with sb_text / sb_int / sb_f64, never + on a string per bar (that allocates).
Control flowif / else, for, while, break, continue, switch on integers; a number is not a truth value (if (value > 0.0), not if (value)).
Functionsfunction 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.
Typesclass Zone { top: f64 = NaN; bottom: f64 = NaN; alive: bool = false; }, constructed in onStart(); a type alias for readability.
Timebar.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).
Colorsnever 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));
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

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:

FieldMeaningPage
id, name, descriptionthe sheet's short name and display stringsDeclarations and the sheet
abi_versionwrun-1 (absent means this) through wrun-5, derived from what the declarations useDeclarations and the sheet
wasm_sha256the compiled module's digest, stamped by RunDeclarations 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_bars?, ...styling}Plotting
ranges[]a band between two outputs, with edges, a ladder or a gradientStyling
boxes[], segments[]per-bar shapes over outputs (16 each)Drawing objects
string_slots[], renderers[], drawings[]the second contract's text and decoration vocabularyDeclarations and the sheet
handles, strategythe third contract's drawing handles and strategy settingsDrawing objects, Writing strategies
frames[], levels[], panels[], matrices[]the fourth contract's snapshots, docked profiles, panels and strike matricesCards, frames and panels
heatmaps[], profiles[]the canvases drawn from cells or a grid of outputs, on any contractPrice canvases
panes[], fills[], contrast_guard, stack_handlesnamed lower panes, interior fills between two lines, and the two chart flagsStyling
alerts[]the declared signals the chart's alert dialog offersAlerts
generated_from, source_digestprovenance of the sheet derived from declarationsthis 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.