Moving averages

These classes smooth price into a trend line or wrap it in a band. Every one of them ships in every workspace as part of src/sdk/ta.ts: the whole kScript…

These classes smooth price into a trend line or wrap it in a band. Every one of them ships in every workspace as part of src/sdk/ta.ts: the whole kScript (legacy) roster (sma, ema, wma, hma, alma, swma, vwma, rma, linreg, bb, keltner, donchian) is a class with the same name in title case, each one built the same way: allocate in the constructor, fold one bar per update(), return NaN until the window is full, restore with reset(). Import the class you need from ./sdk/ta, construct it in init(), and call update() once per bar in state(). Every class mirrors the kScript engine's arithmetic and its edge rules, so a port draws the same line bar for bar (TA library lists the full catalog and the proof).

Every windowed average is NaN until its window fills. An Sma(20) draws nothing for its first 19 bars, then begins once the 20th has loaded; the Hull average warms up a little longer because of its internal weighted sub-windows. Donchian is the exception: it reads the rolling extreme of whatever has loaded from the very first bar. NaN is the warm-up signal on every host, so an abstaining state() (return 0) and a NaN written to one output are the two honest ways to say "not yet" (Execution model).

The shape every class shares:

MemberMeaning
new X(period)allocates the window once; a period below 1 is clamped to 1
.update(x): f64folds one bar, returns the current value, NaN until warm
.reset(): voidback to the freshly constructed state; call it from your reset()

A non-finite input (a NaN or an infinity) inside a window makes the windowed averages return NaN for as long as that bar sits in the window; the two running averages (Ema, Rma) never recover from one, which is the engine's rule too.

Single-line averages

Sma

new Sma(period), .update(x): the unweighted mean of the last period values, summed oldest to newest and divided by the period. Warm-up is period bars (first value at bar period - 1).

import { Sma } from "./sdk/ta";

let sma = new Sma(20);
export function init(): void { sma = new Sma(i32(p_period())); }

Read the period through its accessor and cast it: params are f64, class periods are i32, and AssemblyScript refuses the implicit conversion.

Ema

new Ema(period), .update(x): the exponential average. Weights recent bars more heavily, so it turns faster than Sma. The first period finite values seed it with their simple mean, then `x * alpha + prev * (1

  • alpha)withalpha = 2 / (period + 1)takes over, so it isNaNforperiod - 1bars. A non-finite input before the seed completes restarts the seed count; one after the seed sets the value toNaN` for good (the engine never reseeds).
import { Ema } from "./sdk/ta";

let ema = new Ema(20);

Wma

new Wma(period), .update(x): the linearly weighted average. The newest bar carries weight period, the oldest weight 1, so it sits between Sma and Ema in responsiveness. The weighted sum walks oldest to newest and is divided by period * (period + 1) / 2; NaN until period bars have been seen and whenever any value in the window is not finite.

import { Wma } from "./sdk/ta";

let wma = new Wma(20);
export function init(): void { wma = new Wma(i32(p_period())); }

Hma

new Hma(period), .update(x): the Hull average, three Wma windows composed as WMA(2 * WMA(round(n / 2)) - WMA(n), round(sqrt(n))). Very low lag and smooth. The half and square-root lengths are rounded (never below 1), the inner difference is NaN until both inner windows are warm, and the outer window then needs round(sqrt(n)) finite values, so the first value lands at bar n - 1 + round(sqrt(n)) - 1.

import { Hma } from "./sdk/ta";

let hma = new Hma(21);
export function init(): void { hma = new Hma(i32(p_hull_period())); }

Alma

new Alma(length, offset = 0.85, sigma = 6), .update(x): the Arnaud Legoux average, a Gaussian-weighted window. offset places the peak of the bell at offset * (length - 1) from the oldest bar (near the newest bar by default), sigma sets its width as length / sigma. The weights are computed once in the constructor with the engine's arithmetic, so update() is a single weighted pass, walked oldest to newest. NaN until length bars have been seen and whenever any value in the window is not finite.

import { Alma } from "./sdk/ta";

let alma = new Alma(20);              // offset 0.85, sigma 6
let sharp = new Alma(20, 0.9, 4.0);   // peak closer to the newest bar, narrower bell

Omitting offset and sigma is the same as passing 0.85 and 6; the constructor defaults are the kScript defaults.

Swma

new Swma(), .update(x): the symmetrically weighted average, the fixed four-tap smoother (x[3] + 2 * x[2] + 2 * x[1] + x[0]) / 6 with x[0] the newest bar. NaN on the first three bars (first value at bar 3) and whenever any of the four values is not finite; no period to choose.

import { Swma } from "./sdk/ta";

let swma = new Swma();

Vwma

new Vwma(period), .update(price, volume): the volume-weighted average takes two values per bar, sum(price * volume) / sum(volume) over the window. Bars with more volume pull the average harder, so it tracks where trading actually happened. It needs a volume input beside the price input (input("volume", ohlcv.volume)). NaN until period bars have been seen, whenever any price or volume in the window is not finite, and when the volume sum is exactly 0.

import { Vwma } from "./sdk/ta";

let vwma = new Vwma(20);
export function state(): i32 {
  const value = vwma.update(in_close(), in_volume());
  return isNaN(value) ? 0 : 1;
}

Rma

new Rma(period), .update(x): the running (Wilder) average, the smoothing inside RSI and ATR. The first period finite values seed it with their simple mean, then rma = (rma * (period - 1) + x) / period. Smoother and slower than Ema for the same period. This is the same Wilder accumulator the kScript engine runs and the one Rsi and Atr use internally, so an RSI built by hand over Rma and the shipped Rsi agree. A non-finite input before the seed completes restarts the seed count; one after the seed sets the value to NaN for good.

import { Rma } from "./sdk/ta";

let rma = new Rma(14);

Linreg

new Linreg(period, offset = 0), .update(x): linear regression fits a least-squares line over the last period bars (x positions 0 oldest to period - 1 newest) and returns its value at the newest bar (offset 0) or offset bars back along the fitted line: the trend's fitted price rather than an average. offset is an f64 truncated to an integer. NaN until period bars have been seen and whenever any value in the window is not finite; a period of 1 returns the value itself.

import { Linreg } from "./sdk/ta";

let linreg = new Linreg(20);        // value at the newest bar
let lagged = new Linreg(20, 2.0);   // the fitted line two bars back

Band helpers

A kScript band helper returned three named streams. An Indicator has no tuple outputs: the class exposes upper, basis, and lower as fields after each update(), and you write each one to its own output (Named streams). Shading between the edges is a box on every bar (Drawing primitives).

Bb

new Bb(period, mult), .update(x): Bollinger bands. basis is the window mean (the same arithmetic as Sma), upper and lower sit mult population standard deviations away (the same arithmetic as Stdev, the textbook Bollinger form). update() returns the basis and fills the three fields. All three are NaN until period bars exist and whenever any bar of the window is not finite; the window heals as soon as the bad bar leaves it. Both arguments are required; kScript's usual call is new Bb(20, 2.0).

import { Bb } from "./sdk/ta";

let bb = new Bb(20, 2.0);
export function finalize(): void {
  out_bb_upper(bb.upper);
  out_bb_basis(bb.basis);
  out_bb_lower(bb.lower);
  emitRow();
}

Keltner

new Keltner(period, mult, atrPeriod), .update(x, high, low, close): like Bollinger, but the width comes from the average true range instead of the standard deviation, so it reacts to range rather than dispersion. basis is an Ema(period) over x (usually the close), the width is an Atr(atrPeriod) over the three prices (the Wilder-smoothed true range; its class is on the Trend indicators page) multiplied by mult. update() returns the basis and fills the three fields. The basis is reported as soon as the Ema is seeded, even while the ATR is still warming up; upper and lower are NaN unless both the basis and the ATR are finite.

import { Keltner } from "./sdk/ta";

let kc = new Keltner(20, 1.5, 10);
export function state(): i32 {
  const close = in_close();
  kc.update(close, in_high(), in_low(), close);
  return 1;
}

Donchian

new Donchian(period = 12), .update(high, low): the highest high and lowest low over period bars as upper and lower, with their midpoint as basis (the value update() returns; the kScript builtin returns only the midline). Because it reads the rolling extreme rather than averaging, it is finite from the first loaded bar: the window is partial at the start, and bar 0 returns (high + low) / 2 of that bar alone. The current bar's high and low always take part, even when NaN (so a NaN bar propagates), while non-finite highs or lows of older bars are skipped.

import { Donchian } from "./sdk/ta";

let dc = new Donchian(20);
export function state(): i32 {
  dc.update(in_high(), in_low());
  return 1;
}

Putting them together

Every average and band on one chart: the nine single-line averages on the price pane, the three band helpers as nine more outputs. Every class comes from ./sdk/ta in one import line. Each class is constructed in init() from a param, updated once in state(), and its value written in finalize().

import { input, line, ohlcv, output, overlay, param } from "./sdk/declare";
import { in_close, in_high, in_low, in_volume } from "./gen/inputs";
import {
  emitRow,
  out_alma,
  out_bb_basis,
  out_bb_lower,
  out_bb_upper,
  out_dc_basis,
  out_dc_lower,
  out_dc_upper,
  out_ema,
  out_hma,
  out_kc_basis,
  out_kc_lower,
  out_kc_upper,
  out_linreg,
  out_rma,
  out_sma,
  out_swma,
  out_vwma,
  out_wma,
} from "./gen/outputs";
import { p_atr_period, p_hull_period, p_kc_mult, p_mult, p_period } from "./gen/params";
import { Alma, Bb, Donchian, Ema, Hma, Keltner, Linreg, Rma, Sma, Swma, Vwma, Wma } from "./sdk/ta";

param("period", 20, { min: 2, max: 400, description: "Window for every average and band" });
param("hull_period", 21, { min: 4, max: 400, description: "Hull average window" });
param("mult", 2, { min: 0.5, max: 5, description: "Bollinger standard-deviation multiplier" });
param("kc_mult", 1.5, { min: 0.5, max: 5, description: "Keltner ATR multiplier" });
param("atr_period", 10, { min: 1, max: 200, description: "Keltner ATR window" });
input("close", ohlcv.close);
input("high", ohlcv.high);
input("low", ohlcv.low);
input("volume", ohlcv.volume);
output("sma", line, overlay, { color: "#2563eb", width: 2, description: "Simple moving average" });
output("ema", line, overlay, { color: "#dc2626", width: 2, description: "Exponential moving average" });
output("hma", line, overlay, { color: "#7c3aed", width: 2, description: "Hull moving average" });
output("wma", line, overlay, { color: "#ea580c", width: 2, description: "Weighted moving average" });
output("alma", line, overlay, { color: "#0891b2", width: 2, description: "Arnaud Legoux moving average" });
output("swma", line, overlay, { color: "#0f766e", width: 1, description: "Symmetrically weighted moving average" });
output("vwma", line, overlay, { color: "#b45309", width: 2, description: "Volume-weighted moving average" });
output("rma", line, overlay, { color: "#059669", width: 2, description: "Wilder running moving average" });
output("linreg", line, overlay, { color: "#4b5563", width: 2, description: "Least-squares regression value" });
output("bb_upper", line, overlay, { color: "#0f766e", width: 1, description: "Bollinger upper band" });
output("bb_basis", line, overlay, { color: "#64748b", width: 1, description: "Bollinger basis" });
output("bb_lower", line, overlay, { color: "#be123c", width: 1, description: "Bollinger lower band" });
output("kc_upper", line, overlay, { color: "#16a34a", width: 1, description: "Keltner upper channel" });
output("kc_basis", line, overlay, { color: "#94a3b8", width: 1, description: "Keltner basis" });
output("kc_lower", line, overlay, { color: "#dc2626", width: 1, description: "Keltner lower channel" });
output("dc_upper", line, overlay, { color: "#0ea5e9", width: 1, description: "Donchian upper band" });
output("dc_basis", line, overlay, { color: "#94a3b8", width: 1, description: "Donchian midpoint" });
output("dc_lower", line, overlay, { color: "#f97316", width: 1, description: "Donchian lower band" });

let sma = new Sma(20);
let ema = new Ema(20);
let hma = new Hma(21);
let wma = new Wma(20);
let alma = new Alma(20);
let swma = new Swma();
let vwma = new Vwma(20);
let rma = new Rma(20);
let linreg = new Linreg(20);
let bb = new Bb(20, 2.0);
let kc = new Keltner(20, 1.5, 10);
let dc = new Donchian(20);
let smaValue: f64 = NaN;
let emaValue: f64 = NaN;
let hmaValue: f64 = NaN;
let wmaValue: f64 = NaN;
let almaValue: f64 = NaN;
let swmaValue: f64 = NaN;
let vwmaValue: f64 = NaN;
let rmaValue: f64 = NaN;
let linregValue: f64 = NaN;

export function init(): void {
  const period = i32(p_period());
  sma = new Sma(period);
  ema = new Ema(period);
  hma = new Hma(i32(p_hull_period()));
  wma = new Wma(period);
  alma = new Alma(period);
  swma = new Swma();
  vwma = new Vwma(period);
  rma = new Rma(period);
  linreg = new Linreg(period, 0.0);
  bb = new Bb(period, p_mult());
  kc = new Keltner(period, p_kc_mult(), i32(p_atr_period()));
  dc = new Donchian(period);
}

export function state(): i32 {
  const close = in_close();
  const high = in_high();
  const low = in_low();
  smaValue = sma.update(close);
  emaValue = ema.update(close);
  hmaValue = hma.update(close);
  wmaValue = wma.update(close);
  almaValue = alma.update(close);
  swmaValue = swma.update(close);
  vwmaValue = vwma.update(close, in_volume());
  rmaValue = rma.update(close);
  linregValue = linreg.update(close);
  bb.update(close);
  kc.update(close, high, low, close);
  dc.update(high, low);
  // Donchian is finite from the first bar, so the row is ready as soon as it has computed;
  // the slower lines simply write NaN until their own windows fill.
  return 1;
}

export function finalize(): void {
  out_sma(smaValue);
  out_ema(emaValue);
  out_hma(hmaValue);
  out_wma(wmaValue);
  out_alma(almaValue);
  out_swma(swmaValue);
  out_vwma(vwmaValue);
  out_rma(rmaValue);
  out_linreg(linregValue);
  out_bb_upper(bb.upper);
  out_bb_basis(bb.basis);
  out_bb_lower(bb.lower);
  out_kc_upper(kc.upper);
  out_kc_basis(kc.basis);
  out_kc_lower(kc.lower);
  out_dc_upper(dc.upper);
  out_dc_basis(dc.basis);
  out_dc_lower(dc.lower);
  emitRow();
}

export function reset(): void {
  sma.reset();
  ema.reset();
  hma.reset();
  wma.reset();
  alma.reset();
  swma.reset();
  vwma.reset();
  rma.reset();
  linreg.reset();
  bb.reset();
  kc.reset();
  dc.reset();
  smaValue = NaN;
  emaValue = NaN;
  hmaValue = NaN;
  wmaValue = NaN;
  almaValue = NaN;
  swmaValue = NaN;
  vwmaValue = NaN;
  rmaValue = NaN;
  linregValue = NaN;
}

The whole module is one chart overlay with eighteen legend entries; on your machine every one of them is a metric id (wrun/@you/<name>/hma, .../bb_upper, and so on) that an alert or a screen can read.

Warm-up and named streams

Two ideas worth seeing on their own: an average is blank until its window fills, and a band's three levels are three independent outputs you can draw or hide one at a time. The Bollinger edges below are drawn while the basis stays data-only (none): it still computes, still exists as a metric, and the chart shows only the two bands. All three begin together once the 20-bar window completes, because one Bb feeds them.

import { input, line, none, ohlcv, output, overlay, param } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_bb_basis, out_bb_lower, out_bb_upper, out_sma20 } from "./gen/outputs";
import { p_period } from "./gen/params";
import { Bb, Sma } from "./sdk/ta";

param("period", 20, { min: 2, max: 400 });
input("close", ohlcv.close);
output("sma20", line, overlay, { color: "#2563eb", width: 2, description: "NaN until the window is complete" });
output("bb_upper", line, overlay, { color: "#16a34a", width: 2, description: "Bollinger upper band" });
output("bb_basis", none, overlay, { description: "Computed, never drawn: a metric without a line" });
output("bb_lower", line, overlay, { color: "#dc2626", width: 2, description: "Bollinger lower band" });

let sma = new Sma(20);
let bb = new Bb(20, 2.0);
let value: f64 = NaN;

export function init(): void {
  sma = new Sma(i32(p_period()));
  bb = new Bb(i32(p_period()), 2.0);
}

export function state(): i32 {
  const close = in_close();
  value = sma.update(close);
  bb.update(close);
  // Abstain the whole row while the window is filling: no legend value, no metric row, nothing drawn.
  return isNaN(value) ? 0 : 1;
}

export function finalize(): void {
  out_sma20(value);
  out_bb_upper(bb.upper);
  out_bb_basis(bb.basis);
  out_bb_lower(bb.lower);
  emitRow();
}

export function reset(): void {
  sma.reset();
  bb.reset();
  value = NaN;
}

Alma and Swma boundaries

Alma's omitted offset and sigma default to 0.85 and 6, so a default construction and an explicit one agree to the last bit; Swma uses the fixed four-tap weighting, so it agrees with the arithmetic spelled out by hand over the last four closes. The module below computes both forms and writes each line only where the class agrees with the manual form, which is every bar once the windows are warm: a NaN written on a disagreeing bar would leave a visible hole.

import { input, line, lower, ohlcv, output, param } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_alma_check, out_swma_check } from "./gen/outputs";
import { p_length } from "./gen/params";
import { Alma, Swma } from "./sdk/ta";

param("length", 10, { min: 2, max: 200 });
input("close", ohlcv.close);
output("alma_check", line, lower, { color: "#2563eb", width: 2, description: "ALMA with the default offset 0.85 and sigma 6" });
output("swma_check", line, lower, { color: "#16a34a", width: 2, description: "SWMA against the four-tap arithmetic" });

let almaDefault = new Alma(10);
let almaExplicit = new Alma(10, 0.85, 6.0);
let swma = new Swma();
// The manual SWMA keeps the last four closes by hand: x0 is the newest.
let x0: f64 = NaN;
let x1: f64 = NaN;
let x2: f64 = NaN;
let x3: f64 = NaN;
let almaCheck: f64 = NaN;
let swmaCheck: f64 = NaN;

export function init(): void {
  almaDefault = new Alma(i32(p_length()));
  almaExplicit = new Alma(i32(p_length()), 0.85, 6.0);
  swma = new Swma();
}

export function state(): i32 {
  const close = in_close();
  const a = almaDefault.update(close);
  const b = almaExplicit.update(close);
  const s = swma.update(close);
  x3 = x2;
  x2 = x1;
  x1 = x0;
  x0 = close;
  const manual = (x3 + 2.0 * x2 + 2.0 * x1 + x0) / 6.0;
  almaCheck = Math.abs(a - b) < 0.000001 ? b : NaN;
  swmaCheck = Math.abs(s - manual) < 0.000001 ? s : NaN;
  return isNaN(almaCheck) && isNaN(swmaCheck) ? 0 : 1;
}

export function finalize(): void {
  out_alma_check(almaCheck);
  out_swma_check(swmaCheck);
  emitRow();
}

export function reset(): void {
  almaDefault.reset();
  almaExplicit.reset();
  swma.reset();
  x0 = NaN;
  x1 = NaN;
  x2 = NaN;
  x3 = NaN;
  almaCheck = NaN;
  swmaCheck = NaN;
}

What changed from kScript

  • A builtin call (sma(source=close, period=20)) became an object you own: construct it in init(), keep it in a module-level let, feed it in state(). There is no series to pass in; the class sees one value per bar, which is the whole execution model.
  • A band's .upper / .basis / .lower streams became three fields read after update() and three outputs. Nothing indexes into a tuple.
  • vwma(source=trade) reading volume off an OHLC series became an explicit input("volume", ohlcv.volume) and a two-argument update(price, volume): an Indicator names every field it reads.
  • Warm-up stays NaN, and NaN propagates through arithmetic exactly as na did. isNaN(x) is the test; there is no nz builtin, but a two-line helper is on the Series functions page.
  • The numbers are the engine's numbers: every class here matches the kScript engine bar for bar, so a port is a rename, not a re-tune. The full catalog is on the TA library page.