Statistics & regression
Rolling stdev/variance/dev, correlation, normalize, percentrank/percentile/median, linreg, polyreg2 (+ stderr), zscore. The stats helpers that anchor every quant indicator.
The statistics family is what separates a "moving average" indicator from a "z-score-of-deviation-from-the-regression-fit" indicator. Every function is rolling — it operates over a period-bar window ending at the current bar, returning a series of the same length.
Rolling dispersion
stdev(src, period)
Rolling standard deviation over period bars. Uses population divisor (÷ period), not sample (÷ period − 1), so two consecutive calls with period=p and period=p+1 differ by one bar's contribution rather than by a Bessel correction.
sigma = stdev(close, 20)
variance(src, period)
Rolling variance — stdev(src, period)². Slightly faster than pow(stdev(...), 2) because it avoids the sqrt.
dev(src, period)
Mean absolute deviation around the rolling SMA: Σ|src − sma(src, p)| / p. This is what CCI uses internally (with its × 0.015 constant baked in).
mad = dev(close, 20)
Normalisation
normalize(src, fromMin, fromMax, toMin=0, toMax=1)
Map src from [fromMin, fromMax] into [toMin, toMax] linearly. Any of the four bounds may be a series — the most common use is mapping an oscillator into the visible price range so it overlays the candle pane:
@pane "overlay"
r = rsi(close, 14)
priceLow = lowest(low, 200)
priceHigh = highest(high, 200)
rsiOverlay = normalize(r, 0, 100, priceLow, priceHigh)
plotLine(rsiOverlay, color="#F0B90B", width=1.5)
When fromMin == fromMax the output is toMin (avoids divide-by-zero).
Ranks & percentiles
percentrank(src, period)
Rank of src[i] within its rolling window as a 0-100 percentile. 100 means the current bar is the maximum of the window; 0 means the minimum.
rank = percentrank(close, 50)
plotShape(rank > 95, low, shape="circle", color="#0ECB81") // top 5% breakouts
percentile(src, period, percent)
Linearly-interpolated percentile of the rolling window. percent is [0, 100]. percentile(close, 50, 50) is the rolling 50-bar median.
median(src, period)
Shortcut for percentile(src, period, 50).
mid = median(close, 50)
plotLine(mid, color="rgba(240,185,11,0.6)", width=1)
Correlation & regression
correlation(a, b, period)
Pearson correlation coefficient over a rolling window. Range [−1, +1]. Use to gauge how tightly two series move together.
@pane "below"
@input length = input.int(20, "Length", minval=5, maxval=200)
corr = correlation(close, volume, length)
paneRange(-1, 1)
paneHline( 0, color="rgba(255,255,255,0.2)")
paneLine(corr, color="#F0B90B", width=1.5)
When the rolling stdev of either series is zero (a flat segment), correlation is mathematically undefined and the function returns NaN.
linreg(src, period, offset=0)
Linear-regression value at the END of the window. offset=0 is the newest bar (right edge of the lookback); positive offset is older bars; negative offset projects FORWARD past the right edge.
fit = linreg(close, 50, 0) // current-bar fit
proj = linreg(close, 50, -3) // 3-bar forward projection
polyreg2(src, period, offset=0)
Quadratic (degree-2) polynomial regression — fits y ≈ c₀ + c₁·x + c₂·x² to the last period bars by closed-form Cramer's-rule solve. Same offset semantics as linreg. Catches mild curvature (acceleration / deceleration of the trend) where a straight line would miss it.
fit = polyreg2(close, 50, 0)
plotLine(fit, color="#F0B90B", width=2)
polyreg2_stderr(src, period)
Standard error of the quadratic regression — sqrt(Σ(y − fit)² / period). Use as the band offset for a regression channel:
@input length = input.int(50, "Length", minval=10, maxval=400)
@input widthMul = input.float(2.0, "Width × stderr", minval=0.5, maxval=4.0, step=0.1)
mid = polyreg2(close, length, 0)
stderr = polyreg2_stderr(close, length)
upper = mid + stderr * widthMul
lower = mid - stderr * widthMul
plotLine(mid, color="#F0B90B", width=2)
plotLine(upper, color="rgba(240,185,11,0.5)")
plotLine(lower, color="rgba(240,185,11,0.5)")
plotBand(upper, lower, color="rgba(240,185,11,0.04)")
Z-score
zscore(src, period)
Rolling z-score: (src − sma(src, p)) / stdev(src, p). Says "how many standard deviations away from the rolling mean is the current bar?". NaN where the local stdev is zero (perfectly flat segment).
@pane "below"
z = zscore(close, 50)
paneRange(-4, 4)
paneHline( 2, color="rgba(246,70,93,0.6)")
paneHline(-2, color="rgba(14,203,129,0.6)")
paneHline( 0, color="rgba(255,255,255,0.2)")
paneLine(z, color="#F0B90B", width=1.5)
plotShape(z > 2, high, shape="arrowDown", color="#F6465D", size=8)
plotShape(z < -2, low, shape="arrowUp", color="#0ECB81", size=8)
Patterns
Bollinger bands from scratch
The built-in bb_* functions wrap this — but it's a useful exercise:
@input length = input.int(20, "Length")
@input mult = input.float(2.0, "Std Dev")
mid = sma(close, length)
sigma = stdev(close, length)
upper = mid + sigma * mult
lower = mid - sigma * mult
plotLine(upper, color="rgba(240,185,11,0.5)")
plotLine(mid, color="#F0B90B")
plotLine(lower, color="rgba(240,185,11,0.5)")
plotBand(upper, lower, color="rgba(240,185,11,0.04)")
"Statistically significant" breakout filter
@input zThresh = input.float(2.5, "Z threshold", minval=1.0, maxval=5.0, step=0.1)
z = zscore(close, 50)
breakout = abs(z) > zThresh
plotShape(breakout and z > 0, high, shape="arrowUp", color="#0ECB81")
plotShape(breakout and z < 0, low, shape="arrowDown", color="#F6465D")
Regression channel with optional fork
@input mode = input.string("linear", "Mode", options="linear,quadratic")
@input len = input.int(60, "Length")
mid = na
stde = na
if mode == "linear"
mid := linreg(close, len, 0)
// For linear, we approximate stderr by 1.5× rolling stdev — a
// cheap fallback that approximates the channel width without
// computing a separate linear-regression stderr.
stde := stdev(close, len) * 1.5
else
mid := polyreg2(close, len, 0)
stde := polyreg2_stderr(close, len)
end
upper = mid + stde * 2
lower = mid - stde * 2
plotLine(mid, color="#F0B90B", width=2)
plotLine(upper, color="rgba(240,185,11,0.5)")
plotLine(lower, color="rgba(240,185,11,0.5)")
Performance notes
- Every function in this family is O(n × period) in the worst case — each bar walks its window. For long histories with
period > 200, this is the dominant cost. stdev/varianceuse a running-sum optimisation so they stay close to O(n).polyreg2solves a 3×3 system per bar via Cramer's rule — a fixed amount of arithmetic, so it's O(n) overall, faster than naive least-squares.correlation,linreg,percentilewalk the window per bar.- The whole-script loop budget is 5 000 000 iterations (see Pitfalls). For a 50-bar
polyreg2over 5 000 bars that's well under the cap.
Next
- Color helpers —
rgb,rgba,withAlpha,gradient. - Drawing — rendering the series this page produces.
- Recipes — full statistical indicators end-to-end.