---
title: "Execution model"
description: "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…"
order: 6
section: "core-concepts"
---

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

# 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

![Bar by bar: onStart() runs once; then onBar() runs once for each loaded bar, oldest first, hands its state to the next call and writes one value per bar, which the chart joins into the line; the forming bar at the right runs again as new data arrives](/wrun/images/diagrams/intro-bar-by-bar.svg)

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_<name>(value)`, 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](core-variables.md) 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](../presentation/drawing-objects.md)):

```typescript sample=cc-last-bar
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.

```typescript
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](../reference/versions.md)). 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](../faq/common-errors.md)).

## Warm-up: NaN until the window fills

![Warm-up: an average of 8 bars is NaN on the first 7 bars, so nothing is drawn there; from the bar where the window is full, the line draws](/wrun/images/diagrams/warm-up.svg)

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: `return` from `onBar()` before any write,
  after the state updates the bar still owes (the `update()` that folds
  the bar in, the close you remember). The bar's row then carries `NaN`
  in every output, and the chart draws nothing there. Use it when nothing
  on the bar is meaningful yet.
- Write `NaN` to 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 already `NaN`, 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:

```typescript sample=cc-exec-change
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](#how-many-bars-the-run-holds)).

Higher timeframes follow from the same rule, in either of the chart's two
forms ([Multi-timeframe](multi-timeframe.md)). 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](repainting.md).

## 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](data-sources.md).

## Where an indicator runs

![Where an indicator runs: Run draws a draft in your browser; published with Compiled or Open source code, it runs in each reader's browser, and a padlock marks both as sandboxed; published as Protected, it runs on OpenMarket's servers; alerts on a published indicator run in OpenMarket's cloud](/wrun/images/diagrams/intro-where-it-runs.svg)

| 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](data-sources.md), with pins served as [Multi-timeframe](multi-timeframe.md) and [Multi-source](multi-source.md) 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](../functions/publishing.md)).

**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](../functions/alerts.md)).

**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](../strategies/overview.md)).

## 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](../presentation/plotting.md)). 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](../settings/kinds.md)). 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](../faq/debugging.md)).

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

```typescript
// 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:

```typescript
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 return `0` to emit no row at all for that bar:
  nothing is drawn, no output has a value there, and `finalize()` is not
  called. A file with `onBar()` emits a row on every bar and leaves what
  it did not write `NaN`. A file that needs a bar with no row at all is
  written with the four functions.
- **Phases.** It reads in `state()` and writes in `finalize()`: params are
  readable in `init()` only, inputs in `state()` only, outputs, strings,
  frames, handles and orders writable in `finalize()` only, so a value
  computed in `state()` travels to `finalize()` in a module variable, and
  `finalize()` calls `emitRow()` 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/strategy` when declared), the classes from the kit modules. A
  file with `onBar()` has nothing to import.
- **Reset.** `reset()` is its required fourth export, putting every
  module-level variable back to its starting value. A file with `onBar()`
  may define `function onReset(): void { ... }` for the same job, and the
  build's `reset()` 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
  without `onReset()` 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](../reference/declarations.md).
