Rolling Hedge Ratio and Spread Z-Score
Time Series & Stat-Arb · Snippet ID: pairs-zscore-signal
What it does. The hedge ratio at each date is the slope of y on x over the trailing window, cov(y, x) / var(x). The spread is the residual of that slope with no intercept, y − βx. Its z-score subtracts a trailing mean and divides by a trailing sample standard deviation, both taken over the same window length. A large absolute z-score means the spread is far from its recent average. That is a description of the series, not a recommendation to trade it.
Approach
β_t = Cov_w(y, x)_t / Var_w(x)_t (pandas sample cov and var, ddof = 1) spread_t = y_t − β_t x_t z_t = (spread_t − mean_w(spread)_t) / std_w(spread)_t β_t is defined once `window` observations exist. z_t needs a further `window` finite spreads, so it starts about 2·window observations in. std = 0 → z is missing, not infinite.
How it sits next to cointegration
- A hedge ratio is only interesting as a mean-reverting spread if the series are cointegrated, or at least if the spread rejects a unit root. See the ADF page.
- The half-life of the spread is the usual way to choose the window that standardises z, rather than picking 60 because it is round.
- The same rolling slope, compiled and re-centred, is
rolling_betain rolling-window-numba.
Pitfalls
- No intercept. The spread is y − βx, not y − a − βx. If the two prices differ by a level, that level sits inside the spread and the z-score spends a long time absorbing it. Engle-Granger includes an intercept.
- Nested windows. z_t standardises a spread that already used a window to estimate β. Early z values are noisier than a z-score of a fixed-β residual.
- Look-ahead is not in the formulas, but it is easy to add. β_t uses prices through t. A position decided from z_t must not trade at the close of t. Lag the position by at least one bar, as on the Polars backtest page.
- Not a signal to trade. A z-score of 2 is two recent standard deviations. It is not an expected return and it is not a suggestion to buy or sell the spread.
Python reference implementation
Requires pandas. Tested before publication; the generator re-executes it on every rebuild. MIT licensed. Download pairs-zscore-signal.py · All snippets
import pandas as pd
def pair_zscore(y: pd.Series, x: pd.Series, window: int = 60) -> pd.DataFrame:
"""Rolling slope of y on x, the residual spread, and its rolling z-score.
Sample covariance and variance (ddof=1), so the ratio matches OLS with an
intercept-free slope. Illustrative only; not a trading signal.
"""
if window < 2:
raise ValueError("window must be at least 2")
y, x = y.align(x, join="inner")
beta = y.rolling(window).cov(x) / x.rolling(window).var()
spread = y - beta * x
sd = spread.rolling(window).std()
z = (spread - spread.rolling(window).mean()) / sd.where(sd > 0)
return pd.DataFrame({"beta": beta, "spread": spread, "zscore": z})
FAQ
Why does the z-score start so late?
The slope needs `window` rows, and the mean and standard deviation of the spread need another `window` finite spreads. With a window of 60 the z-score becomes available about 120 rows in, not 60.
Can I trade the sign of the z-score?
This function only standardises a spread. Whether the spread mean-reverts, whether both legs can be traded, and what it costs are separate questions. The output is not an order.
Summary for AI agents (Markdown mirror)
# Rolling Hedge Ratio and Spread Z-Score
> A rolling OLS hedge ratio between two price series, the spread y − βx, and a rolling z-score of that spread. The z-score uses a second window on an already rolling spread, so it is defined later than β. Illustrative only; not a trading signal.
- Category: Time Series & Stat-Arb
- Canonical URL: https://quantcore-labs.com/code/pairs-zscore-signal/
- Part of: https://quantcore-labs.com/code/
- Python source: https://quantcore-labs.com/code/pairs-zscore-signal/pairs-zscore-signal.py