---
title: Series Functions
description: Crossovers, rolling extremes, change, percentile, correlation, and na handling for time series.
---

Series functions operate over a stream of bars rather than a single value. They answer the questions that come up constantly when writing indicators: did two lines just cross, is a series rising, what's the highest high in the last 10 bars, how many bars since a condition was true, and is this value real or missing.

## Price-source helpers

Since 3.0.13 (in-flight), `hl2`, `hlc3`, `ohlc4`, and `hlcc4` expose the common OHLC averages as builtins. Each accepts an optional OHLC source; when you omit `source`, the helper reads the chart's current OHLC source. The verified formulas are `hl2 = (high + low) / 2`, `hlc3 = (high + low + close) / 3`, `ohlc4 = (open + high + low + close) / 4`, and `hlcc4 = (high + low + close + close) / 4`.

| Function | Signature | Formula |
| --- | --- | --- |
| `hl2` | `hl2(source?)` | `(high + low) / 2` |
| `hlc3` | `hlc3(source?)` | `(high + low + close) / 3` |
| `ohlc4` | `ohlc4(source?)` | `(open + high + low + close) / 4` |
| `hlcc4` | `hlcc4(source?)` | `(high + low + close + close) / 4` |

The probe below checked explicit-source and implicit-source calls against the manual formulas. The ladder reported `ok: true`; both plotted checks had 300 finite values from bar 0 to 299, moving from `441.6605495613245` to `459.47412156950656`.

```javascript title="scripts/probes/series-fns/price_source_helpers.ks"
//@version=2
define(title="Price Source Helpers", position="offchart", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)

var explicitSum = hl2(source=trade) + hlc3(source=trade) + ohlc4(source=trade) + hlcc4(source=trade)
var manualSum = ((trade.high + trade.low) / 2) + ((trade.high + trade.low + trade.close) / 3) + ((trade.open + trade.high + trade.low + trade.close) / 4) + ((trade.high + trade.low + trade.close + trade.close) / 4)
var implicitSum = hl2() + hlc3() + ohlc4() + hlcc4()
var explicitCheck = math.abs(explicitSum - manualSum) < 0.000001 ? explicitSum : na
var implicitCheck = math.abs(implicitSum - manualSum) < 0.000001 ? implicitSum : na

plotLine(value=explicitCheck, colors=["#2563eb"], width=2, label=["Explicit"], desc=["price helpers with explicit source match manual OHLC formulas"])
plotLine(value=implicitCheck, colors=["#16a34a"], width=2, label=["Implicit"], desc=["price helpers without source use chart OHLC"])
```

## Crossovers and signals

The crossover family turns two lines into a discrete signal. They're the backbone of almost every entry rule.

- `crossover(a, b)` is true on the single bar where `a` rises above `b`.
- `crossunder(a, b)` is true on the bar where `a` falls below `b`.
- `cross(a, b)` is true on either: the bar they touch or swap order.

This is the classic signal-line pattern. A bullish MACD cross is just `crossover` of the MACD line over its signal:

```javascript
//@version=2
define(title="MACD cross", position="offchart", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)
var m = macd(source=trade.close, fastPeriod=12, slowPeriod=26, signalPeriod=9)

// 1 on the bar MACD crosses up through its signal, otherwise 0.
var bullish = crossover(seriesA=m.macd, seriesB=m.signal) ? 1 : 0

plotLine(value=bullish, colors=["#16a34a"], width=2, label=["Bull cross"], desc=["MACD crossing above its signal line"])
```

Crossovers return a boolean, so a plot value of `cond ? 1 : 0` gives you a clean 0/1 step you can see on the chart. To place a marker on price instead, feed the price to `plotShape` on signal bars and `na` otherwise: `plotShape(value=bullish ? trade.low : na, ...)`.

{% hint style="info" %}
The crossover family expects `timeseries` inputs (it needs the previous bar to detect the turn). Declare the two lines as `timeseries`, or pass indicator outputs that already carry history.


{% endhint %}
## Trend, extremes, and counting

| Function | Signature | Returns |
| --- | --- | --- |
| `rising` | `rising(source, period)` | true if `source` rose over the last `period` bars |
| `falling` | `falling(source, period)` | true if `source` fell over the last `period` bars |
| `change` | `change(source, n)` | `source` now minus `source` `n` bars ago |
| `highest` | `highest(source, period, priceIndex?)` | highest value in the window |
| `lowest` | `lowest(source, period, priceIndex?)` | lowest value in the window |
| `highestbars` | `highestbars(source, period, priceIndex?)` | bar offset back to the window's high |
| `lowestbars` | `lowestbars(source, period, priceIndex?)` | bar offset back to the window's low |
| `pivothigh` | `pivothigh(source, leftbars, rightbars, priceIndex)` | pivot high value, emitted at confirmation |
| `pivotlow` | `pivotlow(source, leftbars, rightbars, priceIndex)` | pivot low value, emitted at confirmation |
| `barssince` | `barssince(condition)` | bars elapsed since `condition` was last true |
| `percentile` | `percentile(source, period, pct)` | the `pct`-th percentile over the window |
| `correlation` | `correlation(s1, s2, period)` | rolling correlation of two series (`-1` to `1`) |

`highest`/`lowest` take an OHLCV source plus a `priceIndex` to pick the field (`2` = high, `3` = low). `highestbars`/`lowestbars` return how many bars back the extreme sits, handy for "is the high in the window recent?" logic. `barssince` counts up from the last time a condition fired, which is perfect for cooldowns and recency checks.

```javascript
//@version=2
define(title="Donchian breakout", position="onchart", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)

// 20-bar high && low channel.
var hi = highest(source=trade, period=20, priceIndex=2)
var lo = lowest(source=trade, period=20, priceIndex=3)

plotLine(value=hi, colors=["#16a34a"], width=1, label=["20-bar high"], desc=["Upper Donchian band"])
plotLine(value=lo, colors=["#dc2626"], width=1, label=["20-bar low"], desc=["Lower Donchian band"])
```
## Pivot confirmation

Since 3.0.13 (in-flight), `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`. The function does not read future bars and does not repaint a prior bar; if you want a continuous line, forward-fill the sparse confirmation series yourself.

The probe below uses `leftbars=2` and `rightbars=2`, then checks each emitted value against `trade.high[2]` or `trade.low[2]` on the confirmation bar. The ladder reported `ok: true`; pivot highs produced 6 sparse finite confirmations from bar 43 to 263, pivot lows produced 7 from bar 14 to 278, and the forward-filled stability lines remained finite from their first confirmed pivot through bar 299.

```javascript title="scripts/probes/series-fns/pivot_confirmation_stability.ks"
//@version=2
define(title="Pivot Confirmation Stability", position="offchart", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)
timeseries pivotHigh = pivothigh(source=trade, leftbars=2, rightbars=2, priceIndex=2)
timeseries pivotLow = pivotlow(source=trade, leftbars=2, rightbars=2, priceIndex=3)

var highAtConfirmation = isna(pivotHigh) ? na : math.abs(pivotHigh - trade.high[2]) < 0.000001 ? pivotHigh : na
var lowAtConfirmation = isna(pivotLow) ? na : math.abs(pivotLow - trade.low[2]) < 0.000001 ? pivotLow : na
var stableHigh = fixnan(source=pivotHigh)
var stableLow = fixnan(source=pivotLow)

plotLine(value=highAtConfirmation, colors=["#2563eb"], width=2, label=["Pivot high"], desc=["pivothigh emits the pivot value on the confirmation bar"])
plotLine(value=lowAtConfirmation, colors=["#dc2626"], width=2, label=["Pivot low"], desc=["pivotlow emits the pivot value on the confirmation bar"])
plotLine(value=stableHigh, colors=["#16a34a"], width=2, label=["Stable high"], desc=["confirmed pivot high remains readable through history"])
plotLine(value=stableLow, colors=["#ea580c"], width=2, label=["Stable low"], desc=["confirmed pivot low remains readable through history"])
```
## Handling missing values (`na`)

Indicators return `na` during their warmup window, and any arithmetic involving `na` stays `na`. These helpers let you test for it and recover:

- `isna(x)` / `isnan(x)`: true when `x` is missing. (`NaN` is an alias of `na`, so both report it.)
- `isnum(x)`: true when `x` is a real, finite number.
- `nz(x, replacement)`: `x` if it's a number, otherwise `replacement`. The standard way to fill gaps before plotting.
- `fixnan(source)`: carries the last real value forward across `na` bars, leaving no holes once data starts.

See [na & Scalar Types](../core-concepts/na-and-scalar-types.md) for how `na` propagates through expressions.

## Every series function in one script

A `sparse` series (forced `na` for the first 10 bars) drives the `na` helpers so you can watch them flip; the rest run over real OHLCV.

```javascript title="scripts/probes/series-fns/all_series_functions.ks"
//@version=2
define(title="Verified Series Functions", position="offchart", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)
timeseries closeSeries = trade.close
timeseries fast = sma(source=closeSeries, period=5)
timeseries slow = sma(source=closeSeries, period=13)
timeseries aboveFast = closeSeries > fast
timeseries sparse = barIndex < 10 ? na : closeSeries

plotLine(value=crossover(seriesA=fast, seriesB=slow) ? 1 : 0, colors=["#2563eb"], label=["crossover"], desc=["crossover boolean as 1 or 0"])
plotLine(value=crossunder(seriesA=fast, seriesB=slow) ? 1 : 0, colors=["#dc2626"], label=["crossunder"], desc=["crossunder boolean as 1 or 0"])
plotLine(value=cross(seriesA=fast, seriesB=slow) ? 1 : 0, colors=["#7c3aed"], label=["cross"], desc=["cross boolean as 1 or 0"])
plotLine(value=rising(source=closeSeries, period=3), colors=["#16a34a"], label=["rising"], desc=["rising over 3 bars"])
plotLine(value=falling(source=closeSeries, period=3), colors=["#ea580c"], label=["falling"], desc=["falling over 3 bars"])
plotLine(value=barssince(condition=aboveFast), colors=["#0891b2"], label=["barssince"], desc=["bars since close was above fast average"])
plotLine(value=change(source=closeSeries, n=3), colors=["#4b5563"], label=["change"], desc=["3 bar change"])
plotLine(value=highest(source=trade, period=10, priceIndex=2), colors=["#0f766e"], label=["highest"], desc=["10 bar highest high"])
plotLine(value=lowest(source=trade, period=10, priceIndex=3), colors=["#be123c"], label=["lowest"], desc=["10 bar lowest low"])
plotLine(value=highestbars(source=trade, period=10, priceIndex=2), colors=["#9333ea"], label=["highestbars"], desc=["offset to 10 bar highest high"])
plotLine(value=lowestbars(source=trade, period=10, priceIndex=3), colors=["#1d4ed8"], label=["lowestbars"], desc=["offset to 10 bar lowest low"])
plotLine(value=percentile(source=closeSeries, period=20, pct=80), colors=["#b45309"], label=["percentile"], desc=["80th percentile over 20 bars"])
plotLine(value=correlation(s1=trade.open, s2=trade.close, period=20), colors=["#15803d"], label=["correlation"], desc=["20 bar correlation between open and close"])
plotLine(value=nz(x=sparse, replacement=trade.open), colors=["#6d28d9"], label=["nz"], desc=["nz replacement for leading na values"])
plotLine(value=isna(sparse) ? 1 : 0, colors=["#0e7490"], label=["isna"], desc=["isna result for sparse series"])
plotLine(value=isnan(sparse) ? 1 : 0, colors=["#991b1b"], label=["isnan"], desc=["isnan result for sparse series"])
plotLine(value=isnum(sparse) ? 1 : 0, colors=["#374151"], label=["isnum"], desc=["isnum result for sparse series"])
plotLine(value=fixnan(source=sparse), colors=["#3b82f6"], label=["fixnan"], desc=["fixnan result for sparse series"])
```


## Warmup and the na window

Windowed functions can't produce a value until they've seen enough bars: a 20-bar `percentile` or `correlation` returns `na` until 20 bars exist, just as a 20-bar SMA does. When the underlying series is itself `na` for a stretch, that warmup shifts forward by the same amount.

The script below makes the warmup visible. It feeds a `sparse` series that's `na` for the first 10 bars into a 5-bar `percentile` and `correlation`, plotting `1` while each is still warming up. `fixnan` shows the recovery: it produces its first real value the moment the source has one (bar 10 here), then holds steady.

```javascript title="scripts/probes/series-fns/na_window_boundary.ks"
//@version=2
define(title="Series NA Window Boundary", position="offchart", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)
timeseries closeSeries = trade.close
timeseries sparse = barIndex < 10 ? na : closeSeries
var percentileWarmup = isna(percentile(source=sparse, period=5, pct=50)) ? 1 : 0
var correlationWarmup = isna(correlation(s1=sparse, s2=trade.open, period=5)) ? 1 : 0
var fixed = fixnan(source=sparse)

plotLine(value=percentileWarmup, colors=["#2563eb"], label=["Percentile warmup"], desc=["1 while percentile lacks a finite 5 bar window"])
plotLine(value=correlationWarmup, colors=["#dc2626"], label=["Correlation warmup"], desc=["1 while correlation lacks a finite 5 bar window"])
plotLine(value=fixed, colors=["#16a34a"], label=["Fixnan"], desc=["fixnan after leading na values"])
```
