Moving averages
These classes smooth price into a trend line or wrap it in a band. Every
one of them ships with the editor in the ./sdk/ta module (Sma,
Ema, Wma, Hma, Alma, Swma, Vwma, Rma, Linreg, Bb,
Keltner, Donchian), 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(). Name the class you need (nothing to import),
construct it in onStart(), and call update() once per bar in onBar().
Every class has fixed arithmetic and edge rules, named in its section
(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,
so a NaN written to one output and an early return from onBar()
that leaves every output unwritten are the two honest ways to say "not
yet" (Execution model).
The shape every class shares:
| Member | Meaning |
|---|---|
new X(period) | allocates the window once; a period below 1 is clamped to 1 |
.update(x): f64 | folds one bar, returns the current value, NaN until warm |
.reset(): void | back to the freshly constructed state; nothing on the chart calls it for you |
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.
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).
let sma = new Sma(20);
function onStart(): 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).
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.
let wma = new Wma(20);
function onStart(): 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.
let hma = new Hma(21);
function onStart(): 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.
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 bellOmitting offset and sigma is the same as passing 0.85 and 6.
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.
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 the bar's volume beside the price
(bar.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.
let vwma = new Vwma(20);
function onBar(): void { out_vwma(vwma.update(bar.close(), bar.volume())); }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 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.
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.
let linreg = new Linreg(20); // value at the newest bar
let lagged = new Linreg(20, 2.0); // the fitted line two bars backBand helpers
A band is three named streams. A wrun 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 range() band over the two drawn edges
(Styling) or a box on every bar
(Cards, frames and panels).
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; the usual call is
new Bb(20, 2.0).
let bb = new Bb(20, 2.0);
function onBar(): void {
bb.update(bar.close());
out_bb_upper(bb.upper);
out_bb_basis(bb.basis);
out_bb_lower(bb.lower);
}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 and volatility 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.
let kc = new Keltner(20, 1.5, 10);
function onBar(): void {
const close = bar.close();
kc.update(close, bar.high(), bar.low(), close);
out_kc_upper(kc.upper);
out_kc_basis(kc.basis);
out_kc_lower(kc.lower);
}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). 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.
The period is used as given: the clamp to 1 the other classes apply does
not run here.
let dc = new Donchian(20);
function onBar(): void {
dc.update(bar.high(), bar.low());
out_dc_upper(dc.upper);
out_dc_basis(dc.basis);
out_dc_lower(dc.lower);
}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 is
used by name with nothing to import. Each class is constructed in
onStart() from a param, then updated once and its value written in
onBar().
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" });
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);
function onStart(): 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);
}
function onBar(): void {
const close = bar.close();
const high = bar.high();
const low = bar.low();
const smaValue = sma.update(close);
const emaValue = ema.update(close);
const hmaValue = hma.update(close);
const wmaValue = wma.update(close);
const almaValue = alma.update(close);
const swmaValue = swma.update(close);
const vwmaValue = vwma.update(close, bar.volume());
const rmaValue = rma.update(close);
const 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 its band draws at once;
// the slower lines simply write NaN until their own windows fill.
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);
}The whole module is one chart overlay with eighteen legend entries, and once the indicator is published every one of them is an output an alert can follow (Alerts).
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 and you can read it at
the Console prompt, but the chart shows only the two bands. All three
begin together once the 20-bar window completes, because one Bb feeds
them.
param("period", 20, { min: 2, max: 400 });
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 value 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);
function onStart(): void {
sma = new Sma(i32(p_period()));
bb = new Bb(i32(p_period()), 2.0);
}
function onBar(): void {
const close = bar.close();
const value = sma.update(close);
bb.update(close);
// Return before writing while the window is filling: no legend value, nothing drawn.
if (isNaN(value)) return;
out_sma20(value);
out_bb_upper(bb.upper);
out_bb_basis(bb.basis);
out_bb_lower(bb.lower);
}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.
param("length", 10, { min: 2, max: 200 });
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;
function onStart(): void {
almaDefault = new Alma(i32(p_length()));
almaExplicit = new Alma(i32(p_length()), 0.85, 6.0);
swma = new Swma();
}
function onBar(): void {
const close = bar.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;
const almaCheck = Math.abs(a - b) < 0.000001 ? b : NaN;
const swmaCheck = Math.abs(s - manual) < 0.000001 ? s : NaN;
out_alma_check(almaCheck);
out_swma_check(swmaCheck);
}Points to remember
- An average is an object you own: construct it in
onStart(), keep it in a module-levellet, feed it inonBar(). 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/lowerstreams are three fields read afterupdate()and three outputs. Nothing indexes into a tuple. Vwmareads volume throughbar.volume()and a two-argumentupdate(price, volume): a wrun indicator names every field it reads.- Warm-up is
NaN, andNaNpropagates through arithmetic.isNaN(x)is the test; a two-linenzhelper is on the Series functions page. - Every class here is checked bit-exact against the reference run described on the TA library page, edge rules included.