Indicator · Pine Script v6 · On the chart

Z-Score Lab

by jayadevrana

Z-Score Lab plots the simple moving average of close and bands at one and two standard deviations on the price chart, and computes the current z-score of price versus that mean.

View on GitHub

GitHub stars1on the repository
Pine versionv6current
Lines of code140
Settings4inputs
LicenceMPL-2.0
Last update4 days ago26 Oct 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.

Source Code

140 lines of Pine Script v6 from scripts/zscore-mean-reversion.pine.txt, published under MPL-2.0. Keep the author's credit and licence lines when you reuse it.

zscore-mean-reversion.pine.txt
//@version=6
indicator("Z-Score Lab", overlay = true)

// 1. Settings
length   = input.int(200, "Lookback for mean and stdev", minval = 20)
sample   = input.int(1000, "Bars in the histogram", minval = 100)
binWidth = input.float(0.5, "Histogram bin width (sigma)", step = 0.25)
showNorm = input.bool(true, "Show what a normal distribution predicts")

// 2. The z-score: how many standard deviations from the mean
mean  = ta.sma(close, length)
sigma = ta.stdev(close, length)
z     = sigma > 0 ? (close - mean) / sigma : 0.0

// 3. The bands are just z = 1, 2 and 3 translated back into price
plot(mean, "Mean", color.orange, 2)
p1u = plot(mean + sigma, "+1", color.new(color.gray, 40))
p1d = plot(mean - sigma, "-1", color.new(color.gray, 40))
p2u = plot(mean + 2 * sigma, "+2", color.new(color.blue, 30))
p2d = plot(mean - 2 * sigma, "-2", color.new(color.blue, 30))
fill(p1u, p1d, color.new(color.gray, 92))
fill(p2u, p1u, color.new(color.blue, 94))
fill(p1d, p2d, color.new(color.blue, 94))

// 4. Keep the recent z-scores so we can measure their real shape
var array<float> zs = array.new<float>()
if not na(z) and bar_index > length
    array.push(zs, z)
    if array.size(zs) > sample
        array.shift(zs)

// 5. The normal distribution's own answer, for comparison (Abramowitz-Stegun)
normCdf(x) =>
    t = 1.0 / (1.0 + 0.2316419 * math.abs(x))
    d = 0.3989423 * math.exp(-x * x / 2)
    p = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 +
      t * (-1.821256 + t * 1.330274))))
    x > 0 ? 1.0 - p : p

// 6. The stats table and a helper that fills one row
var table t = table.new(position.bottom_left, 2, 7, border_width = 1)
cell(r, a, b, bg) =>
    table.cell(t, 0, r, a, text_color = color.white, bgcolor = color.gray,
      text_size = size.normal, text_halign = text.align_left)
    table.cell(t, 1, r, b, text_color = color.white, bgcolor = bg,
      text_size = size.normal)

// 7. The histogram is drawn as a table too: one row per bin, a bar made of
// block characters. A table is painted by the chart itself, so it stays
// readable at any zoom and never falls off the right-hand edge.
var table h = table.new(position.middle_right, 3, 20, border_width = 1)
blocks(count, pk) =>
    w = int(math.round(26.0 * count / math.max(pk, 1)))
    s = ""
    // a Pine for-loop with a start above its end counts DOWNWARDS,
    // so an empty bin would print two blocks instead of none
    if w > 0
        for i = 1 to w
            s += "█"
    s

// 8. Everything below is measured and drawn once, on the last bar
if barstate.islast and array.size(zs) > 100
    n     = array.size(zs)
    zMean = array.avg(zs)
    zSd   = array.stdev(zs)
    // tail counts: how often price really goes beyond 2 and 3 sigma
    beyond2 = 0
    beyond3 = 0
    m3 = 0.0
    m4 = 0.0
    for i = 0 to n - 1
        v = array.get(zs, i)
        beyond2 += math.abs(v) > 2 ? 1 : 0
        beyond3 += math.abs(v) > 3 ? 1 : 0
        m3 += math.pow(v - zMean, 3)
        m4 += math.pow(v - zMean, 4)
    skew = (m3 / n) / math.pow(zSd, 3)
    kurt = (m4 / n) / math.pow(zSd, 4)

    // 9. Bin the z-scores into a histogram
    bins  = math.round(8 / binWidth)
    counts = array.new_int(bins, 0)
    for i = 0 to n - 1
        v = array.get(zs, i)
        b = math.floor((v + 4) / binWidth)
        b := math.max(0, math.min(bins - 1, b))
        array.set(counts, b, array.get(counts, b) + 1)
    peak = array.max(counts)
    // both columns must share one scale, or the comparison is meaningless
    expPeak = 0.0
    for b = 0 to bins - 1
        e0 = -4 + b * binWidth
        expPeak := math.max(expPeak, n * (normCdf(e0 + binWidth) - normCdf(e0)))
    pk = math.max(peak, expPeak)

    // 10. Print the shape: the real one, and the one the textbook predicts
    table.cell(h, 0, 0, "z", text_color = color.white,
      bgcolor = color.new(color.blue, 20), text_size = size.small)
    table.cell(h, 1, 0, "REAL", text_color = color.white,
      bgcolor = color.new(color.blue, 20), text_size = size.small)
    table.cell(h, 2, 0, showNorm ? "NORMAL SAYS" : "", text_color = color.white,
      bgcolor = color.new(color.blue, 20), text_size = size.small)
    for b = 0 to bins - 1
        zLo = -4 + b * binWidth
        zHi = zLo + binWidth
        c   = array.get(counts, b)
        exp = n * (normCdf(zHi) - normCdf(zLo))
        far = math.abs(zLo) >= 2 or math.abs(zHi) >= 2
        r   = bins - b            // biggest z at the top, like a price axis
        table.cell(h, 0, r, str.tostring(zLo, "#0.0"), text_color = color.white,
          bgcolor = color.new(color.gray, 30), text_size = size.tiny)
        table.cell(h, 1, r, blocks(c, pk),
          text_color = far ? color.red : color.teal,
          bgcolor = color.new(color.black, 0), text_size = size.tiny,
          text_halign = text.align_left)
        table.cell(h, 2, r, showNorm ? blocks(int(exp), pk) : "",
          text_color = color.new(color.yellow, 30),
          bgcolor = color.new(color.black, 0), text_size = size.tiny,
          text_halign = text.align_left)

    // 11. The numbers, so nothing has to be taken on trust
    pct2 = 100.0 * beyond2 / n
    pct3 = 100.0 * beyond3 / n
    cell(0, "Z-SCORE LAB", str.tostring(n) + " bars", color.new(color.blue, 20))
    cell(1, "z now", str.tostring(z, "#.00"),
      math.abs(z) > 2 ? color.new(color.red, 20) : color.new(color.teal, 30))
    cell(2, "mean / stdev", str.tostring(zMean, "#.00") + " / " +
      str.tostring(zSd, "#.00"), color.new(color.gray, 20))
    cell(3, "beyond 2 sigma", str.tostring(pct2, "#.0") + "%  vs 4.6% normal",
      pct2 > 4.6 ? color.new(color.red, 20) : color.new(color.teal, 30))
    cell(4, "beyond 3 sigma", str.tostring(pct3, "#.00") + "%  vs 0.27% normal",
      pct3 > 0.27 ? color.new(color.red, 20) : color.new(color.teal, 30))
    cell(5, "skew", str.tostring(skew, "#.00"), color.new(color.gray, 20))
    cell(6, "excess kurtosis", str.tostring(kurt - 3, "#.00"),
      kurt > 3 ? color.new(color.red, 20) : color.new(color.gray, 20))

// 12. Alert when price is statistically stretched
alertcondition(ta.crossover(z, 2), "Stretched up", "{{ticker}} z above 2")
alertcondition(ta.crossunder(z, -2), "Stretched down", "{{ticker}} z below -2")

Overview

On the last bar it stores a rolling sample of z-scores and shows a histogram table plus summary statistics, including mean, standard deviation, skew, kurtosis, and how often price exceeds two and three sigma. Optional normal-distribution counts use an approximation for comparison. Lookback length, sample size, and histogram bin width are adjustable. It does not generate trade signals or exits.

Details

Published by

jayadevrana on GitHub

Draws

On the price chart

Markets

Any symbol

Timeframes

Any timeframe

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

Topics

algorithmic-tradingbacktestingema-crossoverpine-scriptpine-script-v6pinescriptrsitechnical-analysistrading-automationtrading-indicators

Settings

The inputs you can change from the gear icon on the chart. Defaults are the author's.

SettingTypeDefaultVariable
Lookback for mean and stdevint200length
Bars in the histogramint1000sample
Histogram bin width (sigma)float0.5binWidth
Show what a normal distribution predictsbooltrueshowNorm
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

TylerG FVG BB Reversal

by TradersPost
228 stars

This indicator identifies mean-reversion setups by combining Bollinger Bands with Fair Value Gaps (FVGs). It tracks FVGs that print outside the upper or lower Bollinger Bands and counts them toward an alert threshold. Signals trigger upon gap mitigation (by to

Indicator v6 SMC / ICT / price actionVolatility & bands
Updated 3 months ago · None stated GitHub repository of TylerG FVG BB Reversal
46 stars

Leptokurtic Directional Bias is an overlay indicator that measures kurtosis on a chosen source and marks directional bias on the price chart. The source can be a moving average, ATR, volume, close, or a Bollinger-band trend stop. Bias mode switches among RSI m

Indicator v5 Volatility & bandsMean reversion
Updated 8 weeks ago · None stated GitHub repository of Leptokurtic Directional Bias

Commodity Heat Reversal plots score-based mean-reversion reversal markers on the price chart for commodity futures, aimed at the four-hour timeframe. It builds separate long and short heat scores from ATR distance to a mean moving average, ATR expansion, RSI e

Indicator v6 Mean reversionVolatility & bands
Updated 3 days ago · MIT GitHub repository of Commodity Heat Reversal [WavesUnchained]
4 stars

This indicator plots directional trade signals, ATR-based target and stop-loss boxes, and SuperSmoother reversal bands directly on the price chart. It evaluates market conditions across selectable technical strategies, displaying confidence scores alongside an

Indicator v6 Screeners & dashboardsTrend Repaints
Updated 2 days ago · None stated GitHub repository of Elton's Predictions Decision Engine
3 stars

This overlay indicator recreates short-setup conditions and audits their later outcomes on the price chart. It scores optional percent-surge, RSI overbought rollover, Bollinger exhaustion, volume spike, and EMA trend filters, then marks BASE or STRONG signals

Indicator v6 Oscillators & momentumVolatility & bands
Updated 1 year ago · None stated GitHub repository of Hedge Short Setup Outcome Auditor

More Volatility & bands scripts

Related on ForexR

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers