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 only | Lookback |
| Today's value depends on yesterday's value | Incremental |
| Needs more than 200 bars of history | Incremental (the ring buffer can't help you) |
| Needs to react to live ticks without overcounting | Either, 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.