Volatility Clustering in Python
The empirical phenomenon where large market moves tend to be followed by large moves, and calm periods follow calm periods.
Definition
Volatility Clustering is one of the most robust stylized facts in financial econometrics, first formally described by Benoit Mandelbrot in 1963 and later formalized by Robert Engle's ARCH model (1982) and Bollerslev's GARCH model (1986). It describes the empirical observation that return volatility is serially correlated — high-volatility regimes cluster together, as do low-volatility regimes. This violates the classical assumption of i.i.d. (independent and identically distributed) returns that underlies the Black-Scholes model and many naive backtesting frameworks.
Quantitative Formula
This is the GARCH(1,1) model where is the conditional variance at time , is a long-run variance constant, captures the impact of last period's squared shock (ARCH term), and captures the persistence of last period's variance (GARCH term). The constraint ensures mean-reversion. High (typically 0.85–0.95 for equity markets) confirms strong volatility persistence.
Why It Matters in Backtesting
A backtest using constant volatility assumptions (e.g., fixed position sizing based on historical vol) will systematically over-leverage during the transition into high-vol regimes, dramatically amplifying drawdowns. Strategies that adapt position size to current realized volatility — vol-targeting — are far more stable in live markets. Rolling 20-day versus 252-day volatility divergence is a practical, real-time signal for detecting regime transitions in both backtesting and live deployment.
Python Implementation
import numpy as np
import pandas as pd
def analyze_volatility_clustering(returns: pd.Series, short_window: int = 20,
long_window: int = 252) -> dict:
"""
Detects volatility clustering via autocorrelation of squared returns
and computes vol-targeted position sizes.
"""
squared_returns = returns ** 2
# Ljung-Box style: autocorrelation of squared returns (clustering signature)
autocorr_lags = {f"lag_{lag}": squared_returns.autocorr(lag=lag) for lag in [1, 5, 10, 21]}
rolling_short_vol = returns.rolling(short_window).std() * np.sqrt(252)
rolling_long_vol = returns.rolling(long_window).std() * np.sqrt(252)
# Vol-targeting: scale position size to maintain constant risk
target_vol = 0.15 # 15% annualized target
vol_scalar = target_vol / rolling_short_vol.replace(0, np.nan)
vol_regime = pd.cut(
rolling_short_vol,
bins=[0, 0.10, 0.20, 0.35, np.inf],
labels=["Low", "Normal", "Elevated", "Crisis"]
)
return {
"squared_return_autocorrelations": autocorr_lags,
"clustering_detected": autocorr_lags["lag_1"] > 0.1,
"rolling_short_vol": rolling_short_vol,
"rolling_long_vol": rolling_long_vol,
"vol_targeting_scalar": vol_scalar,
"current_regime": str(vol_regime.iloc[-1])
}Test this in a live environment
Stop running Jupyter notebooks locally. Paste this Volatility Clustering code directly into Valetha's Strategy Lab and run a full historical backtest in seconds.
Open the Python Strategy Lab