---
title: "Execution model"
description: "An 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: 23
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

An 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,
what replaces the bar-state globals of kScript (legacy), and which host
runs the file (your browser, your machine, or the platform's hosted alerts
engine).

## The bar loop

kScript ran your whole script once per bar, oldest first, walking forward
to the newest. An Indicator runs `state()` once per bar the same way, and
the difference is what survives between calls: a kScript re-ran its
declarations and re-read its `persist` state each bar; an Indicator keeps
module-level variables alive across the whole walk and calls four
functions in a fixed order.

| Function | Called | Reads | Writes |
| --- | --- | --- | --- |
| `init()` | once, before the first bar | params via `p_<name>()` | nothing; size your averages and buffers here |
| `state()` | once per bar, oldest first | this bar's inputs via `in_<name>()` | your module-level state; returns `1` (row ready) or `0` (abstain) |
| `finalize()` | once per bar whose `state()` returned `1` | your state | every output via `out_<name>(value)`, string slots, drawing handles, then `emitRow()` last |
| `reset()` | when the host replays the forming bar | nothing | every module-level variable back to its starting value, `.reset()` on every TA object |

The host walks the loaded history in order. For each bar it calls
`state()`; when that returns `1` it calls `finalize()` and reads the values
written before `emitRow()`. A bar whose `state()` returned `0` has no row:
nothing is drawn there, no metric value exists for it, and `finalize()` is
not called.

## `barIndex`, `isFirst`, `isLastBar`

Two of the bar-state globals have no counterpart: `barIndex` is a
module-level counter you increment in `state()`, and `isFirst` is that
counter at `0` (one-time setup that needs no bar belongs in `init()`).
`isLastBar` is `bar.isLast()` from `./gen/draw`: 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. `core-variables.md` has the counter idioms in a compiled example.

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, on every tick.
- When that bar closes, the host re-runs 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](../functions/drawing-objects.md)):

```typescript
import { handles, input, line, ohlcv, output, overlay, param, time } from "./sdk/declare";
import { bar, draw } from "./gen/draw";
import { in_close, in_t } 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);
input("t", time.bar_open_sec);
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 value: f64 = NaN;
let t: f64 = NaN;
let prevT: f64 = NaN;

export function init(): void {
  sma = new Sma(i32(p_period()));
}

export function state(): i32 {
  prevT = t;
  t = in_t();
  value = sma.update(in_close());
  return isNaN(value) || isNaN(prevT) ? 0 : 1;
}

export function finalize(): void {
  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);
  emitRow();
}

export function reset(): void {
  sma.reset();
  value = NaN;
  t = NaN;
  prevT = NaN;
}
```

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.

## Everything travels by name

Params, inputs, and outputs reach the code through generated accessors
(`./gen/params`, `./gen/inputs`, `./gen/outputs`), one function per
declared name. Underneath, the host passes values positionally, in
declaration order. The accessors are regenerated from the declarations
before every build, so reordering or renaming a declaration never rebinds
a value silently: the compiler names the import that no longer exists.
Raw positional reads (`getFloat(0)`) are refused by the build for the same
reason (`faq/common-errors.md`).

## Warm-up: NaN until the window fills

Every TA class in `./sdk/ta` 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:

- Abstain the whole row: `return 0` from `state()`. Use it when nothing on
  the row is meaningful yet. The template does this
  (`return isNaN(value) ? 0 : 1`).
- 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).

Anything else written on a warming bar is drawn as if it were true. A
bar-to-bar change with a smoothed line shows both policies at once: the
first bar has no previous close (abstain), then the change exists while
its average does not (abstain again until the average is warm), then both
draw:

```typescript
import { input, line, lower, ohlcv, output, param } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_change_pct, out_smoothed } from "./gen/outputs";
import { p_period } from "./gen/params";
import { Ema } from "./sdk/ta";

param("period", 10, { min: 2, max: 200, description: "EMA length over the bar-to-bar change" });
input("close", ohlcv.close);
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;
let change: f64 = NaN;
let smoothed: f64 = NaN;

export function init(): void {
  ema = new Ema(i32(p_period()));
}

export function state(): i32 {
  const close = in_close();
  // Yesterday's close is whatever we kept from the previous call: there is no close[1] to read.
  change = isNaN(prevClose) || prevClose == 0.0 ? NaN : ((close - prevClose) / prevClose) * 100.0;
  prevClose = close;
  if (isNaN(change)) return 0;
  smoothed = ema.update(change);
  // The EMA is NaN until `period` changes have been folded in; those bars abstain.
  return isNaN(smoothed) ? 0 : 1;
}

export function finalize(): void {
  out_change_pct(change);
  out_smoothed(smoothed);
  emitRow();
}

export function reset(): void {
  ema.reset();
  prevClose = NaN;
  change = NaN;
  smoothed = NaN;
}
```

## History indexing

A kScript read a previous bar with `series[n]` on any `timeseries`. An
Indicator has no history operator: `state()` 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
the property kScript calls no repaint, and an Indicator cannot break it by
accident: there is nothing to peek at.

Higher timeframes follow from the same rule. On the chart every input is
the chart's own interval, so a 4h view is built inside the module: bucket
bars by `time.bar_open_sec`, and fold a bucket into its average only once
the next bucket has started (`multi-timeframe.md`; the cookbook's regime
filter does this). On your machine an `interval` pin reads a coarser feed
instead, and the host contributes a coarse candle only as of its 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": it is
re-evaluated on every tick. On the chart the host keeps the compiled
module alive, snapshots its state and its drawing handles after the last
closed bar, and replays the revised forming bar into both, so a tick never
stacks a second copy of what the forming bar drew; on your machine `om
metric series` returns the forming bar as its newest row. This is why
`reset()` must clear every variable: a field it forgets is a stale value
the replayed bar will read (`repainting.md`).

## You need a primary input to reach the loop

kScript's bar sequence came from the sources it loaded, so a script with no
source never entered the loop. An Indicator's grid comes from its first
input: the market and interval of `inputs[0]` define the rows every other
input aligns to, and `state()` runs once per row of that grid. A `time`
source cannot be that first input (there is no feed behind it), and a
package whose only inputs are pinned to fixed markets computes the same
value for every selector symbol, which mislabels screens and legends: keep
the primary selector-following (`functions/script-definition.md`). This
applies even when the computation does not obviously use price: a
session clock still declares `input("close", ohlcv.close)` first, so the
host knows which bars to walk.

## Where an Indicator runs

| Host | How it gets there | What every input reads | What is supported |
| --- | --- | --- | --- |
| Your browser | **Run** in the editor; a **New engine** row; an Indicators-tab add | the chart's own market and interval, for every input; `symbol`, `exchange`, and `interval` pins are not honored on the chart yet | `ohlcv`, `time`, `trades` (`volume` with a `side`), `oi`, `liquidations`, `funding` (`rate_close` only, as a percent), `implied_volatility` and `skew` (tenors `ONE_W`, `ONE_M`, `THREE_M`), and the celled `book` and `volume_profile` classes; `odds`, `token_supply`, and metric composition refuse by name |
| Your machine | `om wrun install ./dir`, or `om install @scope/name` after a publish | the market you ask for; `symbol` + `exchange` pins read fixed reference markets and `interval` pins read coarser feeds as of their close | watch conditions on the metric id, `om metric get` / `om metric series`, screens, backtests (screens and backtests refuse celled packages) |
| Hosted alerts | an alert armed from the chart on a **published** Indicator | the chart's own candles only | where the site has it switched on (the chart says "Indicator alerts are not enabled yet" while it is off); refused for a pinned market or interval, a source other than candles, a celled class, or a runtime version it does not support. kScript indicators run on the same hosted engine |

The rule to remember when a chart and your machine disagree: the chart
reads its own market for every input, your machine honors pins. A package
that pins another venue or a coarser interval is a package for your machine
(`data-sources.md` lists every source and knob).

## 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 sheet 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). Text reaches the chart only through
string slots and renderers (`functions/plotting.md`); params are numbers,
so a color picker or a free-text setting has no declaration form (style
knobs exist only in hand-written sheets, `functions/styling.md`). There is
no `print()`: emit a data-only output and read it in the legend, or with
`om metric series` on your machine.

## Memory and speed

The module runs once per loaded bar, then once per tick. Allocate in
`init()` or at module scope (a `StaticArray<f64>` sized from a param's
`max`), never per bar: the module has no garbage collector, an array that
grows on every call is the one pattern that makes a long history slow, and
it survives across the forming bar's replays. Loops bounded by a param with
a declared `max` stay cheap; the compiled module itself is a few
kilobytes. kScript stopped a script that took more than 500 milliseconds
on a bar; an Indicator's host enforces an execution timeout on the
evaluation the same way, refusing it by name rather than hanging the chart.
