Functions

Series Functions

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.

FunctionSignatureFormula
hl2hl2(source?)(high + low) / 2
hlc3hlc3(source?)(high + low + close) / 3
ohlc4ohlc4(source?)(open + high + low + close) / 4
hlcc4hlcc4(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.

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:

//@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, ...).

Trend, extremes, and counting

FunctionSignatureReturns
risingrising(source, period)true if source rose over the last period bars
fallingfalling(source, period)true if source fell over the last period bars
changechange(source, n)source now minus source n bars ago
highesthighest(source, period, priceIndex?)highest value in the window
lowestlowest(source, period, priceIndex?)lowest value in the window
highestbarshighestbars(source, period, priceIndex?)bar offset back to the window's high
lowestbarslowestbars(source, period, priceIndex?)bar offset back to the window's low
pivothighpivothigh(source, leftbars, rightbars, priceIndex)pivot high value, emitted at confirmation
pivotlowpivotlow(source, leftbars, rightbars, priceIndex)pivot low value, emitted at confirmation
barssincebarssince(condition)bars elapsed since condition was last true
percentilepercentile(source, period, pct)the pct-th percentile over the window
correlationcorrelation(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.

//@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.

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

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.

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"])