---
title: "Indicators overview"
description: "A complete guide to how an Indicator works: the four functions the host calls, the three ways state is kept between bars, the data types, how market data…"
order: 4
section: "getting-started"
---

<!-- source: docs/indicators/getting-started/overview.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# Indicators overview

A complete guide to how an Indicator works: the four functions the host
calls, the three ways state is kept between bars, the data types, how
market data reaches the code, where the file runs, and the vocabulary you
will reach for most. Read this once and every other page in the tree will
feel familiar. If you know kScript (legacy), each section names the
construct it replaces.

## Key concepts

Indicators are built around a few core ideas that make building them
straightforward:

| Concept | In one line |
| --- | --- |
| Four functions | `init()` once, `state()` per bar, `finalize()` per ready bar, `reset()` on replay. The host calls them; you never do. |
| Module-level state | Variables outside the functions survive from bar to bar. That is where a window, a counter, or yesterday's close lives. |
| Declared, not called | `param`, `input`, and `output` are statements at the top of the file. The build reads them without running the code and derives the sheet. |
| Numbers only | Every param, input, and output is an `f64`. A decision is a `0` or `1` output; the sheet maps decisions to looks. |

## How an Indicator runs: four functions

Where kScript ran three phases (setup, calculate, display) over the whole
script, an Indicator exports four functions and the host calls them 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)`, 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 and no metric value exists for it. The forming bar
is the exception to "once per bar": it re-evaluates on every tick, which is
what `reset()` exists for. The full treatment is in
`core-concepts/execution-model.md`.

## Three ways to store data

kScript had three declaration keywords: `var` for a per-bar value,
`timeseries` for a value with history, and `persist` (or `static`) for a
value carried across bars. An Indicator has the same three needs and meets
them with ordinary TypeScript scoping:

| kScript | Indicator | Where |
| --- | --- | --- |
| `var x = ...` (this bar only) | `const x = ...` inside `state()` | a local: born and gone in one call |
| `persist n = 0` (carried) | `let n: f64 = 0.0;` at module level | a module-level variable, restored by `reset()` |
| `timeseries s`, `s[1]`, `s[5]` (history) | a module-level "previous" variable, or a `StaticArray<f64>` ring buffer | there is no history array; you keep what you need |

All three in one file: a counter that persists, a per-bar change that uses
one remembered value, and a trailing high that uses a ring buffer:

```typescript
import { input, line, lower, ohlcv, output, param } from "./sdk/declare";
import { in_close, in_high } from "./gen/inputs";
import { emitRow, out_bars_seen, out_change, out_window_high } from "./gen/outputs";
import { p_window } from "./gen/params";

param("window", 3, { min: 1, max: 50, description: "Bars in the trailing high" });
input("close", ohlcv.close);
input("high", ohlcv.high);
output("bars_seen", line, lower, { description: "Bars folded so far (a value that persists)" });
output("change", line, lower, { description: "Close minus the previous close (one remembered value)" });
output("window_high", line, lower, { description: "Highest high of the last N bars (a ring buffer)" });

const MAX_WINDOW = 50; // sized from the param's max, allocated once
const highs = new StaticArray<f64>(MAX_WINDOW);
let n: i32 = 3;
let cursor: i32 = 0;
let count: i32 = 0;
let barsSeen: f64 = 0.0; // persists: survives from bar to bar
let prevClose: f64 = NaN; // the one value "history" needs here
let change: f64 = NaN;
let windowHigh: f64 = NaN;

export function init(): void {
  n = i32(p_window());
}

export function state(): i32 {
  const close = in_close(); // a local: this bar only
  barsSeen += 1.0;
  change = isNaN(prevClose) ? NaN : close - prevClose;
  prevClose = close;
  highs[cursor] = in_high(); // the ring buffer stands in for high[1], high[2], ...
  cursor = (cursor + 1) % n;
  if (count < n) count += 1;
  if (count < n) return 0;
  let h = -Infinity;
  for (let i = 0; i < n; i++) if (highs[i] > h) h = highs[i];
  windowHigh = h;
  return isNaN(change) ? 0 : 1;
}

export function finalize(): void {
  out_bars_seen(barsSeen);
  out_change(change);
  out_window_high(windowHigh);
  emitRow();
}

export function reset(): void {
  cursor = 0;
  count = 0;
  barsSeen = 0.0;
  prevClose = NaN;
  change = NaN;
  windowHigh = NaN;
}
```

Quick comparison:

| | local (`const` in `state()`) | module-level `let` | ring buffer |
| --- | --- | --- | --- |
| Resets each bar? | yes | no | no |
| Holds history? | no | one value | as many as you size |
| `reset()` must restore it? | no | yes | yes (the cursor and count) |

The full story, with `isFirst`, `barIndex`, and `isLastBar` mapped, is in
`core-concepts/core-variables.md`.

## Data types

The file is AssemblyScript: TypeScript syntax over fixed-width numbers.

| Type | Description | Example |
| --- | --- | --- |
| `f64` | Every param, input, and output; prices, volumes, scores | `45000.5`, `NaN` |
| `i32` | Whole numbers: periods, counters, cursors, the `state()` return | `20`, `i32(p_period())` |
| `bool` | Conditions | `true`, `close > avg` |
| `string` | Text inside the module; leaves only through a declared string slot | `"buy "` |
| `StaticArray<f64>` | A fixed-size window, allocated once | `new StaticArray<f64>(50)` |
| `class` | Your own typed structs with methods | `class Zone { top: f64; bottom: f64; }` |

`let x = 1` is an `i32` and `let x = 1.0` is an `f64`; mixing them needs an
explicit cast, and a missing value is `NaN`, tested with `isNaN(x)`
(`../core-concepts/data-types.md`, `../core-concepts/type-system.md`).

## Getting market data

kScript loaded a whole timeseries with `ohlcv(...)` or `source(type=...)`
and read fields off it. An Indicator declares one `input` per field it
reads, as `input(name, source.field)`, and reads that field one bar at a
time through `in_<name>()`:

| kScript | Indicator | Fields |
| --- | --- | --- |
| `ohlcv(...)`, `.close` | `input("close", ohlcv.close)` | `open`, `high`, `low`, `close`, `volume` |
| `funding_rate(...)`, `.value` | `input("funding", funding.rate_close)` | `rate_*`, `predicted_*` |
| `liquidations(...)`, `.buy` / `.sell` | `input("liqs", liquidations.liquidations, { side: "BUY" })` | one field, `side` optional |
| `open_interest(...)`, `.close` | `input("oi", oi.close)` | `open`, `high`, `low`, `close` |
| `buy_sell_volume(...)`, `.buy` / `.sell` | `input("buy", trades.volume, { side: "BUY" })` | `open`..`volume`, `side` required |
| `source("deribit_implied_volatility")` | `input("iv", implied_volatility.implied_volatility, { tenor: "ONE_M" })` | `tenor` required |
| `time()`, `barIndex` | `input("bar_t", time.bar_open_sec)` | epoch seconds, UTC |
| `source("volume_profile")`, `orderbook()` | `input("profile", volume_profile.cells, { max_cells: 512 })` | celled classes |

```text
input("close", ohlcv.close);                                  // the chart's own market
input("btc", ohlcv.close, { symbol: "BTCUSDT", exchange: "BINANCE_FUTURES" }); // a pinned reference market
input("daily", ohlcv.close, { interval: "1d" });              // a coarser interval, read as of its close
```

The first `input` is the primary: it sets the grid (market and interval)
every other input aligns to. Pins are honored on your machine; on the
chart every input reads the chart's own market and interval. The complete
catalog of sources, fields, and knobs is `core-concepts/data-sources.md`.

## Exchange and symbol formats

A pinned input names a venue and that venue's own symbol form:

| Exchange | `exchange` | `symbol` format |
| --- | --- | --- |
| Binance Spot | `"BINANCE"` | `"BTCUSDT"` |
| Binance Futures | `"BINANCE_FUTURES"` | `"BTCUSDT"` |
| Bybit | `"BYBIT"` | `"BTCUSDT"` |
| OKX swaps | `"OKEX_SWAP"` | `"BTC-USDT-SWAP"` |

Some venues use hyphens, others no separator, and `symbol` and `exchange`
always pin together (half a pin names a market that does not exist).
`om symbols` and `om exchanges` list what the platform serves;
`faq/symbol-format.md` has the complete list.

## Where an Indicator runs

In your browser (**Run**, a **New engine** row, an Indicators-tab add),
where every input reads the chart's own market and interval; on your
machine (`om wrun install`, or `om install /name` after a publish),
where `symbol` + `exchange` pins read fixed reference markets and
`interval` pins read coarser feeds as of their close; and on the hosted
alerts engine, behind a flag today. The rule when the two disagree: the
chart reads its own market for every input, your machine honors pins
(`../core-concepts/execution-model.md`).

## Writing your own functions

Break logic into plain TypeScript functions with typed parameters and a
typed return. They may read and write module-level state, and they may be
passed around as long as they do not capture locals
(`core-concepts/user-functions.md`):

```text
function pctChange(from: f64, to: f64): f64 {
  return from == 0.0 ? NaN : ((to - from) / from) * 100.0;
}

const move = pctChange(prevClose, close);
```

## Available vocabulary

The kit is small on purpose, and every family has its own page in the
`functions` section:

- **Declarations** (`./sdk/declare`): `param`, `input`, `output` (plots
  `line`, `bar`, `area`, `histogram`, `candle`, `shape`, `scatter`, `none`;
  panels `overlay`, `lower`), `box`, `segment`, `range`, `string`, the
  `render.*` renderers and `draw.*` drawings.
  [Script definition](../functions/script-definition.md),
  [Plotting](../functions/plotting.md).
- **Generated accessors** (`./gen/`): `p_<param>()` in `init()`,
  `in_<input>()` in `state()`, `out_<output>(value)` then `emitRow()` in
  `finalize()`; celled and string accessors when declared.
- **TA classes** (`./sdk/ta`): one class per kScript TA builtin, `Sma`,
  `Ema`, `Rsi`, `Macd`, `Atr`, `Supertrend`, `Vwap` and the rest.
  [TA library](../functions/ta-library.md).
- **Math and control flow**: `Math.*` over `f64`, `isNaN`, `isFinite`; `if`,
  `for`, `while`, `switch`, the ternary.
  [Math functions](../functions/math-functions.md),
  [Loops](../functions/loops.md).

## Important rules to remember

- **Exactly the four exports**, with exactly those signatures; anything
  else compiles and is then refused by the contract check.
- **Declarations are literal and top-level**, and cannot depend on a param.
- **Read params in `init()`, inputs in `state()`, write outputs in
  `finalize()`, `emitRow()` last.** The accessors are phase-bound.
- **`reset()` restores everything**: every module-level variable and every
  TA object. A forgotten field is a stale value the forming bar reads.
- **Allocate once, abstain when not ready.** Size buffers from a param's
  `max`; return `0` from `state()` or write `NaN` while a value is not
  ready. There is no `print()`: emit a data-only output instead.

## What's next

- [Quick start](quick-start.md): build and read one on your machine.
- [Execution model](../core-concepts/execution-model.md): the deep dive.
- [Quick reference](../reference/quick-reference.md): the whole vocabulary
  on one page.
