Oscillators

View as MarkdownOpen the editor

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.

wrun
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.

wrun
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.

wrun
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.

wrun
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), and histismacd - signalwhen both are finite. A non-finite input after a seed poisons that leg toNaN. Draw the histogram as a histogramoutput around zero, or test a signal-line cross withCross.update(macd.macd, macd.signal)` (Series functions).
wrun
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.hist

Stoch 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)overperiodK bars (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; read kanddas fields. Firstkat barperiodK + smoothK - 2, first dat 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.

wrun
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.d

Both 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).

wrun
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.d

Cci

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.

wrun
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.

wrun
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.

wrun
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);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

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);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

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.macd is not) is a weakening trend; the hist output 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 (Rsi at 100 on a flat window, Stochastic at 50, Wpr and Cmo at 0).