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:
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 |
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 closeThe 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):
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(plotsline,bar,area,histogram,candle,shape,scatter,none; panelsoverlay,lower),box,segment,range,string, therender.*renderers anddraw.*drawings. Script definition, Plotting. - Generated accessors (
./gen/):p_<param>()ininit(),in_<input>()instate(),out_<output>(value)thenemitRow()infinalize(); celled and string accessors when declared. - TA classes (
./sdk/ta): one class per kScript TA builtin,Sma,Ema,Rsi,Macd,Atr,Supertrend,Vwapand the rest. TA library. - Math and control flow:
Math.*overf64,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 instate(), write outputs infinalize(),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; return0fromstate()or writeNaNwhile a value is not ready. There is noprint(): emit a data-only output instead.
What's next
- Quick start: build and read one on your machine.
- Execution model: the deep dive.
- Quick reference: the whole vocabulary on one page.