TA library

View as MarkdownOpen the editor

Every wrun indicator can use ./sdk/ta, which ships with the editor: 52 stateful classes covering averages, statistics, oscillators, ranges, trend systems, and events.

Each class folds one bar per update() call, keeps its own window, and has fixed arithmetic, warm-up, and missing-value rules, checked bit-exact against a reference run. This page is the catalog: every class with its constructor, its update() arguments, its fields, and the one convention you need to know about it. The function pages (Moving averages, Oscillators, Trend and volatility, Volume and VWAP, Series functions) go deeper on each group with compiled examples.

Two sibling modules pick up where the catalog stops, there with classes of the same shape: ./sdk/ta-plus adds the textbook indicators the catalog never had (the double and triple EMAs, TRIX, Kaufman's adaptive average, the ultimate oscillator, Vortex, Aroon, choppiness, the volume lines and more), and ./sdk/stats adds allocation-free list math over a StaticArray<f64> plus History, the x[n] window every indicator needs sooner or later. Both are on Extra indicators; the kits for text, time, colors, order flow, levels and market structure sit beside it in this section.

How every class works

  • Allocate in the constructor, never in update(), so per-bar memory stays flat.
  • update(...) folds one bar and returns the current value, NaN until the class is warm (a few classes report a partial window from bar 0 instead; the tables say which).
  • Multi-output classes fill fields. update() returns the primary line and the other lines sit in public fields you read after the call.
  • reset() restores the just-constructed state. Nothing on the chart calls it for you: a revised forming bar replays from a snapshot of the module taken after the last closed bar, so a live update never folds the same bar twice.
  • Periods are i32, params are f64. Construct in onStart() from a param (new Sma(i32(p_period()))), keep the object in a module-level let, and update it once per bar in onBar(). A period below 1 is clamped to 1 (except Donchian, which uses the period as given).

The shipped classes

Averages and smoothing

ClassConstructPer barConvention
Smanew Sma(period).update(x)plain window mean; NaN until period bars exist and whenever the window holds a non-finite value
Emanew Ema(period).update(x)the mean of the first period finite values seeds it, then x * alpha + prev * (1 - alpha) with alpha = 2 / (period + 1); a non-finite input after the seed makes it NaN for good
Rmanew Rma(period).update(x)Wilder smoothing, the accumulator inside Rsi and Atr: same seed as Ema, then (prev * (period - 1) + x) / period
Wmanew Wma(period).update(x)linear weights, the newest value weighs period, the oldest 1; strict window
Hmanew Hma(period).update(x)wma(2 * wma(x, round(period / 2)) - wma(x, period), round(sqrt(period))); first value at bar period - 1 + round(sqrt(period)) - 1
Vwmanew Vwma(period).update(x, volume)sum(x * volume) / sum(volume); NaN when the volume sum is 0
Almanew Alma(length, offset = 0.85, sigma = 6).update(x)Gaussian weights centered at offset * (length - 1), width length / sigma; strict window
Swmanew Swma().update(x)(x[3] + 2 x[2] + 2 x[1] + x[0]) / 6 with x[0] the newest; first value at bar 3
Linregnew Linreg(period, offset = 0).update(x)least-squares line through the window evaluated at period - 1 - offset (offset truncated to an integer); strict window

Statistics and series math

ClassConstructPer barConvention
Sumnew Sum(period).update(x)no warm-up: bar 0 already returns the partial window; NaN inputs are skipped
Mediannew Median(period).update(x)middle of the sorted window, the mean of the two middle values on an even period; strict window
Percentilenew Percentile(period, pct).update(x)nearest rank: rank = ceil(pct / 100 * period), result sorted[max(0, rank - 1)]; pct clamped to 0..100; strict window
Variancenew Variance(period).update(x)population variance (divide by period, not period - 1); strict window
Stdevnew Stdev(period).update(x)population standard deviation, the square root of Variance; strict window
Zscorenew Zscore(period).update(x)(x - mean) / stdev over the window; NaN bars inside the window are skipped for the sums, the variance still divides by period; 0 when the deviation is exactly 0
Correlationnew Correlation(period).update(a, b)Pearson correlation of two series; NaN when either window holds a non-finite value or the denominator is 0
Changenew Change(n = 1).update(x)x - x[n]; NaN for the first n bars
Momnew Mom(n).update(x)the same math as Change under another name
Rocnew Roc(period).update(x)(x - x[n]) / x[n] * 100; NaN for the first n bars and when x[n] is 0
Cumnew Cum().update(x)running sum from bar 0; a non-finite bar makes it NaN for good
Fixnannew Fixnan().update(x)repeats the last finite value over a non-finite bar; NaN until the first finite value

Oscillators and momentum

ClassConstructPer barConvention
Rsinew Rsi(period).update(x)Wilder RSI; bar 0 feeds nothing, first value at bar period; a zero average loss returns 100, so a flat window is 100
Cmonew Cmo(length).update(x)100 * (up - down) / (up + down) over the last length changes; first value at bar length; a zero total returns 0
Tsinew Tsi(short = 13, long = 25).update(x)double EMA (long, then short) of momentum over the double EMA of its absolute value; the engine's parameter order is (short, long); first value at bar long + short - 1
Ccinew Cci(period = 20, constant = 0.015).update(high, low, close)over typical price (high + low + close) / 3; first value at bar period - 1; 0 when the mean deviation is 0
Mfinew Mfi(period = 14).update(high, low, close, volume)money flow over typical price times volume; first value at bar period; a zero negative flow returns 100
Wprnew Wpr(length = 14).update(high, low, close)Williams %R; first value at bar length - 1; a flat window returns 0
Stochnew Stoch(periodK, smoothK, periodD).update(high, low, close) returns kfields k, d; raw %K is 0 on a flat window; first k at bar periodK + smoothK - 2, first d at bar periodK + smoothK + periodD - 3
Stochasticnew Stochastic(kPeriod = 14, kSmoothing = 3, dPeriod = 3).update(high, low, close) returns kthe other spelling, with its own rules: k = d = 0 before bar kPeriod - 1 (never NaN), 50 on a flat window, a NaN k or d is reported as 50
Macdnew Macd(fast = 12, slow = 26, signal = 9).update(x) returns macdfields macd, signal, hist; the line appears at bar slow - 1, the signal at bar slow + signal - 2; hist = macd - signal
Obvnew Obv().update(close, volume)on-balance volume; bar 0 returns 0; an unchanged close adds nothing

Ranges and bands

ClassConstructPer barConvention
Trnew Tr().update(high, low, close)max(high - low, abs(high - prevClose), abs(low - prevClose)); bar 0 is plain high - low
Atrnew Atr(period = 14).update(high, low, close)Rma of Tr; first value at bar period - 1; a non-finite range after the seed makes it NaN for good
Bbnew Bb(period, mult).update(x) returns basisfields basis, upper, lower; Sma basis and population Stdev width; all three NaN while the window holds a non-finite value
Keltnernew Keltner(period, mult, atrPeriod).update(x, high, low, close) returns basisfields basis, upper, lower; Ema basis plus Atr width; the basis is reported as soon as the EMA is seeded, the bands once the ATR is finite
Donchiannew Donchian(period = 12).update(high, low) returns basisfields basis (the midline update() returns), upper, lower; no warm-up, the window is partial at the start; older NaN highs and lows are skipped, the current bar's NaN propagates
Highestnew Highest(period = 12).update(x)the window maximum; field bars is the offset of that maximum (0 = this bar, negative = bars ago, the newest bar wins a tie); pass the high for a bar-high window
Lowestnew Lowest(period = 12).update(x)the window minimum; field bars is the offset of that minimum; pass the low for a bar-low window
HighestBarsnew HighestBars(period).update(x)the offset as the primary value (0 or negative); field value is the matching high
LowestBarsnew LowestBars(period).update(x)the offset as the primary value; field value is the matching low

Trend systems

ClassConstructPer barConvention
Adxnew Adx(period = 14).update(high, low, close) returns adxfields adx, plusDi, minusDi; the seed is the plain sum of the first period true ranges and directional moves, then s = s - s / period + x (the engine's form, not an Rma); +DI and -DI appear at bar period, ADX at bar 2 * period - 1; a non-finite bar after the seed leaves every output NaN for good
Ichimokunew Ichimoku(conversionPeriod = 9, basePeriod = 26, laggingSpanPeriod = 52, displacement = 26).update(high, low, close) returns tenkanfields tenkan, kijun, senkouA, senkouB, chikou; windows are partial from bar 0 (no NaN warm-up), the first displacement bars use the current bar's values instead of shifted ones, a NaN output is reported as 0; chikou is the current close (see the accuracy section)
Psarnew Psar(start = 0.02, increment = 0.02, maxValue = 0.2).update(high, low, close)the stop-and-reverse level; bar 0 is NaN, bar 1 picks the first trend from close[1] >= close[0]; a non-finite bar makes every later bar NaN, as the engine's full recompute does
Supertrendnew Supertrend(factor, atrPeriod).update(high, low, close) returns linefields line, direction (1 up, the line sits below price; -1 down); both NaN while the ATR is NaN, and the band state is left untouched across such a gap
Vwapnew Vwap(anchor = "", price = "hlc3").update(open, high, low, close, volume, tsMs = NaN)anchor is "" (one accumulation from bar 0), "day", "week", "month", "quarter", "year" (UTC calendar boundaries) or a numeric string bucket width in milliseconds; tsMs is the bar's open time in milliseconds since the epoch and is read only when an anchor is set (the time source hands seconds, multiply by 1000); price is hlc3, hl2, ohlc4, hlcc4, or close; a non-finite bar makes the current bucket NaN until the next one starts

Events and conditions

ClassConstructPer barConvention
Risingnew Rising(period).update(x)1 when x is strictly above every one of the previous period values, else 0; NaN while the bar index is below period
Fallingnew Falling(period).update(x)the mirror of Rising
PivotHighnew PivotHigh(leftbars, rightbars).update(high)the value of the bar rightbars back when it beats every value leftbars before and rightbars after it, reported only on the confirming bar, NaN otherwise; ties never count
PivotLownew PivotLow(leftbars, rightbars).update(low)the mirror of PivotHigh
ValueWhennew ValueWhen(occurrence).update(condition, x)x on the most recent bar where condition was true (occurrence 0), or the one before (1); a condition is true when it is finite and not 0; the current bar counts
BarsSincenew BarsSince().update(condition)bars since the condition was last true, 0 on a true bar; NaN until the first true bar
Crossnew Cross().update(a, b): i32+1 when a crosses above b, -1 below, 0 otherwise; the engine's previous-bar rule: crossover is prevA < prevB && a >= b, crossunder is prevA > prevB && a <= b; any NaN among the four values gives 0, and so does the first bar

Cross folds three questions into one return value: test > 0, < 0, or != 0.

A composite over the shipped classes: a Macd read through its three fields, a Cross gate over the line and its signal, and an Rsi. The declarations at the top name the four params and the four outputs; the module below them only folds the bars:

param("fast", 12, { min: 1, max: 200 });
param("slow", 26, { min: 2, max: 400 });
param("signal", 9, { min: 1, max: 200 });
param("rsi_len", 14, { min: 2, max: 200 });
output("macd", line, lower);
output("signal", line, lower);
output("rsi", line, lower);
// Data-only: +1 on a bullish MACD cross, -1 on a bearish one, 0 otherwise.
output("crossed", none);

let macd = new Macd(12, 26, 9);
let rsi = new Rsi(14);
let cross = new Cross();

function onStart(): void {
  macd = new Macd(i32(p_fast()), i32(p_slow()), i32(p_signal()));
  rsi = new Rsi(i32(p_rsi_len()));
  cross = new Cross();
}

function onBar(): void {
  const close = bar.close();
  macd.update(close);
  const strength = rsi.update(close);
  const crossed = f64(cross.update(macd.macd, macd.signal));
  if (isNaN(macd.signal) || isNaN(strength)) return;
  out_macd(macd.macd);
  out_signal(macd.signal);
  out_rsi(strength);
  out_crossed(crossed);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

The catalog by page

Every class grouped by what it does. Every class ships in ./sdk/ta; the page column is where the group is explained with compiled examples.

Moving averages and smoothing

Oscillators and momentum

ClassWhere
RsiOscillators
WprOscillators
CmoOscillators
TsiOscillators
Macd, fields macd, signal, histOscillators
Stoch, fields k, dOscillators
Stochastic, fields k, dOscillators
CciOscillators
MfiOscillators
ChangeSeries functions
MomOscillators
RocOscillators

Trend and volatility

ClassWhere
Adx, fields adx, plusDi, minusDiTrend and volatility
Ichimoku, five fieldsTrend and volatility
PsarTrend and volatility
Supertrend, fields line, directionTrend and volatility
TrTrend and volatility
AtrTrend and volatility
Bb, fields basis, upper, lowerMoving averages
Keltner, fields basis, upper, lowerMoving averages
Donchian, fields basis, upper, lowerMoving averages
StdevTrend and volatility
VarianceTrend and volatility
price helpers (hl2, hlc3, ohlc4, hlcc4): arithmetic on the declared inputsSeries functions

Volume

ClassWhere
Obv, update(close, volume)Volume and VWAP
Vwap, anchored on the bar's open timeVolume and VWAP
CumVolume and VWAP

Statistics

ClassWhere
SumSeries functions
MedianSeries functions
PercentileSeries functions
Correlation, update(a, b)Series functions
ZscoreSeries functions

Bars and events

ClassWhere
Highest, Lowest, field barsSeries functions
HighestBars, LowestBars, field valueSeries functions
PivotHigh, PivotLowSeries functions
Rising, FallingSeries functions
ValueWhenSeries functions
BarsSinceSeries functions
Cross, +1 / -1 / 0Series functions
FixnanSeries functions
isNaN(x)Series functions
nz(x, replacement), a two-line helperSeries functions

The order book and volume profile scans are not classes: they are loops over a celled input on the Order flow page.

Conventions (read this once)

These rules hold across the library and explain nearly every edge case.

Warm-up is NaN, with named exceptions. A windowed class returns NaN until it has a full window and never fabricates an early value. The classes that report a partial window from bar 0: Sum, Donchian, Ichimoku, Cum (bar 0 is x), Obv (bar 0 is 0), and Stochastic (0 before its window fills). Decide per bar whether to return from onBar() before writing or to write the NaN; either way the chart draws nothing on that bar (Execution model).

Windows are strict; accumulators poison. A class that recomputes over its window (Sma, Wma, Alma, Linreg, Median, Percentile, Variance, Stdev, Correlation, Bb, Highest, Lowest, and the rest of the window family) yields NaN while any value inside the window is not finite and heals as soon as it leaves. A class that carries a running accumulator (Ema, Rma, Atr, Rsi, Macd, Adx, Cum, Psar, Supertrend's ATR) restarts its seed when a non-finite value arrives before the seed completes, and turns NaN for good when one arrives after, because the engine never reseeds. Sum and Zscore skip NaN bars instead. Run a sparse source through Fixnan or an isNaN guard before an accumulator.

Columns are yours to choose. The classes take one value per bar: pass bar.high() to Highest, HighestBars, and PivotHigh for a bar-high window, bar.low() to their counterparts, or the close for a close-based one.

Classes compose. update() takes any f64, including another class's output (rsi.update(sma.update(close))) and any series derived from a celled input. Warm-up propagates through the composition because NaN propagates through arithmetic.

Values carry; events do not. A missing scalar carries the latest eligible value forward by default (the missing policy on the source decides), so math keeps working. Events are stricter: Cross.update() returns 0 on any bar where either side is NaN, BarsSince and ValueWhen read a condition you computed (finite and not 0), so stale data cannot fabricate a signal.

Rma is Wilder. Rsi, Atr, Keltner, and Supertrend share the same accumulator, so an indicator built by hand over Rma agrees with the shipped classes.

Multi-output indicators

An indicator with several streams is a class with several fields. There are no tuple outputs and nothing indexes into an array: update() returns the primary line, and you read the other fields after the call and write each to its own output (Named streams).

Classupdate() returnsFields
Bb, Keltner, Donchianbasisbasis, upper, lower
Macdmacdmacd, signal, hist
Stoch, Stochastickk, d
Supertrendlineline, direction
Adxadxadx, plusDi, minusDi
Ichimokutenkantenkan, kijun, senkouA, senkouB, chikou
Highest, Lowestthe extremebars (the offset of that extreme)
HighestBars, LowestBarsthe offsetvalue (the extreme at that offset)

Fields feed a band declaration or a box the same way outputs do: write bb.upper and bb.lower to two drawn outputs and declare a range() between their names (Styling), or a box between their handles (Drawing objects).

Over microstructure

Every windowed class also runs over order-flow series, which is where the library stops being a price-only kit. A celled volume_profile input delivers each bar's [low, high, buy, sell] tuples; sum the buy and sell columns into a per-bar delta and the delta is just an f64 any class can fold: an Rsi over it is delta-RSI, a running sum is cumulative volume delta. Declaring the celled input switches the derived sheet to the second runtime contract, and the numeric classes compute exactly as before.

param("period", 14, { min: 2, max: 200, description: "RSI window over the per-bar delta" });
input("close", ohlcv.close);
input("profile", volume_profile.cells, { max_cells: 8192 });
output("delta", line, lower, { color: "#94a3b8", width: 1, description: "Buy minus sell volume across the bar's profile" });
output("delta_rsi", line, lower, { color: "#7c3aed", width: 2, description: "RSI of the per-bar delta" });
output("cvd", line, lower, { color: "#22d3ee", width: 2, description: "Cumulative volume delta" });

let rsi = new Rsi(14);
let cvd: f64 = 0.0;

function onStart(): void {
  rsi = new Rsi(i32(p_period()));
}

function onBar(): void {
  const n = in_profile_cells();
  if (n < 0) return; // this bar carries no block
  let delta = 0.0;
  if (n > 0) {
    const cells = in_profile_view();
    for (let i = 0; i + 3 < n; i += 4) delta += cells[i + 2] - cells[i + 3];
  }
  cvd += delta;
  const deltaRsi = rsi.update(delta);
  out_delta(delta);
  out_delta_rsi(deltaRsi);
  out_cvd(cvd);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

The Order flow page has every footprint read as a scan over the same tuples, and does the same over book levels for a book-imbalance average.

Accuracy as a contract

Every class is checked bit-exact against a reference implementation. The reference run covers a 690-bar 1h BTCUSDT window; each class is compiled through the real build and executed through the real runtime over the same bars, and every output is compared per bar: a maximum absolute deviation of 0 and identical NaN placement (a bar that is NaN in the reference is NaN here, and only that bar). The conventions on this page are written down so that comparison holds, and a change that drifts a class from the reference numbers fails the build.

Two honest exceptions.

  • Ichimoku.chikou is the current close. The engine computes the lagging span by reading close[i + displacement], a bar in the future of bar i, and only falls back to the current close on the last displacement bars of the series. A class that sees one bar at a time cannot read ahead, so the field carries the current close on every bar; it matches the engine on those last bars only. The other four fields match bar for bar.
  • Vwap anchors beyond "", "day", and a numeric millisecond width are unproven on that window. The "week", "month", "quarter", and "year" boundaries follow the engine's UTC calendar arithmetic, but the 690-bar window does not exercise a deep-history quarter or year, and the engine's session-calendar bucketing and RTH session filter (venues with a trading-session calendar) are not mirrored, since a per-bar class never sees a session calendar.

Everything else in the catalog, warm-up bars included, matches the reference bar for bar.