TA library
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,NaNuntil 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 aref64. Construct inonStart()from a param (new Sma(i32(p_period()))), keep the object in a module-levellet, and update it once per bar inonBar(). A period below 1 is clamped to 1 (exceptDonchian, which uses the period as given).
The shipped classes
Averages and smoothing
| Class | Construct | Per bar | Convention |
|---|---|---|---|
Sma | new Sma(period) | .update(x) | plain window mean; NaN until period bars exist and whenever the window holds a non-finite value |
Ema | new 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 |
Rma | new Rma(period) | .update(x) | Wilder smoothing, the accumulator inside Rsi and Atr: same seed as Ema, then (prev * (period - 1) + x) / period |
Wma | new Wma(period) | .update(x) | linear weights, the newest value weighs period, the oldest 1; strict window |
Hma | new Hma(period) | .update(x) | wma(2 * wma(x, round(period / 2)) - wma(x, period), round(; first value at bar period - 1 + round( |
Vwma | new Vwma(period) | .update(x, volume) | sum(x * volume) / sum(volume); NaN when the volume sum is 0 |
Alma | new Alma(length, offset = 0.85, sigma = 6) | .update(x) | Gaussian weights centered at offset * (length - 1), width length / sigma; strict window |
Swma | new Swma() | .update(x) | (x[3] + 2 x[2] + 2 x[1] + x[0]) / 6 with x[0] the newest; first value at bar 3 |
Linreg | new 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
| Class | Construct | Per bar | Convention |
|---|---|---|---|
Sum | new Sum(period) | .update(x) | no warm-up: bar 0 already returns the partial window; NaN inputs are skipped |
Median | new Median(period) | .update(x) | middle of the sorted window, the mean of the two middle values on an even period; strict window |
Percentile | new Percentile( | .update(x) | nearest rank: rank = ceil(pct / 100 * period), result sorted[max(0, rank - 1)]; pct clamped to 0..100; strict window |
Variance | new Variance( | .update(x) | population variance (divide by period, not period - 1); strict window |
Stdev | new Stdev(period) | .update(x) | population standard deviation, the square root of Variance; strict window |
Zscore | new 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 |
Correlation | new Correlation( | .update(a, b) | Pearson correlation of two series; NaN when either window holds a non-finite value or the denominator is 0 |
Change | new Change(n = 1) | .update(x) | x - x[n]; NaN for the first n bars |
Mom | new Mom(n) | .update(x) | the same math as Change under another name |
Roc | new Roc(period) | .update(x) | (x - x[n]) / x[n] * 100; NaN for the first n bars and when x[n] is 0 |
Cum | new Cum() | .update(x) | running sum from bar 0; a non-finite bar makes it NaN for good |
Fixnan | new Fixnan() | .update(x) | repeats the last finite value over a non-finite bar; NaN until the first finite value |
Oscillators and momentum
| Class | Construct | Per bar | Convention |
|---|---|---|---|
Rsi | new 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 |
Cmo | new Cmo(length) | .update(x) | 100 * (up - down) / (up + down) over the last length changes; first value at bar length; a zero total returns 0 |
Tsi | new 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 |
Cci | new 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 |
Mfi | new 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 |
Wpr | new Wpr(length = 14) | .update(high, low, close) | Williams %R; first value at bar length - 1; a flat window returns 0 |
Stoch | new Stoch(periodK, smoothK, periodD) | .update(high, low, close) returns k | fields 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 |
Stochastic | new Stochastic( | .update(high, low, close) returns k | the 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 |
Macd | new Macd(fast = 12, slow = 26, signal = 9) | .update(x) returns macd | fields macd, signal, hist; the line appears at bar slow - 1, the signal at bar slow + signal - 2; hist = macd - signal |
Obv | new Obv() | .update(close, volume) | on-balance volume; bar 0 returns 0; an unchanged close adds nothing |
Ranges and bands
| Class | Construct | Per bar | Convention |
|---|---|---|---|
Tr | new Tr() | .update(high, low, close) | max(high - low, abs(high - prevClose), abs(low - prevClose)); bar 0 is plain high - low |
Atr | new 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 |
Bb | new Bb(period, mult) | .update(x) returns basis | fields basis, upper, lower; Sma basis and population Stdev width; all three NaN while the window holds a non-finite value |
Keltner | new Keltner(period, mult, atrPeriod) | .update(x, high, low, close) returns basis | fields 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 |
Donchian | new Donchian(period = 12) | .update(high, low) returns basis | fields 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 |
Highest | new 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 |
Lowest | new Lowest(period = 12) | .update(x) | the window minimum; field bars is the offset of that minimum; pass the low for a bar-low window |
HighestBars | new Highest | .update(x) | the offset as the primary value (0 or negative); field value is the matching high |
LowestBars | new Lowest | .update(x) | the offset as the primary value; field value is the matching low |
Trend systems
| Class | Construct | Per bar | Convention |
|---|---|---|---|
Adx | new Adx(period = 14) | .update(high, low, close) returns adx | fields 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 |
Ichimoku | new Ichimoku( | .update(high, low, close) returns tenkan | fields 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) |
Psar | new 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 |
Supertrend | new Supertrend( | .update(high, low, close) returns line | fields 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 |
Vwap | new 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
| Class | Construct | Per bar | Convention |
|---|---|---|---|
Rising | new 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 |
Falling | new Falling(period) | .update(x) | the mirror of Rising |
PivotHigh | new Pivot | .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 |
PivotLow | new Pivot | .update(low) | the mirror of PivotHigh |
ValueWhen | new Value | . | 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 |
BarsSince | new BarsSince() | . | bars since the condition was last true, 0 on a true bar; NaN until the first true bar |
Cross | new 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);
}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
| Class | Where |
|---|---|
Sma | Moving averages |
Ema | Moving averages |
Rma | Moving averages |
Wma | Moving averages |
Hma | Moving averages |
Vwma, update(x, volume) | Moving averages |
Alma | Moving averages |
Swma | Moving averages |
Linreg | Moving averages |
Oscillators and momentum
| Class | Where |
|---|---|
Rsi | Oscillators |
Wpr | Oscillators |
Cmo | Oscillators |
Tsi | Oscillators |
Macd, fields macd, signal, hist | Oscillators |
Stoch, fields k, d | Oscillators |
Stochastic, fields k, d | Oscillators |
Cci | Oscillators |
Mfi | Oscillators |
Change | Series functions |
Mom | Oscillators |
Roc | Oscillators |
Trend and volatility
| Class | Where |
|---|---|
Adx, fields adx, plusDi, minusDi | Trend and volatility |
Ichimoku, five fields | Trend and volatility |
Psar | Trend and volatility |
Supertrend, fields line, direction | Trend and volatility |
Tr | Trend and volatility |
Atr | Trend and volatility |
Bb, fields basis, upper, lower | Moving averages |
Keltner, fields basis, upper, lower | Moving averages |
Donchian, fields basis, upper, lower | Moving averages |
Stdev | Trend and volatility |
Variance | Trend and volatility |
price helpers (hl2, hlc3, ohlc4, hlcc4): arithmetic on the declared inputs | Series functions |
Volume
| Class | Where |
|---|---|
Obv, update(close, volume) | Volume and VWAP |
Vwap, anchored on the bar's open time | Volume and VWAP |
Cum | Volume and VWAP |
Statistics
| Class | Where |
|---|---|
Sum | Series functions |
Median | Series functions |
Percentile | Series functions |
Correlation, update(a, b) | Series functions |
Zscore | Series functions |
Bars and events
| Class | Where |
|---|---|
Highest, Lowest, field bars | Series functions |
HighestBars, LowestBars, field value | Series functions |
PivotHigh, PivotLow | Series functions |
Rising, Falling | Series functions |
ValueWhen | Series functions |
BarsSince | Series functions |
Cross, +1 / -1 / 0 | Series functions |
Fixnan | Series functions |
isNaN(x) | Series functions |
nz(x, replacement), a two-line helper | Series 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).
| Class | update() returns | Fields |
|---|---|---|
Bb, Keltner, Donchian | basis | basis, upper, lower |
Macd | macd | macd, signal, hist |
Stoch, Stochastic | k | k, d |
Supertrend | line | line, direction |
Adx | adx | adx, plusDi, minusDi |
Ichimoku | tenkan | tenkan, kijun, senkouA, senkouB, chikou |
Highest, Lowest | the extreme | bars (the offset of that extreme) |
HighestBars, LowestBars | the offset | value (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);
}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.chikouis the current close. The engine computes the lagging span by readingclose[i + displacement], a bar in the future of bari, and only falls back to the current close on the lastdisplacementbars 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.Vwapanchors 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.