RSI
The RSI indicator from the public library (rsi), written against the current DSL.
What it plots
A 0 to 100 oscillator in its own pane (Wilder RSI), with overbought and oversold guide lines.
Key decisions
Piece | Why it matters |
|---|---|
Seed from the first | Classic RSI, not a plain average of changes. |
| Wilder smoothing needs about 50 times its period before the starting point stops showing. |
Guide levels plotted as lines |
|
| No down moves means RSI 100, not a division by zero. |
Full script
meta({
shortName: "RSI",
kind: "pane",
dataSource: "candles",
category: "custom",
});
dialog({
title: "RSI Configuration",
description: "Configure Relative Strength Index (RSI) settings",
width: 400,
height: 400,
resizable: false,
showEnabledCheckbox: true,
resetToDefaults: {
enabled: true,
confirmationMessage: "Are you sure you want to reset all RSI settings to their default values?",
},
});
const period = input.number({
id: "period",
label: "Period",
default: 14,
min: 1,
step: 1,
});
const overbought = input.number({
id: "overbought",
label: "Overbought Level",
default: 70,
min: 0,
max: 100,
step: 1,
});
const oversold = input.number({
id: "oversold",
label: "Oversold Level",
default: 30,
min: 0,
max: 100,
step: 1,
});
const lineColor = input.color({
id: "lineColor",
label: "Line Color",
default: "#2962FF",
allowAlpha: true,
});
const lineStyle = input.lineStyle({
id: "lineStyle",
label: "Line Style",
default: LineStyle.Solid,
});
const labelsEnabled = input.boolean({
id: "labelsEnabled",
label: "Show Labels",
default: true,
});
warmup((w) => w.wilder(period));
layout(
tab({ id: "general", title: "General" },
section({ id: "parameters", title: "Parameters" }, period, overbought, oversold),
section({ id: "display", title: "Display Options" }, labelsEnabled),
),
tab({ id: "appearance", title: "Appearance" },
section({ id: "line", title: "Line", columns: 2 }, lineColor, lineStyle),
),
);
output.line({
id: "rsi",
lineWidth: 2,
colorFrom: lineColor,
lineStyleFrom: lineStyle,
labelFrom: labelsEnabled,
titleWhenLabeled: "RSI",
});
output.line({
id: "overbought",
lineWidth: 1,
colorFrom: lineColor,
lineStyleFrom: lineStyle,
labelFrom: labelsEnabled,
titleWhenLabeled: "Overbought",
});
output.line({
id: "oversold",
lineWidth: 1,
colorFrom: lineColor,
lineStyleFrom: lineStyle,
labelFrom: labelsEnabled,
titleWhenLabeled: "Oversold",
});
// onBar runs again for the forming bar on every tick. The chart rolls ctx.accum back between
// ticks; foldBar does not depend on that, so the same code is right on every runtime.
// foldBar keeps two states: `done` (as of the last closed bar) and `tip` (this bar). `step`
// builds the tip from `done` without mutating it, so a repeat of the same bar recomputes the tip
// instead of stacking on top of it. A new bar time commits the tip.
function foldBar(ctx, id, init, step) {
const s = ctx.accum(id, () => ({ t: null, done: init, tip: init }), (s) => {
if (s.t !== ctx.time) {
s.done = s.tip;
s.t = ctx.time;
}
s.tip = step(s.done);
return s;
});
return s.tip;
}
function mean(values) {
let sum = 0;
for (const v of values) sum += v;
return sum / values.length;
}
/** @param {ScriptedCtx} ctx */
export function onBar(ctx) {
const p = ctx.params.period;
const st = foldBar(ctx, "rsi", { close: null, gains: [], losses: [], avgGain: null, avgLoss: null }, (prev) => {
if (prev.close == null) {
return { close: ctx.close, gains: [], losses: [], avgGain: null, avgLoss: null };
}
const change = ctx.close - prev.close;
const gain = Math.max(change, 0);
const loss = Math.max(-change, 0);
if (prev.avgGain == null) {
// Collect the first `p` changes, then seed both averages with their mean.
const gains = prev.gains.concat(gain);
const losses = prev.losses.concat(loss);
if (gains.length < p) return { close: ctx.close, gains, losses, avgGain: null, avgLoss: null };
return { close: ctx.close, gains: [], losses: [], avgGain: mean(gains), avgLoss: mean(losses) };
}
// Wilder smoothing from then on.
return {
close: ctx.close,
gains: [],
losses: [],
avgGain: (prev.avgGain * (p - 1) + gain) / p,
avgLoss: (prev.avgLoss * (p - 1) + loss) / p,
};
});
if (st.avgGain == null) return;
ctx.plot("overbought", ctx.params.overbought);
ctx.plot("oversold", ctx.params.oversold);
// No down moves in the window: RSI is 100, not a division by zero.
ctx.plot("rsi", st.avgLoss === 0 ? 100 : 100 - 100 / (1 + st.avgGain / st.avgLoss));
}Related
Warmup and memory for the Wilder warmup, and Indicator API for drawing in its own pane.
