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
), 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).
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.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`.
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.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.
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.minusDiObv
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 namedhistdrawn as ahistogramis the kScript histogram plot.crossover(m.macd, m.signal)iscross.update(macd.macd, macd.signal)with the shippedCrossclass, which returns+1,-1, or0(Series functions).- The numbers are the engine's numbers: every class here is checked
bit-exact against the kScript engine, edge rules included (
Rsiat 100 on a flat window,Stochasticat 50,WprandCmoat 0). - A cumulative line (
Obv) is state the module owns, soreset()must zero it: the host replays the forming bar throughreset()on every tick, and a forgotten accumulator would double-count.