Custom indicators

Examples

Bollinger Bands (lookback), EMA (incremental), RSI (bands + filled zones) — three reference indicators with full metadata and code.

Three classic indicators, each illustrating a different pattern. Copy them straight into the editor and they compile.

Bollinger Bands — lookback pattern

Stateless: each call recomputes the SMA and stdev from the last N closes. Live ticks are handled automatically because the recompute reads the current bar's evolving close.

metadata.json

{
    "name": "Bollinger Bands",
    "shortName": "BB",
    "overlay": true,
    "inputs": {
        "length":     { "type": "int",   "default": 20,  "min": 2,   "max": 500 },
        "multiplier": { "type": "float", "default": 2.0, "min": 0.1, "max": 10.0 }
    },
    "plots": {
        "upper":  { "type": "line", "color": "#2196F3", "linewidth": 1 },
        "middle": { "type": "line", "color": "#9C27B0", "linewidth": 2 },
        "lower":  { "type": "line", "color": "#2196F3", "linewidth": 1 }
    },
    "filledAreas": [
        { "from": "upper", "to": "lower", "color": "#2196F3", "transparency": 90 }
    ]
}

calc.ts

function calc(ctx: Context): void {
    const len: i32 = ctx.inputs.length;
    const mult: f64 = ctx.inputs.multiplier;

    if (ctx.bars.count < len) return;

    let sum: f64 = 0.0;
    for (let i: i32 = 0; i < len; i++) sum += ctx.bars.close(i);
    const sma: f64 = sum / <f64>len;

    let sqSum: f64 = 0.0;
    for (let i: i32 = 0; i < len; i++) {
        const d: f64 = ctx.bars.close(i) - sma;
        sqSum += d * d;
    }
    const stdev: f64 = Math.sqrt(sqSum / <f64>len);

    ctx.plots.upper  = sma + mult * stdev;
    ctx.plots.middle = sma;
    ctx.plots.lower  = sma - mult * stdev;
}

The early return when ctx.bars.count < len leaves the plots as NaN, which means "don't draw" — a clean warmup with no dotted-line artefacts.

EMA — incremental pattern

Stateful: the previous EMA value is kept in a module-level variable. On a new bar, commit the previous live value and advance. On live ticks, recompute the tentative current value from the committed previous value and the live close.

metadata.json

{
    "name": "EMA",
    "shortName": "EMA",
    "overlay": true,
    "inputs": {
        "length": { "type": "int", "default": 20, "min": 2, "max": 500 }
    },
    "plots": {
        "ema": { "type": "line", "color": "#FF9800", "linewidth": 2 }
    }
}

calc.ts

let prevEma: f64 = 0.0; // committed EMA from last completed bar
let ema: f64 = 0.0;     // current value (recalc on each tick)

function calc(ctx: Context): void {
    const price: f64 = ctx.bar.close;
    const len: i32 = ctx.inputs.length;
    const a: f64 = 2.0 / (<f64>len + 1.0);

    if (ctx.barIndex == 0) {
        ema = price;
    } else if (ctx.isNewBar) {
        prevEma = ema;                          // commit previous bar
        ema = a * price + (1.0 - a) * prevEma;
    } else {
        ema = a * price + (1.0 - a) * prevEma;  // tentative live value
    }

    if (ctx.barIndex < len) return; // warmup
    ctx.plots.ema = ema;
}

The same shape extends to MACD (two EMAs plus a signal EMA, with a four-color histogram via colors), RSI (incremental average gain and loss), and ATR.

RSI — bands and filled zones

RSI is incremental on the inside and uses bands to mark the overbought/oversold lines plus a filled area between them. This snippet just covers the metadata; the calc would follow the EMA pattern with running averages of gains and losses.

metadata.json

{
    "name": "RSI",
    "shortName": "RSI",
    "overlay": false,
    "inputs": {
        "length": { "type": "int", "default": 14, "min": 2, "max": 500 }
    },
    "plots": {
        "rsi": { "type": "line", "color": "#7E57C2", "linewidth": 2 }
    },
    "bands": {
        "overbought": { "value": 70, "color": "#787B86", "linestyle": "dashed" },
        "oversold":   { "value": 30, "color": "#787B86", "linestyle": "dashed" }
    },
    "filledAreas": [
        { "from": "overbought", "to": "oversold", "color": "#7E57C2", "transparency": 95 }
    ]
}

overlay: false opens RSI in its own pane below the chart. filledAreas can target either two plot ids or two band ids — the type is auto-detected.

Picking the right pattern

Looks like…Pattern
Function of the last N bars onlyLookback
Today's value depends on yesterday's valueIncremental
Needs more than 200 bars of historyIncremental (the ring buffer can't help you)
Needs to react to live ticks without overcountingEither, but always check isNewBar before mutating committed state

Or skip writing code and have AI generate one of these for you.

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