◆ New — Strategy Validator, Code Fixer & Prop Compliance Checker are live. Try one free →
Home / Indicators / Pairs Trading Spread & Z-Score
Statistics indicator

Pairs Trading Spread & Z-Score

A statistical construction that measures the gap between two related instruments and expresses it in standard deviations.

Illustrative diagram — not live market data.

What it is

A pairs-trading spread is not a single built-in indicator but a statistical construction: you take two instruments that have historically moved together, estimate a hedge ratio that scales one against the other, and plot the difference between them. The idea, popularized by statistical-arbitrage desks in the 1980s, is that the spread between two related instruments may be more stable and more mean-reverting than either price on its own. The z-score is the second half of the construction: it normalizes the spread by its own rolling mean and standard deviation, so a reading of +2 means the spread is two standard deviations above its recent average. To build one in Pine Script you need three ingredients: a second symbol fetched safely with request.security, a rolling hedge ratio (a least-squares slope, computable from correlation and standard deviations), and a rolling z-score of the resulting spread. All of this is descriptive statistics about the recent past. A spread that has mean-reverted historically carries no guarantee it will continue to — relationships between instruments break, sometimes abruptly. This page is educational only, not financial advice, and any trading based on spreads carries real risk of loss.

How it works

Start with two price series: the chart symbol (call it Y) and a second symbol (X) retrieved with request.security. To avoid repainting, the second series should be requested with a one-bar offset and lookahead disabled, which means the spread is built from the other symbol's last completed bar. Next, estimate the hedge ratio (beta) — how many units of X offset one unit of Y. A rolling ordinary-least-squares slope can be computed directly on the chart as correlation(Y, X) × stdev(Y) / stdev(X) over a chosen lookback. The spread is then Y − beta × X. Finally, the z-score standardizes the spread: subtract its rolling mean and divide by its rolling standard deviation over a second lookback window. The result oscillates around zero, with ±2 commonly drawn as reference lines. Every stage involves a lookback choice, and each choice changes the output: a short hedge-ratio window makes beta jumpy; a long one makes it stale when the relationship shifts. Critically, the whole construction assumes the two instruments are cointegrated — that the spread is genuinely stationary. That property is estimated in-sample and routinely breaks out-of-sample, which is the honest core caveat of all pairs analysis: statistical correctness of the calculation says nothing about future stability of the relationship.

How traders read it

Common settings

There are no universal defaults because this is a construction, not a packaged indicator. Common starting points are a hedge-ratio lookback of 100–250 bars and a z-score lookback of 20–100 bars, with reference lines at ±2. Some practitioners fix beta at 1 (a simple price difference or ratio) for closely related instruments; others re-estimate it on a rolling basis. Longer windows give steadier estimates that adapt slowly; shorter windows adapt fast but are noisy. Every choice should be tested for sensitivity rather than assumed.

Strengths

Pitfalls to watch

Pine v6 example

//@version=6
indicator("Pairs Spread Z-Score", overlay = false)

sym      = input.symbol("OANDA:EURGBP", "Second Symbol")
betaLen  = input.int(100, "Hedge Ratio Lookback", minval = 20)
zLen     = input.int(50, "Z-Score Lookback", minval = 10)

// Non-repainting fetch: prior completed bar, lookahead off
x = request.security(sym, timeframe.period, close[1], lookahead = barmerge.lookahead_off)
y = close

// Rolling OLS hedge ratio: beta = corr(y,x) * stdev(y) / stdev(x)
beta = ta.correlation(y, x, betaLen) * ta.stdev(y, betaLen) / ta.stdev(x, betaLen)

spread = y - beta * x
meanS  = ta.sma(spread, zLen)
stdS   = ta.stdev(spread, zLen)
z      = stdS != 0 ? (spread - meanS) / stdS : 0.0

plot(z, "Spread Z-Score", color = color.blue)
hline(2,  "+2", color = color.gray)
hline(0,  "Zero", color = color.new(color.gray, 50))
hline(-2, "-2", color = color.gray)

Pro tip: Before reading any z-score, plot beta itself and check whether it is stable over your sample. If the hedge ratio drifts materially, the spread is not measuring what you think it is, and the z-score inherits that error. Re-test the construction across different lookbacks and time periods rather than trusting one clean-looking chart — an in-sample relationship is a description of the past, not a forecast. This is educational material only, not financial advice; pairs relationships can and do break, and trading involves risk of loss.

Built an indicator from this? Run it through the Validator to catch look-ahead bias and repainting, or convert a strategy to Pine Script.

Educational only — not financial advice, not a recommendation to trade. No indicator is predictive; trading involves substantial risk of loss.

Catch the bug that compiles.Run auditGet Pro