---
title: "Oscillators"
description: "Oscillators measure momentum, overbought and oversold pressure, and trend strength. Every oscillator of the kScript (legacy) roster ships in every workspace as…"
order: 33
section: "functions"
---

<!-- source: docs/indicators/functions/oscillators.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# Oscillators

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](ta-library.md) 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.

```typescript
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`.

```typescript
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.

```typescript
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`.

```typescript
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 `hist` is `macd - signal` when both are finite. A non-finite
input after a seed poisons that leg to `NaN`. Draw the histogram as a
`histogram` output around zero, or test a signal-line cross with
`Cross.update(macd.macd, macd.signal)` ([Series functions](series-functions.md)).

```typescript
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)` over `periodK` bars (`0` on a flat window, `NaN` while the
window is short or holds a non-finite value), `k` is the strict simple
average of the last `smoothK` raw values, and `d` is the same average of the
last `periodD` values of `k`. `update()` returns `k`; read `k` and `d` as
fields. First `k` at bar `periodK + smoothK - 2`, first `d` at bar `periodK
+ 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`.

```typescript
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`.

```typescript
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.

```typescript
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).

```typescript
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.

```typescript
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.

```typescript
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](../core-concepts/execution-model.md)).

```typescript
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.

```typescript
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.

```typescript
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](series-functions.md)).
- 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.
