Custom indicators

Examples

Bollinger Bands (lookback), EMA (incremental), RSI (bands + filled zones) — reference indicators for the patterns every indicator falls into.

Three classic indicators, each illustrating a different pattern. Bollinger Bands and EMA are complete — copy the metadata and code straight into the editor. The RSI example shows the metadata, with the calculation described in prose.

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": 200 },
        "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

Need more than 200 bars of history? Incremental is the only option. Either way, gate mutations of committed state on isNewBar.

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

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