Custom indicators

Indicator API

The full Context, metadata schema, conditional colors, and AssemblyScript subset.

This is the reference for the indicator API: the Context object your calc() function receives, the metadata schema that shapes it, and the AssemblyScript subset you can use.

The entry point

Every indicator defines exactly one top-level function:

function calc(ctx: Context): void

It returns nothing; outputs are written to ctx.plots. For how and when the chart calls it, see the overview's execution model.

Context

MemberTypeDescription
ctx.barBarThe current bar — see below. Read-only.
ctx.barsBarHistoryHistorical ring buffer, max 200 bars. Index 0 = current bar, 1 = previous, etc. Read-only.
ctx.inputsgeneratedYour declared inputs as typed fields. Read-only.
ctx.plotsgeneratedYour declared plots. Writable. Reset to NaN at the start of each new bar; values persist across live ticks of the same bar.
ctx.barIndexi320-based bar index. Stable across live ticks of the same bar.
ctx.isNewBarbooltrue on the first call for a bar; false on live ticks of the same bar.

Bar

The current bar's OHLCV plus a timestamp:

declare class Bar {
    readonly open: f64;
    readonly high: f64;
    readonly low: f64;
    readonly close: f64;
    readonly volume: f64;
    readonly timestamp: f64;  // Unix seconds, UTC
}

Timestamp is Unix seconds, UTC. There is no timezone API and no per-period info exposed inside the sandbox — you can't tell whether you're on 1m or 1H from inside calc(). For hour-of-day or day-index logic, use integer math:

const t = <i32>ctx.bar.timestamp;
const utcHour = (t / 3600) % 24;
const utcDay  = t / 86400;

BarHistory

declare class BarHistory {
    readonly count: i32;             // up to 200
    open(barsBack: i32): f64;        // 0 = current bar
    high(barsBack: i32): f64;
    low(barsBack: i32): f64;
    close(barsBack: i32): f64;
    volume(barsBack: i32): f64;
}

Returns NaN when barsBack >= count. Use only for the lookback pattern — incremental indicators must keep state in module-level variables instead.

Inputs and Plots

These two classes are generated from your metadata, so the exact shape depends on what you declared. The editor's autocomplete reflects your current metadata in real time. As a reference, an indicator with one int input and three plots gets:

declare class Inputs {
    readonly length: i32;
}

declare class Plots {
    upper: f64;
    middle: f64;
    lower: f64;
}

For plots that declare a colors array, an additional <name>Color: u32 field appears for selecting the per-bar color index.

Metadata schema

The full set of fields you can declare:

Top level

  • name — display name. Max 50 chars; letters / digits / spaces / - / . / _ / ( / ).
  • shortName — short label shown on the chart. Same charset rules.
  • overlaytrue draws on the price pane; false opens a separate pane below (RSI, MACD).
  • precision — optional. Number of decimals on the price scale; mainly useful for separate-pane indicators, which get their own scale.
  • inputGroups — optional. Named sections for the settings dialog; inputs opt in via their group field (see below).
  • inputs — see below.
  • plots — see below.
  • bands — optional horizontal reference lines.
  • filledAreas — optional filled zones between two plots or two bands.

Inputs

TypeAS typeNotes
inti32Supports default, min, max.
floatf64Supports default, min, max.
sourcePrice selector (open / high / low / close / hl2 / hlc3 / ohlc4 / hlcc4). See the note below.

Picked a price source and nothing changed? source inputs currently have no effect on the calculation — the dropdown renders, but the selection isn't passed to your code. Read ctx.bar / ctx.bars directly.

Every input also takes an optional display name, a description (shown as the ⓘ tooltip next to the input in the settings dialog), and a group — a key into the top-level inputGroups map, which renders the settings form in titled sections:

"inputGroups": { "bands": { "name": "Bands" } },
"inputs": {
    "length":     { "type": "int", "default": 20, "description": "Averaging window" },
    "multiplier": { "type": "float", "default": 2.0, "group": "bands" }
}

Section order follows input order, not inputGroups key order; each group's name (or its key) is the section header.

Plots

Only type: "line" is supported (no shapes, arrows, or bar-colorers). Style variants:

  • line, histogram, columns, area, stepLine, circles, cross
  • With breaks: lineWithBreaks, stepLineWithBreaks, areaWithBreaks; also stepLineWithDiamonds

Plot fields:

  • color — use 6-digit hex like "#2962FF". An 8-digit hex's alpha component is ignored; 3-digit hex works too. Use transparency for opacity instead of alpha.
  • linewidth — 1–4 recommended; linestyle solid | dotted | dashed, transparency 0–100.
  • title, visible, showOnPriceScale, showInDataWindow, showInStatusLine, trackPrice.
  • colors — array of hex colors for conditional per-bar coloring (see below).
  • offset — visual shift in bars. Number for fixed shift, string for input ref ("offset": "displacement"), or negated string for left shift.

Use lineWithBreaks if you want gaps at NaN bars — session ranges, signal lines, "only when condition X" outputs. It's the style meant for intermittent plots.

Bands and filled areas

Bands are horizontal reference lines: { value, name?, color?, linewidth?, linestyle?, visible? }. Useful for RSI 30 / 70 or any oscillator zero-line.

Filled areas are semi-transparent fills between two plots or two bands: { from, to, color?, transparency?, title? }. Use them between plots for clouds (Bollinger, Ichimoku) and between bands for zone shading (RSI 30–70).

Conditional colors

Adding a colors array to a plot creates a <name>Color: u32 field on ctx.plots that picks which color to use per bar — the palette drives the per-bar color. Out-of-range indices are clamped.

"hist": { "type": "line", "style": "columns",
    "colors": ["#26A69A", "#B2DFDB", "#EF5350", "#FFCDD2"] }
ctx.plots.hist = value;
ctx.plots.histColor = value >= 0 ? (rising ? 0 : 1) : (falling ? 2 : 3);

AssemblyScript gotchas

AS is TypeScript-shaped but stricter. The traps people hit most often:

  • Untyped numeric literals default to i32. Annotate floats explicitly: let x: f64 = 0.0;.
  • Don't return on a live tick without assigning plots. Plots reset to NaN only when a new bar starts; skipping the assignment mid-bar leaves the forming bar frozen at the last value you set.
  • Strict types. No any or undefined. Use f64 for prices, i32 for counters, bool, void. null and nullable reference types exist, but indicator math rarely needs them.
  • Casts use angle brackets. <f64>(len).
  • Math.min and Math.max take exactly two arguments, not rest args.
  • No closures. Functions can't capture outer-scope variables.
  • No for..of. Use index-based for loops.
  • source inputs don't reach your code — see the callout above.
  • No try / catch, no async, no Promises, no imports. throw compiles, but it aborts the indicator's calculation — plots stay unset.
  • No dynamic property access (obj[key]). (Strings do support the standard methods — indexOf, substring, split… — even though autocomplete doesn't list them.)

What you can use:

  • Classes with getters/setters, for / while / if / switch, const / let.
  • StaticArray<T>, Array<T>.
  • Math.{sqrt, abs, pow, min, max, log, exp, floor, ceil, round, PI, E}, NaN, Infinity, isNaN(), isFinite().
  • These also exist as bare functions: abs, sqrt, floor, ceil, min, max.

Something missing or wrong? Email support@strategytune.com.