Oscillators
Oscillators measure momentum and overbought or oversold pressure. Every
oscillator ships with the editor as a stateful class in ./sdk/ta:
Rsi, Wpr, Cmo, Tsi, Macd, Stoch, Stochastic, Cci, Mfi,
Mom, and Roc (the full catalog is on the TA library
page).
Name the ones you use; there is nothing to import. A multi-output
oscillator exposes its streams as fields after update() (macd.signal,
stoch.k) and each one goes to its own output.
Every oscillator needs a warm-up window. Until enough bars have loaded to
fill its longest period the value is NaN and nothing draws. Rsi(14) is
NaN for its seed window and Macd warms up over the slow average plus
the signal period. Leading bars are blank, then the line begins. The
compiled modules below also fold two classes that live elsewhere: Adx,
which takes longest because it smooths directional movement twice, on
Trend and volatility, and Obv on
Volume and VWAP.
Reference
Every class allocates in its constructor, never in update(), and
reset() restores the just-constructed state. A period below 1 is
clamped to 1. Construct in onStart() from a param (params are f64,
periods are i32, so new Rsi(i32(p_rsi_period()))).
Rsi
new Rsi(period), .update(x): the relative strength index, bounded
0..100. Wilder smoothing (the gain and loss averages of the first period
one-bar changes seed it, then avg = (avg * (period - 1) + gain) / period),
first value at bar period. The ratio step is 100 - 100 / (1 + gain / loss), and a zero average loss returns 100: a flat window reads 100,
never 50; all-loss reads 0. A non-finite input before the seed restarts the
seed count; after the seed it makes the value NaN for good.
let rsi = new Rsi(14);
function onStart(): void { rsi = new Rsi(i32(p_rsi_period())); }Wpr
new Wpr(length = 14), .update(high, low, close): Williams %R over a
high/low window, -100 * (highest high - close) / (highest high - lowest low), bounded -100..0. NaN for the first length - 1 bars and whenever
any high or low in the window (or the close) is non-finite; a flat window
(highest equals lowest) returns 0.
let wpr = new Wpr(14);
function onStart(): void { wpr = new Wpr(i32(p_length())); }Cmo
new Cmo(length = 9), .update(x): the Chande momentum oscillator
compares summed gains and losses over the last length one-bar changes,
100 * (up - down) / (up + down). NaN until bar length (the first bar
has no previous value), 0 when up + down == 0 (a flat window), and
NaN when any value in the window, or the bar before it, is non-finite.
let cmo = new Cmo(14);
function onStart(): void { cmo = new Cmo(i32(p_length())); }Tsi
new Tsi(short = 13, long = 25), .update(x): the true strength index
double-smooths one-bar momentum and its absolute value with an EMA of
long then an EMA of short, and returns their ratio times 100. The
engine's parameter order is short first, then long. Each EMA stage
seeds on the mean of its first period finite inputs, so the first value
lands at bar long + short - 1; the ratio is NaN when either stage is
not finite or the denominator is 0.
let tsi = new Tsi(13, 25);
function onStart(): void { tsi = new Tsi(i32(p_short()), i32(p_long())); }Macd
new Macd(fastPeriod = 12, slowPeriod = 26, signalPeriod = 9),
.update(x): returns the MACD line and fills the fields macd, signal,
and hist. The MACD line is the fast EMA minus the slow EMA, each leg
seeded on the mean of its first period finite inputs (first value at
bar slowPeriod - 1), the signal is an EMA of that line seeded on its first
signalPeriod finite values (first value at bar `slowPeriod + signalPeriod
- 2
), andhistismacd - signalwhen both are finite. A non-finite input after a seed poisons that leg toNaN. Draw the histogram as ahistogramoutput around zero, or test a signal-line cross withCross.update(macd.macd, macd.signal)` (Series functions).
let macd = new Macd(12, 26, 9);
function onStart(): void { macd = new Macd(i32(p_fast()), i32(p_slow()), i32(p_signal())); }
// after macd.update(close): macd.macd, macd.signal, macd.histStoch and Stochastic
new Stoch(periodK, smoothK, periodD), .update(high, low, close): the
stochastic oscillator. Raw %K is `100 * (close - lowest low) / (highest high
- lowest low)
overperiodKbars (0on a flat window,NaNwhile the window is short or holds a non-finite value),kis the strict simple average of the lastsmoothKraw values, anddis the same average of the lastperiodDvalues ofk.update()returnsk; readkanddas fields. Firstkat barperiodK + smoothK - 2, firstdat barperiodK
- smoothK + periodD - 3`.
Two spellings with different edge rules ship. new Stochastic(kPeriod = 14, kSmoothing = 3, dPeriod = 3) is the other
one: bars before kPeriod - 1 report k = 0
and d = 0 (not NaN), a flat window reads 50, k is the raw value
itself until the smoothing window fills, d equals k until its own
window fills, and a NaN k or d is reported as 50. Reach for Stoch
unless you want those edge rules.
let stoch = new Stoch(14, 3, 3);
function onStart(): void { stoch = new Stoch(i32(p_period_k()), 3, 3); }
// after stoch.update(high, low, close): stoch.k, stoch.dBoth read the close as the source. Pine's ta.stoch(source, high, low, length) takes any source: SourceStoch in ./sdk/ta-plus
(Extra indicators) moves the source into
update(source, high, low) and keeps Stoch's smoothing and fields;
fed the close it is Stoch on every window with a range (on a flat
window Stoch reads 0 where SourceStoch follows TradingView: the
previous raw %K when the source sits on the window's value, NaN
otherwise).
let stochHl2 = new SourceStoch(14, 3, 3);
function onStart(): void { stochHl2 = new SourceStoch(i32(p_period_k()), 3, 3); }
// in onBar(): stochHl2.update((bar.high() + bar.low()) / 2.0, bar.high(), bar.low()); stochHl2.k, stochHl2.dCci
new Cci(period = 20, constant = 0.015), .update(high, low, close):
the commodity channel index over the typical price (high + low + close) / 3, (tp - sma) / (constant * meanDev) where sma is the window mean of
the typical price and meanDev the mean absolute deviation around it.
Readings beyond +100 and -100 mark momentum extremes. NaN for the first
period - 1 bars; 0 when the mean deviation is 0.
let cci = new Cci(20, 0.015);
function onStart(): void { cci = new Cci(i32(p_period()), 0.015); }Mfi
new Mfi(period = 14), .update(high, low, close, volume): the money
flow index, a volume-weighted RSI bounded 0..100. Each bar's raw flow is
typical price * volume, added to the positive sum when the typical price
rose against the previous bar, to the negative sum when it fell, and to
neither when equal; the result is 100 - 100 / (1 + positive / negative),
and a zero negative sum returns 100. NaN for the first period bars;
a non-finite bar fails both comparisons and adds nothing, as in the
engine. It needs volume, so pass bar.volume() beside the prices. Above
80 reads overbought and below 20 oversold; price at a new high while the
index above 80 is not is a classic exhaustion read.
let mfi = new Mfi(14);
function onStart(): void { mfi = new Mfi(i32(p_period())); }Mom and Roc
new Roc(n), .update(x): rate of change in percent, ((x - x[n]) / x[n]) * 100, NaN for the first n bars and when the lagged value is
0. new Mom(n), .update(x): momentum, the raw difference x - x[n],
NaN for the first n bars and when either value is non-finite. Change
is the same math under another name with a one-bar default lag; it lives
with the series helpers on
Series functions.
let mom = new Mom(10);
let roc = new Roc(10);
function onStart(): void { mom = new Mom(i32(p_lag())); roc = new Roc(i32(p_lag())); }Putting them together
One module wiring every oscillator above into a lower pane, the
multi-output ones (Macd, Stoch, Adx) written stream by stream. Every
class is used by name with nothing to import. onBar() returns before
writing until the slowest line (Adx) is warm so every output starts
together; write each value as it comes instead if you want the fast lines
to appear first, since a NaN draws nothing
(Execution model).
param("rsi_period", 14, { min: 2, max: 200 });
param("fast", 12, { min: 1, max: 200, description: "MACD fast EMA" });
param("slow", 26, { min: 2, max: 400, description: "MACD slow EMA" });
param("signal", 9, { min: 1, max: 200, description: "MACD signal EMA" });
param("adx_period", 14, { min: 1, max: 200 });
output("rsi", line, lower, { color: "#7c3aed", width: 2, description: "Relative strength index" });
output("wpr", line, lower, { color: "#2563eb", width: 1, description: "Williams %R" });
output("cmo", line, lower, { color: "#16a34a", width: 1, description: "Chande momentum oscillator" });
output("tsi", line, lower, { color: "#9333ea", width: 1, description: "True strength index" });
output("macd", line, lower, { color: "#1d4ed8", width: 2, description: "MACD line" });
output("signal", line, lower, { color: "#ea580c", width: 2, description: "MACD signal" });
output("hist", histogram, lower, { color: "#15803d", description: "MACD histogram" });
output("stoch_k", line, lower, { color: "#0e7490", width: 2, description: "Stochastic %K" });
output("stoch_d", line, lower, { color: "#be123c", width: 2, description: "Stochastic %D" });
output("cci", line, lower, { color: "#4b5563", width: 1, description: "Commodity channel index" });
output("mfi", line, lower, { color: "#16a34a", width: 2, description: "Money flow index" });
output("mom", line, lower, { color: "#9333ea", width: 1, description: "Momentum over 10 bars" });
output("roc", line, lower, { color: "#0891b2", width: 1, unit: "%", description: "Rate of change over 10 bars" });
output("adx", line, lower, { color: "#111827", width: 2, description: "Average directional index" });
output("di_plus", line, lower, { color: "#2563eb", width: 1, description: "+DI" });
output("di_minus", line, lower, { color: "#dc2626", width: 1, description: "-DI" });
output("obv", line, lower, { color: "#0f766e", width: 1, description: "On-balance volume" });
let rsi = new Rsi(14);
let wpr = new Wpr(14);
let cmo = new Cmo(14);
let tsi = new Tsi(13, 25);
let macd = new Macd(12, 26, 9);
let stoch = new Stoch(14, 3, 3);
let cci = new Cci(20, 0.015);
let mfi = new Mfi(14);
let mom = new Mom(10);
let roc = new Roc(10);
let adx = new Adx(14);
let obv = new Obv();
function onStart(): void {
rsi = new Rsi(i32(p_rsi_period()));
wpr = new Wpr(14);
cmo = new Cmo(14);
tsi = new Tsi(13, 25);
macd = new Macd(i32(p_fast()), i32(p_slow()), i32(p_signal()));
stoch = new Stoch(14, 3, 3);
cci = new Cci(20, 0.015);
mfi = new Mfi(14);
mom = new Mom(10);
roc = new Roc(10);
adx = new Adx(i32(p_adx_period()));
obv = new Obv();
}
function onBar(): void {
const close = bar.close();
const high = bar.high();
const low = bar.low();
const volume = bar.volume();
const rsiValue = rsi.update(close);
const wprValue = wpr.update(high, low, close);
const cmoValue = cmo.update(close);
const tsiValue = tsi.update(close);
macd.update(close);
stoch.update(high, low, close);
const cciValue = cci.update(high, low, close);
const mfiValue = mfi.update(high, low, close, volume);
const momValue = mom.update(close);
const rocValue = roc.update(close);
adx.update(high, low, close);
const obvValue = obv.update(close, volume);
// ADX is the slowest line here; return before writing until it is warm so every output begins together.
if (isNaN(adx.adx)) return;
out_rsi(rsiValue);
out_wpr(wprValue);
out_cmo(cmoValue);
out_tsi(tsiValue);
out_macd(macd.macd);
out_signal(macd.signal);
out_hist(macd.hist);
out_stoch_k(stoch.k);
out_stoch_d(stoch.d);
out_cci(cciValue);
out_mfi(mfiValue);
out_mom(momValue);
out_roc(rocValue);
out_adx(adx.adx);
out_di_plus(adx.plusDi);
out_di_minus(adx.minusDi);
out_obv(obvValue);
}Edge behavior: Wpr, Cmo, Tsi
Two edges are worth seeing: %R is NaN until its window fills, and CMO
returns 0 on a flat series. One module shows both with the shipped
classes. flat is a series held at 100 on
every bar, so its Cmo changes are all zero and the class returns 0 once
its window is full; wpr_warm is 1 on the bars where the class has an
answer and 0 before, so the boundary is a step you can read off the pane.
param("length", 14, { min: 2, max: 200 });
output("wpr", line, lower, { color: "#2563eb", width: 2, description: "Williams %R" });
output("cmo", line, lower, { color: "#16a34a", width: 2, description: "Chande momentum oscillator" });
output("tsi", line, lower, { color: "#7c3aed", width: 2, description: "True strength index, short 13 long 25" });
output("wpr_warm", none, lower, { description: "1 once %R has a full window, 0 before" });
output("cmo_flat", line, lower, { color: "#ea580c", width: 1, description: "CMO of a flat series: 0 once its window fills" });
let wpr = new Wpr(14);
let cmo = new Cmo(14);
let cmoFlat = new Cmo(5);
let tsi = new Tsi(13, 25);
function onStart(): void {
wpr = new Wpr(i32(p_length()));
cmo = new Cmo(i32(p_length()));
cmoFlat = new Cmo(5);
tsi = new Tsi(13, 25);
}
function onBar(): void {
const close = bar.close();
const wprValue = wpr.update(bar.high(), bar.low(), close);
const cmoValue = cmo.update(close);
const cmoFlatValue = cmoFlat.update(100.0);
const tsiValue = tsi.update(close);
out_wpr(wprValue);
out_cmo(cmoValue);
out_tsi(tsiValue);
out_wpr_warm(isNaN(wprValue) ? 0.0 : 1.0);
out_cmo_flat(cmoFlatValue);
}Warm-up in practice
To see warm-up directly, draw a few oscillators and look at where each line
begins. The leading gap is the seed window: each line stays blank until its
longest period has enough bars, then turns finite. Adx starts last (bar
2 * period - 1), Rsi first (bar period), Stoch at bar period + 1
with its default smoothing of 3, and Macd once its slow EMA is seeded.
This module writes every output on every bar (a NaN draws nothing)
instead of returning early, so the pane shows each line switching on at
its own bar.
param("period", 14, { min: 2, max: 200 });
output("rsi", line, lower, { color: "#7c3aed", width: 2, description: "Finite from bar period on" });
output("macd", line, lower, { color: "#2563eb", width: 2, description: "Finite once the slow EMA is seeded" });
output("stoch_k", line, lower, { color: "#0891b2", width: 2, description: "Finite after the %K window plus smoothing" });
output("adx", line, lower, { color: "#111827", width: 2, description: "Finite after two smoothing windows" });
let rsi = new Rsi(14);
let macd = new Macd(12, 26, 9);
let stoch = new Stoch(14, 3, 3);
let adx = new Adx(14);
function onStart(): void {
const period = i32(p_period());
rsi = new Rsi(period);
macd = new Macd(12, 26, 9);
stoch = new Stoch(period, 3, 3);
adx = new Adx(period);
}
function onBar(): void {
const close = bar.close();
const rsiValue = rsi.update(close);
macd.update(close);
stoch.update(bar.high(), bar.low(), close);
adx.update(bar.high(), bar.low(), close);
// Every bar has a row; each output carries NaN until its own class is warm.
out_rsi(rsiValue);
out_macd(macd.macd);
out_stoch_k(stoch.k);
out_adx(adx.adx);
}Points to remember
- Every oscillator takes the fields it needs as arguments, each one read
from the bar (
bar.high(),bar.low(),bar.close(),bar.volume()); reading a field is what puts it on the sheet. macd.hist,stoch.d, and the ADX trio are fields on the class and one output each.- A MACD divergence (price at a new high while
macd.macdis not) is a weakening trend; thehistoutput shrinking toward zero says the same thing earlier. - Every class here is checked bit-exact against the reference run on the
TA library page, edge rules included (
Rsiat 100 on a flat window,Stochasticat 50,WprandCmoat 0).