Oscillators

Oscillators measure momentum, overbought and oversold pressure, and trend strength. Every oscillator of the kScript (legacy) roster ships in every workspace as…

Oscillators measure momentum, overbought and oversold pressure, and trend strength. Every oscillator of the kScript (legacy) roster ships in every workspace as a stateful class in src/sdk/ta.ts: Rsi, Wpr, Cmo, Tsi, Macd, Stoch, Stochastic, Cci, Mfi, Mom, Change, Roc, Adx, and Obv, each one matching the kScript engine bar for bar (the full catalog is on the TA library page). Import the ones you use from ./sdk/ta. A multi-output oscillator exposes its streams as fields after update() (macd.signal, stoch.k, adx.plusDi) 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; Macd warms up over the slow average plus the signal period; Adx takes longest because it smooths directional movement twice. Leading bars are blank, then the line begins.

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 init() 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.

import { Rsi } from "./sdk/ta";

let rsi = new Rsi(14);
export function init(): 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.

import { Wpr } from "./sdk/ta";

let wpr = new Wpr(14);
export function init(): 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.

import { Cmo } from "./sdk/ta";

let cmo = new Cmo(14);
export function init(): 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.

import { Tsi } from "./sdk/ta";

let tsi = new Tsi(13, 25);
export function init(): 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 (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).
import { Macd } from "./sdk/ta";

let macd = new Macd(12, 26, 9);
export function init(): 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`.

kScript had two spellings with different rules, and both ship. new Stochastic(kPeriod = 14, kSmoothing = 3, dPeriod = 3) is the older stochastic(source, ...) builtin: 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 are matching a script that called stochastic.

import { Stoch } from "./sdk/ta";

let stoch = new Stoch(14, 3, 3);
export function init(): void { stoch = new Stoch(i32(p_period_k()), 3, 3); }
// after stoch.update(high, low, close): stoch.k, stoch.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.

import { Cci } from "./sdk/ta";

let cci = new Cci(20, 0.015);
export function init(): 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. It needs volume, so declare a volume input beside the prices.

import { Mfi } from "./sdk/ta";

let mfi = new Mfi(14);
export function init(): void { mfi = new Mfi(i32(p_period())); }

Mom, Change, 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. new Change(n = 1) is the same math under kScript's other name (its default lag is one bar).

import { Mom, Roc } from "./sdk/ta";

let mom = new Mom(10);
let roc = new Roc(10);
export function init(): void { mom = new Mom(i32(p_lag())); roc = new Roc(i32(p_lag())); }

Adx

new Adx(period = 14), .update(high, low, close): Wilder's directional movement system. update() returns the ADX line and fills the fields adx, plusDi, and minusDi. The true range, +DM, and -DM are seeded on the plain sum (not the average) of their first period values, then smoothed as s = s - s / period + x, the engine's own form rather than a Wilder average; plusDi and minusDi are the smoothed movements as a percent of the smoothed range (0 when that range is 0); DX is their normalized difference and adx is the plain average of the first period DX values, then (adx * (period - 1) + dx) / period. plusDi and minusDi appear at bar period, adx at bar 2 * period - 1, which is why it warms up last. After the seed a non-finite bar flows through the sums and the outputs stay NaN for the rest of the series. ADX above 25 reads as a strong trend, below 20 as a range; plusDi over minusDi is upward pressure.

import { Adx } from "./sdk/ta";

let adx = new Adx(14);
export function init(): void { adx = new Adx(i32(p_adx_period())); }
// after adx.update(high, low, close): adx.adx, adx.plusDi, adx.minusDi

Obv

new Obv(), .update(close, volume): on-balance volume, a running cumulative line with no period. Bar 0 returns 0; afterwards the bar's volume is added on an up close, subtracted on a down close, and ignored when the close is unchanged, so its slope tracks whether volume confirms price. reset() clears the running total.

import { Obv } from "./sdk/ta";

let obv = new Obv();
export function reset(): void { obv.reset(); }

A cumulative line remembers everything since the first loaded bar, so its level depends on how much history the host loaded; its slope does not.

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 comes from ./sdk/ta. state() abstains until the slowest line (Adx) is warm so the row and every metric on it start together; write NaN per output instead if you want the fast lines to appear first (Execution model).

import { histogram, input, line, lower, ohlcv, output, param } from "./sdk/declare";
import { in_close, in_high, in_low, in_volume } from "./gen/inputs";
import {
  emitRow,
  out_adx,
  out_cci,
  out_cmo,
  out_di_minus,
  out_di_plus,
  out_hist,
  out_macd,
  out_mfi,
  out_mom,
  out_obv,
  out_roc,
  out_rsi,
  out_signal,
  out_stoch_d,
  out_stoch_k,
  out_tsi,
  out_wpr,
} from "./gen/outputs";
import { p_adx_period, p_fast, p_rsi_period, p_signal, p_slow } from "./gen/params";
import { Adx, Cci, Cmo, Macd, Mfi, Mom, Obv, Roc, Rsi, Stoch, Tsi, Wpr } from "./sdk/ta";

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 });
input("close", ohlcv.close);
input("high", ohlcv.high);
input("low", ohlcv.low);
input("volume", ohlcv.volume);
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();
let rsiValue: f64 = NaN;
let wprValue: f64 = NaN;
let cmoValue: f64 = NaN;
let tsiValue: f64 = NaN;
let cciValue: f64 = NaN;
let mfiValue: f64 = NaN;
let momValue: f64 = NaN;
let rocValue: f64 = NaN;
let obvValue: f64 = NaN;

export function init(): 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();
}

export function state(): i32 {
  const close = in_close();
  const high = in_high();
  const low = in_low();
  const volume = in_volume();
  rsiValue = rsi.update(close);
  wprValue = wpr.update(high, low, close);
  cmoValue = cmo.update(close);
  tsiValue = tsi.update(close);
  macd.update(close);
  stoch.update(high, low, close);
  cciValue = cci.update(high, low, close);
  mfiValue = mfi.update(high, low, close, volume);
  momValue = mom.update(close);
  rocValue = roc.update(close);
  adx.update(high, low, close);
  obvValue = obv.update(close, volume);
  // ADX is the slowest line here; the row starts once it is warm so every metric begins together.
  return isNaN(adx.adx) ? 0 : 1;
}

export function finalize(): void {
  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);
  emitRow();
}

export function reset(): void {
  rsi.reset();
  wpr.reset();
  cmo.reset();
  tsi.reset();
  macd.reset();
  stoch.reset();
  cci.reset();
  mfi.reset();
  mom.reset();
  roc.reset();
  adx.reset();
  obv.reset();
  rsiValue = NaN;
  wprValue = NaN;
  cmoValue = NaN;
  tsiValue = NaN;
  cciValue = NaN;
  mfiValue = NaN;
  momValue = NaN;
  rocValue = NaN;
  obvValue = NaN;
}

Edge behavior: Wpr, Cmo, Tsi

The kScript page made two edges visible: %R is NaN until its window fills, and CMO returns 0 on a flat series. The same module shape shows both here 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.

import { input, line, lower, none, ohlcv, output, param } from "./sdk/declare";
import { in_close, in_high, in_low } from "./gen/inputs";
import { emitRow, out_cmo, out_cmo_flat, out_tsi, out_wpr, out_wpr_warm } from "./gen/outputs";
import { p_length } from "./gen/params";
import { Cmo, Tsi, Wpr } from "./sdk/ta";

param("length", 14, { min: 2, max: 200 });
input("close", ohlcv.close);
input("high", ohlcv.high);
input("low", ohlcv.low);
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);
let wprValue: f64 = NaN;
let cmoValue: f64 = NaN;
let cmoFlatValue: f64 = NaN;
let tsiValue: f64 = NaN;

export function init(): void {
  wpr = new Wpr(i32(p_length()));
  cmo = new Cmo(i32(p_length()));
  cmoFlat = new Cmo(5);
  tsi = new Tsi(13, 25);
}

export function state(): i32 {
  const close = in_close();
  wprValue = wpr.update(in_high(), in_low(), close);
  cmoValue = cmo.update(close);
  cmoFlatValue = cmoFlat.update(100.0);
  tsiValue = tsi.update(close);
  return 1;
}

export function finalize(): void {
  out_wpr(wprValue);
  out_cmo(cmoValue);
  out_tsi(tsiValue);
  out_wpr_warm(isNaN(wprValue) ? 0.0 : 1.0);
  out_cmo_flat(cmoFlatValue);
  emitRow();
}

export function reset(): void {
  wpr.reset();
  cmo.reset();
  cmoFlat.reset();
  tsi.reset();
  wprValue = NaN;
  cmoValue = NaN;
  cmoFlatValue = NaN;
  tsiValue = NaN;
}

Warm-up in practice

To see warm-up directly, draw a few oscillators and watch 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 NaN per output instead of abstaining, so the pane shows each line switching on at its own bar.

import { input, line, lower, ohlcv, output, param } from "./sdk/declare";
import { in_close, in_high, in_low } from "./gen/inputs";
import { emitRow, out_adx, out_macd, out_rsi, out_stoch_k } from "./gen/outputs";
import { p_period } from "./gen/params";
import { Adx, Macd, Rsi, Stoch } from "./sdk/ta";

param("period", 14, { min: 2, max: 200 });
input("close", ohlcv.close);
input("high", ohlcv.high);
input("low", ohlcv.low);
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);
let rsiValue: f64 = NaN;

export function init(): 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);
}

export function state(): i32 {
  const close = in_close();
  rsiValue = rsi.update(close);
  macd.update(close);
  stoch.update(in_high(), in_low(), close);
  adx.update(in_high(), in_low(), close);
  // Every row is ready; each output carries NaN until its own class is warm.
  return 1;
}

export function finalize(): void {
  out_rsi(rsiValue);
  out_macd(macd.macd);
  out_stoch_k(stoch.k);
  out_adx(adx.adx);
  emitRow();
}

export function reset(): void {
  rsi.reset();
  macd.reset();
  stoch.reset();
  adx.reset();
  rsiValue = NaN;
}

What changed from kScript

  • Every oscillator that read the chart's OHLC implicitly (stoch(), supertrend()) takes the fields it needs as arguments, and every field is a declared input. Nothing is read by default.
  • macd.histogram, stoch.d, and the [ADX, DI+, DI-] tuple are fields on the class and one output each. An output named hist drawn as a histogram is the kScript histogram plot.
  • crossover(m.macd, m.signal) is cross.update(macd.macd, macd.signal) with the shipped Cross class, which returns +1, -1, or 0 (Series functions).
  • The numbers are the engine's numbers: every class here is checked bit-exact against the kScript engine, edge rules included (Rsi at 100 on a flat window, Stochastic at 50, Wpr and Cmo at 0).
  • A cumulative line (Obv) is state the module owns, so reset() must zero it: the host replays the forming bar through reset() on every tick, and a forgotten accumulator would double-count.