Series functions
Series functions answer the questions that come up constantly when writing an indicator: did two lines just cross, is a series rising, what is the highest high in the last 10 bars, what is the window total or z-score, how many bars since a condition was true, and is this value real or missing.
A wrun indicator sees one
bar at a time (Execution model),
so each is a small class in ./sdk/ta that keeps the history it needs
and folds one bar per update() call, or a two-line function over
NaN. Every class on this page ships in the kit; the full catalog is on
the TA library page.
The one series question this page does not answer is source[n], the
value a few bars back. That is History from ./sdk/stats: construct
it in onStart(), push() once per bar, read ago(n), NaN until the
window holds the bar. The same module does the list math (mean,
stdev, slope, correlation, median, percentile) over a
StaticArray<f64> without allocating, and ./sdk/ta-plus adds the
running extremes, PercentRank, Range and Mode over a window; all
on Extra indicators. The pivots below are also
the raw material of the Market structure kit,
whose Swings turns them into confirmed swing highs and lows.
Price-source helpers
hl2, hlc3, ohlc4, and hlcc4 are arithmetic on the bar's fields.
Read the fields you need and write the formula:
| Helper | Formula |
|---|---|
hl2 | (high + low) / 2.0 |
hlc3 | (high + low + close) / 3.0 |
ohlc4 | (open + high + low + close) / 4.0 |
hlcc4 | (high + low + close + close) / 4.0 |
There is no implicit source: a wrun indicator names every field it
reads, bar.high() for the high, bar.close() for the close.
output("hl2", line, overlay, { color: "#2563eb", width: 1, description: "Bar midpoint" });
output("hlc3", line, overlay, { color: "#16a34a", width: 1, description: "Typical price" });
output("ohlc4", line, overlay, { color: "#f97316", width: 1, description: "Four-price average" });
output("hlcc4", line, overlay, { color: "#7c3aed", width: 1, description: "Close-weighted average" });
function onBar(): void {
const open = bar.open();
const high = bar.high();
const low = bar.low();
const close = bar.close();
out_hl2((high + low) / 2.0);
out_hlc3((high + low + close) / 3.0);
out_ohlc4((open + high + low + close) / 4.0);
out_hlcc4((high + low + close + close) / 4.0);
}Crossovers and signals
Cross ships in ./sdk/ta: new Cross(), .update(a, b): i32
returns +1 on the bar a crosses above b, -1 on the bar it crosses
below, and 0 otherwise, including the first bar and any bar where either
side is NaN. The edge rule is the engine's: a crossover is a below b
on the previous bar and a at or above b on this bar (touching counts
on the current bar, not on the previous one), and a crossunder is the
mirror image. One object answers all three questions:
| You want | wrun form |
|---|---|
| a cross above | cross.update(a, b) == 1 |
| a cross below | cross.update(a, b) == -1 |
| a cross in either direction | cross.update(a, b) != 0 |
Call update() exactly once per bar per pair: it remembers the previous
pair, so a second call on the same bar would compare the bar to itself.
Keep the result in a local and test it as many times as you like. A
crossover against a constant (rsi over 70) is cross.update(value, 70.0).
The classic signal-line pattern is a Cross over the MACD line and its
signal; the result is a 0/1 step you can draw, or a gate for a
shape output that marks the price bar (Plotting):
const crossed = cross.update(macd.macd, macd.signal);
bullish = crossed == 1 ? 1.0 : 0.0;Trend, extremes, and counting
| You want | wrun form | Returns |
|---|---|---|
| is the series rising | new Rising(period), .update(x) | 1 when x is strictly above every one of the previous period values, else 0; NaN for the first period bars |
| is the series falling | new Falling(period), .update(x) | 1 when x is strictly below every one of the previous period values, else 0; NaN for the first period bars |
the change over n bars | new Change(n), .update(x) | x now minus x n bars ago (n defaults to 1); NaN for the first n bars |
| the window high | new Highest(period), .update(x), field bars | the highest value in the window; bars is the same offset HighestBars reports for the window |
| the window low | new Lowest(period), .update(x), field bars | the lowest value in the window; bars is the same offset LowestBars reports |
| how long ago the window high was | new Highest, .update(x), field value | how many bars ago the window's high sits, as 0 or a negative number (0 = this bar, -3 = three bars ago); value is the high itself |
| how long ago the window low was | new Lowest, .update(x), field value | the same offset for the window's low |
| bars since a condition | new BarsSince(), .update(cond) | bars elapsed since cond was last true (0 on that bar), NaN until it has been true once |
| the value when a condition last held | new Value, .update(cond, x) | x on the occurrence-th most recent bar where cond was true (0 = the latest, the current bar counts) |
| a percentile over a window | new Percentile(, .update(x) | the nearest-rank pct-th percentile over the window |
| a median over a window | new Median(period), .update(x) | the middle value (the mean of the two middle values on an even window) |
| a correlation over a window | new Correlation(, .update(a, b) | the rolling Pearson correlation, -1..1 |
| a window total | new Sum(period), .update(x) | the sum of the last period values; no warm-up, bar 0 already returns the partial window |
| a z-score over a window | new Zscore(period), .update(x) | (x - mean) / stdev over the window; 0 when the deviation is exactly 0 |
A condition is an f64: true means finite and not 0, so a 0/1
series or a comparison cast with ? 1.0 : 0.0 both work. You feed the
class the series it should scan, so new Highest(20) over bar.high()
is the 20-bar high and a Lowest(20)
over bar.low() is the 20-bar low. Among equal values the newest bar wins
the offset. The bars fields are handy for "is the high in the window
recent?" logic, and BarsSince counts up from the last time a condition
fired, the natural cooldown and recency check.
Construct the windowed ones in onStart() from a param and update each
once per bar:
let highs = new Highest(20);
let lows = new Lowest(20);
let p80 = new Percentile(20, 80.0);
let median = new Median(20);
const rising = new Rising(3);
// In onBar(): the return value is the extreme, the field is its offset.
// const hi = highs.update(high); // highs.bars: 0 = this bar, -3 = three bars ago
// const lo = lows.update(low);
// const up = rising.update(close); // 1, 0, or NaN for the first 3 barsEvery windowed class here follows the strict window rule:
NaN until period bars exist, and NaN whenever any value inside the
window is not finite. Percentile sorts the window and takes rank
ceil(pct / 100 * period) (1-based, pct clamped to 0..100); Median
sorts and takes the middle, averaging the two middle values on an even
period; Correlation returns NaN when either side has no variance. None
of them allocates after construction. Sum and Zscore are the two that
skip a NaN bar instead of refusing the window (Zscore still divides
its variance by period), and Sum has no warm-up at all: a 20-bar Sum
draws from the first bar where a 20-bar Stdev draws from bar 19.
Pivot confirmation
PivotHigh and PivotLow are causal confirmation signals. A pivot only
emits after rightbars later bars have closed, so the emitted value
appears on the confirmation bar and lags the true pivot bar by
rightbars. Nothing reads a future bar and no earlier bar repaints; an
indicator could not do otherwise, because onBar() sees one bar.
new PivotHigh(leftbars, rightbars) and new PivotLow(leftbars, rightbars) keep left + right + 1 bars and, once the window is full,
test the candidate rightbars back: strictly higher than every other
value in the window is a pivot high, strictly lower a pivot low (a tie
never counts), and update(x) returns the candidate's value on the
confirmation bar only and NaN everywhere else, including any window
with a non-finite value. Feed PivotHigh the high and PivotLow the
low.
const pivotHigh = new PivotHigh(2, 2);
const pivotLow = new PivotLow(2, 2);
const lastHigh = new Fixnan();
// In onBar(): a value on the confirmation bar, NaN between.
// const confirmed = pivotHigh.update(bar.high());
// const level = lastHigh.update(confirmed); // the last confirmed pivot, carried forwardA confirmed pivot is a sparse series: one value on the confirmation bar,
NaN between. To carry it forward into a continuous line, run it
through Fixnan below.
Handling missing values
Warm-up is NaN, and any arithmetic touching NaN stays NaN. isNaN
and isFinite are AssemblyScript builtins on f64; there is no separate
missing-value type, so a missing value is NaN everywhere and these two
are the whole vocabulary. The helpers are two lines each:
| You want | wrun form |
|---|---|
| is the value missing | isNaN(x) |
| is the value a real number | isFinite(x) (finite and not NaN) |
| a replacement for a missing value | nz(x, replacement) below |
| the last real value carried forward | new Fixnan(), .update(x): carries the last finite value forward, NaN until the first one |
function nz(x: f64, replacement: f64): f64 {
return isNaN(x) ? replacement : x;
}
const fixnan = new Fixnan();
// In onBar(): fixnan.update(sparse) repeats the last finite value across every NaN bar.Every series function in one module
A sparse series (forced NaN for the first 10 bars) drives the missing
value helpers so you can see them flip; the rest run over real bars. The
classes are the shipped ones from ./sdk/ta.
param("fast", 5, { min: 1, max: 200 });
param("slow", 13, { min: 2, max: 400 });
param("window", 10, { min: 2, max: 200, description: "Window for the extremes, percentile, median, and correlation" });
output("crossover", line, lower, { color: "#2563eb", description: "1 on the bar the fast average crosses above the slow" });
output("crossunder", line, lower, { color: "#dc2626", description: "1 on the bar it crosses below" });
output("cross", line, lower, { color: "#7c3aed", description: "1 on either cross" });
output("rising", line, lower, { color: "#16a34a", description: "1 while the close is above its previous 3 values" });
output("falling", line, lower, { color: "#ea580c", description: "1 while the close is below its previous 3 values" });
output("barssince", line, lower, { color: "#0891b2", description: "Bars since the close was above the fast average" });
output("change", line, lower, { color: "#4b5563", description: "3-bar change" });
output("highest", line, lower, { color: "#0f766e", description: "Window highest high" });
output("lowest", line, lower, { color: "#be123c", description: "Window lowest low" });
output("highestbars", line, lower, { color: "#9333ea", description: "Offset of the window high (0 = this bar, negative = bars ago)" });
output("lowestbars", line, lower, { color: "#1d4ed8", description: "Offset of the window low" });
output("valuewhen", line, lower, { color: "#0e7490", description: "The close on the most recent bullish cross" });
output("percentile", line, lower, { color: "#b45309", description: "80th percentile of the close over the window" });
output("median", line, lower, { color: "#a16207", description: "Median close over the window" });
output("correlation", line, lower, { color: "#15803d", description: "Correlation between open and close over the window" });
output("nz", line, lower, { color: "#6d28d9", description: "nz over the sparse series, the open as the replacement" });
output("isna", line, lower, { color: "#0e7490", description: "1 while the sparse series is NaN" });
output("isnum", line, lower, { color: "#374151", description: "1 while the sparse series is a finite number" });
output("fixnan", line, lower, { color: "#3b82f6", description: "The sparse series with gaps carried forward" });
output("pivot_high", line, lower, { color: "#2563eb", description: "Confirmed 2/2 pivot highs, carried forward" });
output("pivot_low", line, lower, { color: "#dc2626", description: "Confirmed 2/2 pivot lows, carried forward" });
function nz(x: f64, replacement: f64): f64 {
return isNaN(x) ? replacement : x;
}
let fast = new Sma(5);
let slow = new Sma(13);
const cross = new Cross();
const rising = new Rising(3);
const falling = new Falling(3);
const barsSince = new BarsSince();
const change = new Change(3);
let highs = new Highest(10);
let lows = new Lowest(10);
const valueWhen = new ValueWhen(0);
let percentile = new Percentile(10, 80.0);
let median = new Median(10);
let correlation = new Correlation(10);
const fixnan = new Fixnan();
const pivotHigh = new PivotHigh(2, 2);
const pivotLow = new PivotLow(2, 2);
const stableHigh = new Fixnan();
const stableLow = new Fixnan();
let barIndex: i32 = 0;
function onStart(): void {
fast = new Sma(i32(p_fast()));
slow = new Sma(i32(p_slow()));
const window = i32(p_window());
highs = new Highest(window);
lows = new Lowest(window);
percentile = new Percentile(window, 80.0);
median = new Median(window);
correlation = new Correlation(window);
}
function onBar(): void {
const close = bar.close();
const openValue = bar.open();
const f = fast.update(close);
const s = slow.update(close);
const crossed = cross.update(f, s);
const risingValue = rising.update(close);
const fallingValue = falling.update(close);
const since = barsSince.update(!isNaN(f) && close > f ? 1.0 : 0.0);
const changeValue = change.update(close);
const highValue = highs.update(bar.high());
const lowValue = lows.update(bar.low());
const whenValue = valueWhen.update(crossed == 1 ? 1.0 : 0.0, close);
const pctValue = percentile.update(close);
const medianValue = median.update(close);
const corrValue = correlation.update(openValue, close);
const sparse = barIndex < 10 ? NaN : close;
const fixed = fixnan.update(sparse);
const pivotHighValue = stableHigh.update(pivotHigh.update(bar.high()));
const pivotLowValue = stableLow.update(pivotLow.update(bar.low()));
barIndex += 1;
out_crossover(crossed == 1 ? 1.0 : 0.0);
out_crossunder(crossed == -1 ? 1.0 : 0.0);
out_cross(crossed != 0 ? 1.0 : 0.0);
out_rising(risingValue);
out_falling(fallingValue);
out_barssince(since);
out_change(changeValue);
out_highest(highValue);
out_lowest(lowValue);
out_highestbars(highs.bars);
out_lowestbars(lows.bars);
out_valuewhen(whenValue);
out_percentile(pctValue);
out_median(medianValue);
out_correlation(corrValue);
out_nz(nz(sparse, openValue));
out_isna(isNaN(sparse) ? 1.0 : 0.0);
out_isnum(isFinite(sparse) ? 1.0 : 0.0);
out_fixnan(fixed);
out_pivot_high(pivotHighValue);
out_pivot_low(pivotLowValue);
}barIndex is state the module counts itself: there is no barIndex
global, so it lives at module level with the rest of the state that
spans bars.
A Donchian breakout
A 20-bar high and low channel as a Highest over the high
and a Lowest over the low, with a gated mark on the bar that closes
above the channel. The gate is a data-only output, and the shape output
draws only where it is nonzero.
param("period", 20, { min: 2, max: 400, description: "Channel lookback" });
output("hi", line, overlay, { color: "#16a34a", width: 1, description: "Upper Donchian band" });
output("lo", line, overlay, { color: "#dc2626", width: 1, description: "Lower Donchian band" });
output("breakout_mark", shape, overlay, { color: "#16a34a", shape_where: "breakout", description: "The close on a breakout bar" });
output("breakout", none, overlay, { description: "1 when the close is above the previous bar's upper band" });
let highs = new Highest(20);
let lows = new Lowest(20);
let prevHi: f64 = NaN;
function onStart(): void {
highs = new Highest(i32(p_period()));
lows = new Lowest(i32(p_period()));
}
function onBar(): void {
const close = bar.close();
// Test against the channel as it stood BEFORE this bar, so the bar cannot break its own high.
const breakout = !isNaN(prevHi) && close > prevHi ? 1.0 : 0.0;
const hi = highs.update(bar.high());
const lo = lows.update(bar.low());
prevHi = hi;
if (isNaN(hi)) return;
out_hi(hi);
out_lo(lo);
out_breakout_mark(close);
out_breakout(breakout);
}Window statistics, a channel, and a cross
The window statistics (Highest, Lowest, Sum, Stdev), a Donchian
channel, a Cross over two averages, and an isFinite gate in one
module. Stdev and Donchian are explained with their families, on
Trend and volatility and
Moving averages.
param("period", 20, { min: 2, max: 400 });
output("low20", line, overlay, { color: "#dc2626", width: 1, description: "Lowest low over the period" });
output("high20", line, overlay, { color: "#16a34a", width: 1, description: "Highest high over the period" });
output("donchian_mid", line, overlay, { color: "#94a3b8", width: 1, description: "Donchian midpoint" });
output("volume_sum", line, lower, { color: "#0891b2", description: "Volume summed over the period" });
output("volatility", line, lower, { color: "#7c3aed", description: "Standard deviation of the close" });
output("bullish", none, lower, { description: "1 on the bar the fast average crosses above the slow" });
output("bearish", none, lower, { description: "1 on the bar it crosses below" });
output("any_cross", none, lower, { description: "1 on either" });
output("valid", none, lower, { description: "1 when every input on the bar is a finite number" });
let highs = new Highest(20);
let lows = new Lowest(20);
let volumeSum = new Sum(20);
let stdev = new Stdev(20);
let donchian = new Donchian(20);
let fast = new Sma(5);
let slow = new Sma(20);
const cross = new Cross();
function onStart(): void {
const period = i32(p_period());
highs = new Highest(period);
lows = new Lowest(period);
volumeSum = new Sum(period);
stdev = new Stdev(period);
donchian = new Donchian(period);
fast = new Sma(5);
slow = new Sma(period);
}
function onBar(): void {
const close = bar.close();
const high = bar.high();
const low = bar.low();
const volume = bar.volume();
const valid = isFinite(close) && isFinite(high) && isFinite(low) && isFinite(volume) ? 1.0 : 0.0;
const highValue = highs.update(high);
const lowValue = lows.update(low);
const sumValue = volumeSum.update(volume);
const volatility = stdev.update(close);
donchian.update(high, low);
const crossed = cross.update(fast.update(close), slow.update(close));
out_low20(lowValue);
out_high20(highValue);
out_donchian_mid(donchian.basis);
out_volume_sum(sumValue);
out_volatility(volatility);
out_bullish(crossed == 1 ? 1.0 : 0.0);
out_bearish(crossed == -1 ? 1.0 : 0.0);
out_any_cross(crossed != 0 ? 1.0 : 0.0);
out_valid(valid);
}Warm-up and the missing-value window
Windowed functions cannot produce a value until they have seen enough
bars: a 20-bar Percentile or Correlation returns NaN until 20 bars
exist, just as a 20-bar Sma does. When the underlying series is itself
NaN for a stretch, the warm-up shifts forward by the same amount,
because the windowed classes refuse a window with a NaN inside it.
Fixnan shows the recovery: it produces its first real value the moment
the source has one, then holds it across every gap. Feed the sparse
series from the tour into a 5-bar Percentile and the percentile
output stays NaN through bar 14, not bar 4.
Practices that carry over
- Lookback periods. Choose them for the interval you trade: short
windows for scalping and lower timeframes, longer ones for swing
context. Declare the range on the param (
min,max) and size buffers frommaxso the module never allocates per bar. - One object per statistic. Compute a statistic once per bar and reuse the return value or field rather than constructing a second object over the same series.
- Cross detection. Combine a cross with a trend read (
Adx, a slope, a higher-timeframe fold) before treating it as a signal; in a chop the cross flips every few bars. - Donchian channels. Breakout logic in trends, support and resistance in ranges: the same two outputs, different rules on top.
- Data validation. Check
isFinitebefore a division, and writeNaNrather than a made-up number when a value has no answer; aNaNdraws nothing and never trips an alert.