Documentation

4. Custom Indicator API Reference

Created
Jun 5, 2026
Updated
Jun 5, 2026

This is the full ctx surface available to a custom indicator. You author an indicator the same way as a strategy: export default an object with a next(ctx) method (and an optional init(ctx)). next runs once per bar, oldest → newest, and emits plot values with ctx.plot(). State persists across bars, so each bar is O(1).

Write TypeScript or JavaScript — types are stripped, not type-checked. See Authoring Language & Syntax for what is rejected (imports, async/await, returning a Promise from next, TS namespace/decorators).

Shape

export default {
  name: "My Indicator",
  init(ctx) {
    // optional — runs once before the bars. Read config, store on `this`.
    this.length = ctx.input("length", 20);
  },
  next(ctx) {
    // runs once per bar
    ctx.plot("value", ctx.ta.sma(this.length).push(ctx.source("source", "close")));
  }
};

You can keep state on this or use ctx.var. A bare return { next(ctx) { … } } is accepted too.

next(ctx) — runs once per bar

  • ctx.bar — this bar's { time, open, high, low, close, volume }.
  • ctx.index — 0-based bar number. ctx.isLast / ctx.isConfirmed (forming vs. closed).
  • ctx.input(key, fallback) — your configured inputs (Settings → Inputs).
  • ctx.source(key, fallback) — this bar's price: an OHLC source ("close", "open", "high", "low", "hl2", "hlc3", "ohlc4") or another indicator "ind:<id>#<output>" (e.g. "ind:rsi#value").
  • ctx.plot(id, value, { color? }) — emit a plot. id must match a plot in Settings → Plots; null = no value (warmup).
  • ctx.var(key, () => init) — persistent scalar. ctx.series(key) — history buffer (s.set(v) writes this bar, s(n) reads n bars back).
  • ctx.na(v) / ctx.nz(v, fb) — null/NaN helpers.
  • ctx.ta — streaming math (stateful; call once per bar with .push):
ctx.ta.ema(len)   ctx.ta.sma(len)   ctx.ta.wma(len)   ctx.ta.smma(len)    // .push(value)
ctx.ta.highest(len)  ctx.ta.lowest(len)  ctx.ta.sum(len)  ctx.ta.stdev(len) // .push(value)
ctx.ta.atr(len)   // .push(bar)          ctx.ta.vwma(len)  // .push(value, volume)

Each returns this bar's value or null during warmup. Pass a stable string key as the 2nd argument when calling the same helper twice (e.g. ctx.ta.ema(12, "fast")).

init(ctx) — runs once before the loop (optional)

  • ctx.input(key, fallback) / ctx.style(key, fallback) — read config; store on this.
  • ctx.useIndicator(id, inputs?){ at(n=0) } — consume another indicator. at(n) is its value n bars back (cursor-bound, no lookahead), or null.
  • ctx.useMultiIndicator(id, inputs?){ at(field, n=0) } — multi-line (e.g. macd"macd" / "signal" / "histogram").
  • ctx.usePattern(kind, params?){ firedAt(n=0), recent(lookback=1) } — chart patterns (bullFlag, doubleTop, headShoulders, …). Store the handle on this; read it in next().

See Composing Indicators & Patterns for consumption details.

Rules

  • A throw inside next() nulls just that bar and continues — it never aborts the series.
  • Use the editor's Template menu for ready examples (Moving Average, ATR, Bollinger Bands, MACD, composition).

Legacy (schemaVersion 1) indicators use a whole-array body: return { values: { <plotId>: (number|null)[] }, colors? } with (bars, input, math, source). Existing ones keep working; new indicators use the per-bar form above.

Next steps

Apply the same engine to trading logic.

Next: Writing Strategies