Execution model
A wrun indicator is a function the host calls once per bar, oldest bar first, with state that lives between calls in module-level variables.
This page is the mental model behind every recipe: what runs when, why the first bars of a line are empty, what "no lookahead" means when you write the code, how to know the bar's index and whether it is the newest, and where an indicator runs: in your browser, on OpenMarket's servers, on OpenMarket's alerts engine, and in the Strategy Tester.
The bar loop
An indicator runs onBar() once per bar, oldest first, walking forward
to the newest. Module-level variables stay alive across the whole walk,
and the file defines two hooks with fixed roles (onStart() is optional):
| Function | Called | Reads | Writes |
|---|---|---|---|
onStart() | once, before the first bar | params via p_<name>() | nothing; size your averages and buffers here |
onBar() | once per bar, oldest first | this bar's candle via bar.<field>(), every declared input via in_<name>(), params | your module-level state; every output via out_, string slots, drawing handles, strategy orders |
The hooks are plain top-level functions with no arguments that return
nothing: function onStart(): void { ... } and function onBar(): void { ... }. Anything else (an argument, a missing or different return type,
an arrow) is refused on the hook's own line: onBar must be a function with no arguments that returns nothing: write function onBar(): void { ... } (the build calls it once per bar).
Params are readable from onStart() on, anywhere in the file (at module
scope, before it, a p_<name>() reads NaN). Inputs and the bar's fields
are readable in onBar() and the helpers it calls (in onStart() they
read NaN). The readers return values cached for the bar, so reading one
twice costs nothing.
The host walks the loaded history in order. On the chart that history is
exactly the candles the chart has loaded, every source is fetched over the
same window, and panning back runs the indicator again over the longer
window. For each bar the host calls onBar(), then emits the bar's row
with whatever onBar() wrote. Every bar has a row: an output onBar()
did not write is NaN on that bar, and nothing is drawn there. Nothing in
the file has to send the row: each out_<name>(value) keeps its value for
the bar (the last write wins), and the row reaches the host in one step
after onBar() returns, so an emitRow() of your own does nothing.
The bar's index, the first bar, the newest bar
There are no bar-state globals. The bar's index is a module-level counter
you increment in onBar(), and the first bar is that counter at 0
(one-time setup that needs no bar belongs in onStart()). The newest bar is
bar.isLast(): true exactly when the bar
being evaluated is the newest bar the host holds for this run, false on
every earlier bar. Run-level renderers and declared drawings still
evaluate the newest ready row on their own without it; the signal is for
a handle drawing that should exist on the newest bar only, or reach past
it. Variables has the counter idioms in a compiled
example, and the bar's own fields beside them.
The signal is a function of the run's window, so a replay from bar 0 over the same window reproduces it:
- A full run answers true on its last row only.
- On the live chart it answers true on the forming bar, each time the forming bar is evaluated again.
- When that bar closes, the chart evaluates it ONCE more as a closed bar,
with the signal false, before the new bar is evaluated with it true.
The re-run's outputs, strings, and handle drawings replace the row's.
This is what keeps a chart that has been open all day identical to a
fresh load: nothing the forming bar drew under
bar.isLast()survives its close unless the closed bar draws it too.
An average with a dotted projection five bars ahead, drawn on the newest bar only. The projection's far end lies past the loaded range, which a handle's absolute time coordinates allow (Drawing objects):
param("period", 20, { min: 1, max: 200 });
output("sma", line, overlay, { color: "#38bdf8", width: 2 });
handles.line({ color: "#38bdf8", width: 1, lineStyle: "dotted" });
const projection = draw.line(0);
let sma = new Sma(20);
let t: f64 = NaN;
let prevT: f64 = NaN;
function onStart(): void {
sma = new Sma(i32(p_period()));
}
function onBar(): void {
prevT = t;
t = bar.time();
const value = sma.update(bar.close());
if (isNaN(value) || isNaN(prevT)) return;
out_sma(value);
// Only the newest bar carries the projection: five bars ahead is past the loaded
// range, and an absolute x makes that legal.
if (bar.isLast()) projection.set(t, value, t + 5.0 * (t - prevT), value);
}Over a full run the line is created on the last row and nowhere else; when that bar closes on the live chart, the closed-bar re-run draws no line, and the new forming bar creates it again one bar to the right.
How many bars the run holds
bar.count() is the number of bars the run holds while the bar is
evaluated, the forming bar included. With your own bar counter it tells a
bar whether a later bar exists, which a lagging line needs: a line drawn
shift bars back can only show a bar's value once the bar shift bars
later exists.
let index: i32 = 0;
function onBar(): void {
const later = index + 1 < bar.count(); // a bar after this one exists in the run
out_settled(index + shift < bar.count() ? 1.0 : 0.0); // the bar shift bars later exists
index += 1;
}- A full run (a fresh load) answers the run's bar count on every bar, so every bar sees the whole window, including bars after it.
- On the live chart the forming bar answers the bars held so far, and a new revision of the forming bar evaluates only that bar again.
- When a new bar opens, the chart runs every bar again from the first one with the new count, so each bar's reading stays current and a chart open all day matches a fresh load over the same bars.
Reading bar.count() makes the sheet wrun-6 (Versions and
contracts). It answers inside onBar() only;
read in onStart() it stops the run by name (wrun_bar_count_phase).
Everything travels by name
Params, inputs, and outputs reach the code through generated readers and
writers (p_<name>(), in_<name>(), out_<name>()), one function per
declared name, and the chart's own candle through bar.<field>().
Underneath, the host passes values positionally, in
declaration order. The editor regenerates the readers from the
declarations on every compile, so reordering or renaming a declaration
never rebinds a value silently: the compiler names the reader or writer
that no longer exists. Raw positional reads (getFloat(0)) are refused by
the editor's lint stage for the same reason
(Common errors).
Warm-up: NaN until the window fills
Every TA class returns NaN until it has seen enough bars:
Sma, Stdev, and Zscore need period values, Ema seeds itself with a
simple average of the first period values, Rsi is warm after period + 1 samples, Roc after period + 1. Cross.update() returns 0 on any
bar where either side is NaN.
You have two ways to handle a value that is not ready yet, and both are correct:
- Write nothing on the bar:
returnfromonBar()before any write, after the state updates the bar still owes (theupdate()that folds the bar in, the close you remember). The bar's row then carriesNaNin every output, and the chart draws nothing there. Use it when nothing on the bar is meaningful yet. - Write
NaNto one output: the chart draws nothing for that output on that bar while the other outputs still draw. Use it when one line warms slower than another, or when a value legitimately has no answer (a session that has not completed yet, a ratio with a zero denominator). A warming TA value is alreadyNaN, so writing it as it comes needs no test at all: the Moving Average starter does this (out_sma(sma.update(bar.close()))).
Anything else written on a warming bar is drawn as if it were true. The
chart does not skip a warm-up count for you: it hands the module every
loaded bar and onBar() decides what to write. A bar-to-bar change
with a smoothed line returns early twice over: on the first bar there is
no previous close, then the change exists while its average does not,
and only once both are real does it write them:
param("period", 10, { min: 2, max: 200, description: "EMA length over the bar-to-bar change" });
output("change_pct", line, lower, { unit: "%", color: "#94a3b8" });
output("smoothed", line, lower, { unit: "%", color: "#38bdf8", width: 2 });
let ema = new Ema(10);
let prevClose: f64 = NaN;
function onStart(): void {
ema = new Ema(i32(p_period()));
}
function onBar(): void {
const close = bar.close();
// Yesterday's close is whatever we kept from the previous call: there is no close[1] to read.
const change = isNaN(prevClose) || prevClose == 0.0 ? NaN : ((close - prevClose) / prevClose) * 100.0;
prevClose = close;
if (isNaN(change)) return;
const smoothed = ema.update(change);
// The EMA is NaN until `period` changes have been folded in; those bars write nothing.
if (isNaN(smoothed)) return;
out_change_pct(change);
out_smoothed(smoothed);
}History indexing
An indicator has no history operator: onBar() sees exactly one bar, and
indexing a number (close[1]) is a compile error (Index signature is missing in type 'f64'). Keep yesterday's value in a module-level variable
when you see it (prevClose above), keep a window in a StaticArray<f64>
ring buffer, and let a TA class keep the window a statistic needs.
No lookahead
A value can therefore only depend on bars at or before its own, and a mark
that appears in history would have appeared live on the same bar. This is
what no repaint means, and an indicator cannot break it by accident: there
is nothing to peek at. The one deliberate exception is bar.count(): a
value that reads it can change when a later bar arrives, which is why the
chart runs every bar again when a bar opens (How many bars the run
holds).
Higher timeframes follow from the same rule, in either of the chart's two
forms (Multi-timeframe). Build the 4h view inside the
module: bucket bars by bar.time(), and fold a bucket into its
average only once the next bucket has started (the cookbook's regime
filter does this). Or pin a secondary input to interval: "4h": the chart
fetches the 4h candles as their own series and hands each one to a chart
bar only as of that bar's close, so a forming 4h candle never leaks into
the 1h rows under it.
The forming bar is the one exception to "one call per bar": the chart evaluates it again as new data arrives, at most about once a second per indicator, coalescing the updates in between and never dropping the latest. The chart keeps the compiled module alive, snapshots its state after the last closed bar (its memory, every module-level variable, and its drawing handles), and restores that snapshot before each new evaluation of the forming bar, so an update never counts the bar twice or stacks a second copy of what the forming bar drew. What can still change a bar you have already seen is on Repainting.
Declare the chart's candles first
On the chart an indicator's rows are always the chart's own candles, and
onBar() runs once per row. A file with no input line runs on them with
no declaration at all: bar.close() and the other fields are its inputs,
and the chart's close is its grid. A file whose first input line names
another candle field (a lone input("high", ohlcv.high);) keeps that
line, so the grid stays on that field instead of moving to the close;
bar.high() and the other bar reads work either way, through the line
or without one. Once a file declares an input, its first input
line is the grid: it follows the chart's market and interval, so a market
or interval pin on it is refused by name (a Polymarket odds input is the
one source that may come first with a market of its own), and a time or
celled source cannot be the first input (there is no feed behind the
clock; a block needs a grid to be sliced by). So when the file declares
anything else, declare the chart's own candles first even when the
computation does not obviously use price: a file with a celled
volume_profile input still declares input("close", ohlcv.close) before
it, a forming view on a pinned input needs it, and a strategy's fills run
against the chart's candles (with no input line a strategy gets close as
its grid). The full rules, the reserved names and the order of the sheet
are on Data sources.
Where an indicator runs
| Where | How it gets there | What it computes over |
|---|---|---|
| Your browser | Run in the editor, or adding an indicator published with Compiled or Open source code | the chart's loaded bars and every source on Data sources, with pins served as Multi-timeframe and Multi-source describe |
| OpenMarket's servers | adding an indicator published as Protected, or Run on > Cloud in the editor for your own | the chart's candles; the module never downloads to the chart, which shows the result the servers stream |
| OpenMarket's alerts engine | an alert set from the chart on a published indicator | the chart's own market, with the settings the overlay carries |
| The Strategy Tester | an indicator that declares strategy(...) | the chart's own candles, in your browser |
Your browser. Run compiles the file in your browser and runs the module in a sandboxed worker beside the chart. The computation never leaves the browser: the only network traffic is the data the declared sources need and a one-time download of the compiler. A draft from Run lasts for the session and is not saved with the layout; running again replaces it. A published indicator with Compiled or Open source code runs the same way, in each reader's browser: Compiled readers get a compiled module, never the source.
OpenMarket's servers. An indicator published with the Publish dialog's Code choice Protected runs on OpenMarket's servers only: they compute it over the chart's candles and stream the result, and the code never leaves them; its settings dialog is the file's own, and a source, timeframe or symbol setting keeps its declared default there for now. In the editor, Run on offers Cloud only for a published Protected indicator whose published version matches the code in the tab; otherwise it says "Publish first", "Publish the current version first", or "Runs in the browser. Only a Protected Indicator runs in the cloud." Where an indicator runs is fixed by its first published version (Publishing).
OpenMarket's alerts engine. An alert on an indicator runs in OpenMarket's cloud wherever the overlay itself computes: the alerts engine loads the published version the overlay carries and evaluates it on the chart's market and interval with the overlay's settings, over at most 600 bars of that interval. A draft cannot carry an alert ("Publish the Indicator before adding an alert."). The engine reads another market's candles and a coarser pinned timeframe, and refuses, by name, a pin it cannot serve ("This Indicator is pinned to a different interval than the chart.") or a data source alerts cannot evaluate yet (Alerts).
The Strategy Tester. An indicator that declares strategy(...) places
its orders in onBar(), and the Strategy Tester runs it in your
browser with its own simulated broker, filling orders against the
chart's own candles (Strategies overview).
Numbers only
Every output is a 64-bit float, and NaN means "nothing here". A decision
is an output too: write 1 or 0 and let the declaration turn it into a
look (color_by picks a palette entry per bar, shape_where gates a mark,
when gates a box or a segment). A file declares at least one output;
without one the build stops with a file with onBar() needs at least one output(...) statement: declare what the Indicator draws, e.g. output("value", line, overlay), and write it in onBar() with out_value(...). Text reaches the chart only through
string slots and renderers (Plotting). Params
reach the module as numbers too: every setting is a row in the overlay's
settings dialog, drawn as the control its kind names (param.int,
param.bool, param.choice, param.color, ...; plain param(...) is a
number field labelled by its description, else its name, and bounded by
its min and max), read once before the first bar, and changing one
reruns the compiled module over the loaded bars without compiling again
(Setting kinds). There is no print(): declare a
string slot named debug, write it with str_debug(...) in onBar(),
and each non-empty line prints in the editor's Console with its bar's time
(Debugging).
Memory and speed
The module runs once per loaded bar, then again each time the forming bar
updates. Allocate in onStart() or at module scope (a StaticArray<f64>
sized from a param's max), never per bar: the module has no garbage
collector, and the chart's sandbox caps its memory at 4 MiB and refuses a
run whose memory grows once the bars start ("The Indicator allocated
memory after init() ...: the sandbox forbids growth once the bars
start."). Loops bounded by a param with a declared max stay cheap; the
compiled module itself is a few kilobytes. A run that does not
finish within 20 seconds is stopped and refused by name ("The run did not
finish within 20 s.") rather than hanging the chart, and the chart keeps
the last result that drew.
The four-function form
Underneath, the chart has always called four exported functions, init(),
state(), finalize() and reset(). A file with onBar() is built into
them: the build adds the four around your hooks, with import lines for
every kit name, reader and writer the file uses. For the Moving Average
example (an onStart() that sizes the average, an onBar() that writes
it) the added functions do this:
// What the build adds around the Moving Average's onStart() and onBar().
export function init(): void { /* reads period once */ onStart(); }
export function state(): i32 { /* reads this bar's close */ return 1; }
export function finalize(): void { /* sma starts the bar as NaN */ onBar(); /* then the row goes out */ }
export function reset(): void { /* nothing: the file has no onReset() */ }A file that writes the four functions itself keeps building unchanged, with the same bytes as before; every file written before the hooks existed is one of these. The Moving Average written that way:
import { input, line, ohlcv, output, overlay, param } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_sma } from "./gen/outputs";
import { p_period } from "./gen/params";
import { Sma } from "./sdk/ta";
param("period", 20, { min: 1, max: 200 });
input("close", ohlcv.close);
output("sma", line, overlay);
let sma = new Sma(20);
let value: f64 = NaN;
export function init(): void { sma = new Sma(i32(p_period())); }
export function state(): i32 { value = sma.update(in_close()); return isNaN(value) ? 0 : 1; }
export function finalize(): void { out_sma(value); emitRow(); }
export function reset(): void { sma.reset(); value = NaN; }What differs from a file with onBar():
- Rows.
state()may return0to emit no row at all for that bar: nothing is drawn, no output has a value there, andfinalize()is not called. A file withonBar()emits a row on every bar and leaves what it did not writeNaN. A file that needs a bar with no row at all is written with the four functions. - Phases. It reads in
state()and writes infinalize(): params are readable ininit()only, inputs instate()only, outputs, strings, frames, handles and orders writable infinalize()only, so a value computed instate()travels tofinalize()in a module variable, andfinalize()callsemitRow()last. - Imports. It needs its import lines: the declaring words from
./sdk/declare, its readers and writers from./gen/params,./gen/inputs,./gen/outputs(and./gen/strings,./gen/draw,./gen/strategywhen declared), the classes from the kit modules. A file withonBar()has nothing to import. - Reset.
reset()is its required fourth export, putting every module-level variable back to its starting value. A file withonBar()may definefunction onReset(): void { ... }for the same job, and the build'sreset()calls it when the host resets the run; without one, nothing is reset. The chart in your browser never resets a run this way (it restores the snapshot of the closed bars instead), so a file withoutonReset()loses nothing there.
A file is one form or the other: exporting any of the four, even one, makes it the four-function form, which then declares everything itself, imports included. The reference rows for both are on Declarations and the sheet.