---
title: "TA library"
description: "Every wrun indicator can use ./sdk/ta, which ships with the editor: 52 stateful classes covering averages, statistics, oscillators, ranges, trend systems, and…"
order: 41
section: "functions"
---

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

# 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](moving-averages.md),
[Oscillators](oscillators.md), [Trend and volatility](trend-indicators.md),
[Volume and VWAP](volume-indicators.md),
[Series functions](series-functions.md)) 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](extra-indicators.md); 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

| 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(sqrt(period)))`; first value at bar `period - 1 + round(sqrt(period)) - 1` |
| `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(period, pct)` | `.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(period)` | `.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(period)` | `.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(kPeriod = 14, kSmoothing = 3, dPeriod = 3)` | `.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 HighestBars(period)` | `.update(x)` | the offset as the primary value (0 or negative); field `value` is the matching high |
| `LowestBars` | `new LowestBars(period)` | `.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(conversionPeriod = 9, basePeriod = 26, laggingSpanPeriod = 52, displacement = 26)` | `.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(factor, atrPeriod)` | `.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 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 |
| `PivotLow` | `new PivotLow(leftbars, rightbars)` | `.update(low)` | the mirror of `PivotHigh` |
| `ValueWhen` | `new 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 |
| `BarsSince` | `new BarsSince()` | `.update(condition)` | 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:

```typescript sample=fn-ta-tour
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](moving-averages.md) |
| `Ema` | [Moving averages](moving-averages.md) |
| `Rma` | [Moving averages](moving-averages.md) |
| `Wma` | [Moving averages](moving-averages.md) |
| `Hma` | [Moving averages](moving-averages.md) |
| `Vwma`, `update(x, volume)` | [Moving averages](moving-averages.md) |
| `Alma` | [Moving averages](moving-averages.md) |
| `Swma` | [Moving averages](moving-averages.md) |
| `Linreg` | [Moving averages](moving-averages.md) |

**Oscillators and momentum**

| Class | Where |
| --- | --- |
| `Rsi` | [Oscillators](oscillators.md) |
| `Wpr` | [Oscillators](oscillators.md) |
| `Cmo` | [Oscillators](oscillators.md) |
| `Tsi` | [Oscillators](oscillators.md) |
| `Macd`, fields `macd`, `signal`, `hist` | [Oscillators](oscillators.md) |
| `Stoch`, fields `k`, `d` | [Oscillators](oscillators.md) |
| `Stochastic`, fields `k`, `d` | [Oscillators](oscillators.md) |
| `Cci` | [Oscillators](oscillators.md) |
| `Mfi` | [Oscillators](oscillators.md) |
| `Change` | [Series functions](series-functions.md) |
| `Mom` | [Oscillators](oscillators.md) |
| `Roc` | [Oscillators](oscillators.md) |

**Trend and volatility**

| Class | Where |
| --- | --- |
| `Adx`, fields `adx`, `plusDi`, `minusDi` | [Trend and volatility](trend-indicators.md) |
| `Ichimoku`, five fields | [Trend and volatility](trend-indicators.md) |
| `Psar` | [Trend and volatility](trend-indicators.md) |
| `Supertrend`, fields `line`, `direction` | [Trend and volatility](trend-indicators.md) |
| `Tr` | [Trend and volatility](trend-indicators.md) |
| `Atr` | [Trend and volatility](trend-indicators.md) |
| `Bb`, fields `basis`, `upper`, `lower` | [Moving averages](moving-averages.md) |
| `Keltner`, fields `basis`, `upper`, `lower` | [Moving averages](moving-averages.md) |
| `Donchian`, fields `basis`, `upper`, `lower` | [Moving averages](moving-averages.md) |
| `Stdev` | [Trend and volatility](trend-indicators.md) |
| `Variance` | [Trend and volatility](trend-indicators.md) |
| price helpers (`hl2`, `hlc3`, `ohlc4`, `hlcc4`): arithmetic on the declared inputs | [Series functions](series-functions.md) |

**Volume**

| Class | Where |
| --- | --- |
| `Obv`, `update(close, volume)` | [Volume and VWAP](volume-indicators.md) |
| `Vwap`, anchored on the bar's open time | [Volume and VWAP](volume-indicators.md) |
| `Cum` | [Volume and VWAP](volume-indicators.md) |

**Statistics**

| Class | Where |
| --- | --- |
| `Sum` | [Series functions](series-functions.md) |
| `Median` | [Series functions](series-functions.md) |
| `Percentile` | [Series functions](series-functions.md) |
| `Correlation`, `update(a, b)` | [Series functions](series-functions.md) |
| `Zscore` | [Series functions](series-functions.md) |

**Bars and events**

| Class | Where |
| --- | --- |
| `Highest`, `Lowest`, field `bars` | [Series functions](series-functions.md) |
| `HighestBars`, `LowestBars`, field `value` | [Series functions](series-functions.md) |
| `PivotHigh`, `PivotLow` | [Series functions](series-functions.md) |
| `Rising`, `Falling` | [Series functions](series-functions.md) |
| `ValueWhen` | [Series functions](series-functions.md) |
| `BarsSince` | [Series functions](series-functions.md) |
| `Cross`, `+1` / `-1` / `0` | [Series functions](series-functions.md) |
| `Fixnan` | [Series functions](series-functions.md) |
| `isNaN(x)` | [Series functions](series-functions.md) |
| `nz(x, replacement)`, a two-line helper | [Series functions](series-functions.md) |

The order book and volume profile scans are not classes: they are loops
over a celled input on the [Order flow](order-flow-kit.md) 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](../core-concepts/execution-model.md)).

**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](../core-concepts/named-streams.md)).

| 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](../presentation/styling.md)), or a `box`
between their handles ([Drawing objects](../presentation/drawing-objects.md)).

## 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.

```typescript sample=fn-delta-rsi
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](order-flow-kit.md) 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.
