Indicator scripting
Examples
Working snippets for SMA, EMA, Bollinger Bands, Donchian, VWAP, RSI.
Simple moving average
// SMA(20) drawn as a single polyline.
const closes = candles.map(c => c.close);
function sma(values, p) {
const out = [];
let sum = 0;
for (let i = 0; i < values.length; i++) {
sum += values[i];
if (i >= p) sum -= values[i - p];
out.push(i >= p - 1 ? sum / p : NaN);
}
return out;
}
const line = sma(closes, 20);
return {
outputs: [
{ name: 'sma20', value: line[line.length - 1], color: 'azure' },
],
series: [
{ name: 'SMA 20', values: line, color: '#3b82c4' },
],
};EMA
// EMA(21) — single line, NaN warmup.
const closes = candles.map(c => c.close);
const period = 21;
const k = 2 / (period + 1);
const out = [];
let prev = NaN;
for (let i = 0; i < closes.length; i++) {
const v = closes[i];
prev = i === 0 ? v : v * k + prev * (1 - k);
out.push(i >= period - 1 ? prev : NaN);
}
return {
outputs: [{ name: 'ema21', value: out[out.length - 1], color: 'azure' }],
series: [{ name: 'EMA 21', values: out, color: '#1e7a8c' }],
};Bollinger Bands (20·2)
const closes = candles.map(c => c.close);
const period = 20, mult = 2;
const mid = [], up = [], lo = [];
for (let i = 0; i < closes.length; i++) {
if (i < period - 1) { mid.push(NaN); up.push(NaN); lo.push(NaN); continue; }
let s = 0;
for (let j = i - period + 1; j <= i; j++) s += closes[j];
const m = s / period;
let v = 0;
for (let j = i - period + 1; j <= i; j++) v += (closes[j] - m) ** 2;
const sd = Math.sqrt(v / period);
mid.push(m); up.push(m + mult * sd); lo.push(m - mult * sd);
}
return {
outputs: [
{ name: 'mid', value: mid[mid.length - 1], color: 'ink' },
{ name: 'upper', value: up[up.length - 1], color: 'down' },
{ name: 'lower', value: lo[lo.length - 1], color: 'up' },
],
series: [
{ name: 'Mid', values: mid, color: '#8a4b2a' },
{ name: 'Upper', values: up, color: '#c4554c' },
{ name: 'Lower', values: lo, color: '#3f8a6e' },
],
};Donchian Channels (20)
const period = 20;
const hi = [], lo = [];
for (let i = 0; i < candles.length; i++) {
if (i < period - 1) { hi.push(NaN); lo.push(NaN); continue; }
let h = -Infinity, l = Infinity;
for (let j = i - period + 1; j <= i; j++) {
if (candles[j].high > h) h = candles[j].high;
if (candles[j].low < l) l = candles[j].low;
}
hi.push(h); lo.push(l);
}
return {
outputs: [
{ name: 'hi', value: hi[hi.length - 1], color: 'up' },
{ name: 'lo', value: lo[lo.length - 1], color: 'down' },
],
series: [
{ name: 'High', values: hi, color: '#3f8a6e' },
{ name: 'Low', values: lo, color: '#c4554c' },
],
};Cumulative VWAP
// Volume-weighted average of typical price, cumulative across history.
const out = [];
let pv = 0, vv = 0;
for (const c of candles) {
const tp = (c.high + c.low + c.close) / 3;
const v = c.volume ?? 0;
pv += tp * v;
vv += v;
out.push(vv > 0 ? pv / vv : NaN);
}
return {
outputs: [{ name: 'vwap', value: out[out.length - 1], color: 'azure' }],
series: [{ name: 'VWAP', values: out, color: '#5b6cc4' }],
};RSI(14) as a scalar
// RSI is bounded 0–100, so it makes more sense as a chip readout
// than as a polyline overlaid on price. Return a single scalar.
const closes = candles.map(c => c.close);
const period = 14;
let up = 0, dn = 0;
for (let i = 1; i <= period; i++) {
const d = closes[i] - closes[i - 1];
if (d > 0) up += d; else dn -= d;
}
let rs = up / (dn || 1);
let rsi = 100 - 100 / (1 + rs);
for (let i = period + 1; i < closes.length; i++) {
const d = closes[i] - closes[i - 1];
const u = d > 0 ? d : 0, dd = d < 0 ? -d : 0;
up = (up * (period - 1) + u) / period;
dn = (dn * (period - 1) + dd) / period;
rs = up / (dn || 1);
rsi = 100 - 100 / (1 + rs);
}
return { name: 'rsi14', value: rsi, color: rsi > 70 ? 'down' : rsi < 30 ? 'up' : 'azure' };