---
title: "Series functions"
description: "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…"
order: 46
section: "functions"
---

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

# 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](../core-concepts/execution-model.md)),
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](ta-library.md) 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](extra-indicators.md). The pivots below are also
the raw material of the [Market structure kit](market-structure-kit.md),
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.

```typescript sample=fn-price-helpers
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](../presentation/plotting.md)):

```typescript
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 HighestBars(period)`, `.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 LowestBars(period)`, `.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 ValueWhen(occurrence)`, `.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(period, pct)`, `.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(period)`, `.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:

```typescript
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 bars
```

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

```typescript
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 forward
```

A 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 |

```typescript
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`.

```typescript sample=fn-series-tour
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.

```typescript sample=fn-donchian-breakout
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](trend-indicators.md#volatility-primitives) and
[Moving averages](moving-averages.md#donchian).

```typescript sample=fn-utility-tour
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
  from `max` so 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 `isFinite` before a division, and write `NaN`
  rather than a made-up number when a value has no answer; a `NaN` draws
  nothing and never trips an alert.
