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…

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:

ConceptIn one line
Four functionsinit() once, state() per bar, finalize() per ready bar, reset() on replay. The host calls them; you never do.
Module-level stateVariables outside the functions survive from bar to bar. That is where a window, a counter, or yesterday's close lives.
Declared, not calledparam, input, and output are statements at the top of the file. The build reads them without running the code and derives the sheet.
Numbers onlyEvery 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:

FunctionCalledReadsWrites
init()once, before the first barparams via p_<name>()nothing; size your averages and buffers here
state()once per bar, oldest firstthis bar's inputs via in_<name>()your module-level state; returns 1 (row ready) or 0 (abstain)
finalize()once per bar whose state() returned 1your stateevery output via out_<name>(value), then emitRow() last
reset()when the host replays the forming barnothingevery 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:

kScriptIndicatorWhere
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 levela module-level variable, restored by reset()
timeseries s, s[1], s[5] (history)a module-level "previous" variable, or a StaticArray<f64> ring bufferthere 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:

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 letring buffer
Resets each bar?yesnono
Holds history?noone valueas many as you size
reset() must restore it?noyesyes (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.

TypeDescriptionExample
f64Every param, input, and output; prices, volumes, scores45000.5, NaN
i32Whole numbers: periods, counters, cursors, the state() return20, i32(p_period())
boolConditionstrue, close > avg
stringText inside the module; leaves only through a declared string slot"buy "
StaticArray<f64>A fixed-size window, allocated oncenew StaticArray<f64>(50)
classYour own typed structs with methodsclass 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>():

kScriptIndicatorFields
ohlcv(...), .closeinput("close", ohlcv.close)open, high, low, close, volume
funding_rate(...), .valueinput("funding", funding.rate_close)rate_*, predicted_*
liquidations(...), .buy / .sellinput("liqs", liquidations.liquidations, { side: "BUY" })one field, side optional
open_interest(...), .closeinput("oi", oi.close)open, high, low, close
buy_sell_volume(...), .buy / .sellinput("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(), barIndexinput("bar_t", time.bar_open_sec)epoch seconds, UTC
source("volume_profile"), orderbook()input("profile", volume_profile.cells, { max_cells: 512 })celled classes
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:

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

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, Plotting.
  • 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.
  • Math and control flow: Math.* over f64, isNaN, isFinite; if, for, while, switch, the ternary. Math functions, Loops.

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