GARCH Models: Forecasting Volatility for Trading
GARCH volatility modeling is the standard parametric approach to forecasting the conditional variance of returns, and the place where the difference between fitting a model and using one becomes obvious. Volatility is forecastable in a way the conditional mean is not — it clusters, persists, and mean-reverts — but a GARCH forecast is only as good as its specification, its error distribution, and your honesty about estimation risk and refit frequency. This article builds the model from the conditional-variance recursion, derives the term-structure forecast, and focuses on the parts that decide whether the forecast helps or hurts: stationarity constraints, leverage asymmetry, distributional choice, and out-of-sample validation.
The conditional variance recursion
Decompose returns as rt = mut + et, et = sigmat * zt, where zt is i.i.d. with mean zero and unit variance and sigmat is the conditional volatility known at t-1. GARCH(1,1) specifies the variance recursion:
sigma2_t = omega + alpha * e2_{t-1} + beta * sigma2_{t-1}
omega > 0is the baseline,alpha >= 0is the reaction to the last squared shock (the ARCH/news term),beta >= 0is the persistence of past variance (the GARCH/memory term).
The genius of Bollerslev's 1986 generalization over Engle's ARCH is parsimony: by letting variance depend on its own lag, GARCH(1,1) reproduces the long memory that would otherwise need many ARCH lags, with three parameters. Substituting the recursion into itself shows GARCH(1,1) is an ARCH(infinity) with geometrically decaying weights alpha beta^(k-1) — that geometric decay is* volatility persistence.
Stationarity, the long-run anchor, and IGARCH
Covariance stationarity of the variance process requires alpha + beta < 1. When it holds, the unconditional (long-run) variance is
sigma2_inf = omega / (1 - alpha - beta)
and the model mean-reverts toward it. For daily equities alpha + beta is typically 0.95-0.99 — variance is extremely persistent, decaying slowly toward sigma2_inf. As the sum approaches 1 the half-life of a variance shock, ln(0.5)/ln(alpha+beta), blows up: at 0.99 it is ~69 days. At exactly 1 you have IGARCH, where shocks never decay, there is no finite long-run variance, and multi-step forecasts grow without mean-reverting — RiskMetrics EWMA is the omega=0, alpha+beta=1 special case. A near-unit persistence estimate is a warning that your sample spans a regime shift or that the model is absorbing a structural break as spurious persistence.
| Parameter | Role | Typical daily equities |
|---|---|---|
| omega | baseline variance | small, positive |
| alpha | reaction to shocks | 0.05-0.10 |
| beta | persistence | 0.85-0.92 |
| alpha + beta | total persistence | 0.95-0.99 |
| half-life | shock decay (days) | 14-70 |
The multi-step forecast has closed form
The h-step-ahead variance forecast mean-reverts geometrically from current conditional variance toward the long-run level:
E[sigma2_{t+h}] = sigma2_inf + (alpha + beta)^(h-1) * (sigma2_{t+1} - sigma2_inf)
This is the single most useful property for trading. If volatility is currently elevated, GARCH forecasts it to decline toward sigma2_inf at rate (alpha+beta); if calm, to rise. Note the term-structure subtlety: the volatility of an h-day return is not sqrt(h) times the one-day forecast, because the conditional variance is changing — you must sum the variance path. This is exactly where naive sqrt-time scaling of a GARCH one-day number goes wrong for option and multi-day risk.
Fitting with the arch library
Kevin Sheppard's arch library is the standard. The two choices that matter most are the error distribution and return scaling.
import numpy as np
import pandas as pd
from arch import arch_model
def fit_garch(returns_decimal, dist="skewt", asym=True):
# Scale to percent: optimizers struggle with tiny decimal variances
r = returns_decimal.dropna() * 100.0
vol = "GARCH" if not asym else "EGARCH" # EGARCH captures leverage
model = arch_model(r, mean="Constant", vol=vol, p=1, o=int(asym), q=1, dist=dist)
res = model.fit(disp="off")
p = res.params
return res
def persistence_and_halflife(res):
a = res.params.get("alpha[1]", 0.0)
b = res.params.get("beta[1]", 0.0)
pers = a + b
hl = np.log(0.5) / np.log(pers) if 0 < pers < 1 else np.inf
return pers, hl
- Fat tails. A Gaussian
z_tunderstates extreme moves even after conditioning
on sigma_t; standardized residuals of real returns remain leptokurtic. Use Student's t or, better, skewed-t — the conditional return distribution is both fat-tailed and asymmetric. This directly affects VaR/CVaR quantiles.
- Scaling. Multiply decimal returns by 100. Feeding
1e-4-scale variances to the
optimizer is the most common cause of convergence failures and bogus parameters.
Leverage: symmetric GARCH is wrong for equities
Plain GARCH treats a +5% and -5% shock identically, but equity volatility rises more after drops (the leverage effect / volatility feedback). Two fixes:
- GJR-GARCH adds an indicator term:
... + gamma e2{t-1} I(e{t-1} < 0), so
negative shocks get extra weight alpha + gamma.
- EGARCH models log-variance (guaranteeing positivity without parameter
constraints) and includes an asymmetric term in standardized shocks.
For equity indices, GJR or EGARCH with a skewed-t error is the sensible default; symmetric GARCH will systematically under-forecast vol after selloffs — exactly when accurate risk estimates matter most.
Validate the forecast, not the fit
A good in-sample log-likelihood says nothing about forecast quality. Volatility is latent, so you evaluate forecasts against a proxy (realized variance) using a loss function robust to the noisy proxy — QLIKE is the standard choice because, unlike MSE, it is robust to the fact that realized variance is an imperfect estimate of the true variance.
def qlike(forecast_var, realized_var):
# Robust to noisy realized-variance proxy; lower is better
rv, f = np.asarray(realized_var), np.asarray(forecast_var)
return np.mean(rv / f - np.log(rv / f) - 1.0)
def walk_forward_garch(returns, train=750, refit_every=21):
r = returns.dropna() * 100.0
preds, realized = [], []
for t in range(train, len(r) - 1):
if (t - train) % refit_every == 0:
res = arch_model(r.iloc[:t], mean="Constant", vol="EGARCH",
p=1, o=1, q=1, dist="skewt").fit(disp="off")
f = res.forecast(horizon=1, reindex=False).variance.iloc[-1, 0]
preds.append(f)
realized.append(r.iloc[t] ** 2) # crude 1-day proxy; use intraday RV if available
return qlike(preds, realized)
Two non-negotiables embedded above: walk-forward evaluation with no look-ahead, and periodic refitting (parameters drift; a once-fit model goes stale). Refit cadence is a trade-off — too frequent adds noise and turnover, too rare lets the model lag regime changes.
Estimation: QMLE, identification, and standard errors
GARCH is estimated by maximizing the (quasi-)likelihood. A useful robustness result: the Gaussian QMLE is consistent for the variance parameters even when the true errors are non-Gaussian, provided the variance equation is correctly specified — the fat tails do not bias alpha and beta, they only make the Gaussian likelihood inefficient and its standard errors wrong. That is why you should either use a fat- tailed likelihood (skewed-t) or, at minimum, robust (Bollerslev-Wooldridge) standard errors; the naive Hessian-based errors understate parameter uncertainty.
Two identification pathologies to watch in the fit output. When alpha is near zero, beta is weakly identified (the model collapses toward constant variance) and its standard error balloons. When alpha + beta is pinned near 1, you are on the IGARCH boundary and the long-run variance estimate is essentially meaningless — small changes in the sample swing it wildly. Always read the parameter standard errors, not just the point estimates; a "persistence of 0.99" with a wide error bar is not a finding, it is the optimizer telling you the data cannot separate persistence from a unit root.
Multivariate GARCH and dynamic correlation
Volatility models extend to the cross section, where the object of interest is the time-varying covariance matrix that drives portfolio risk. Full multivariate GARCH (BEKK, VEC) has too many parameters to estimate beyond a handful of assets, so the practical workhorse is DCC-GARCH (Engle): fit univariate GARCH to each series, standardize, then model the conditional correlation of the standardized residuals with a parsimonious two-parameter recursion. This captures the empirical fact that correlations rise in stress — a dynamic the static sample covariance misses entirely — and feeds a conditional covariance matrix into portfolio optimization and risk parity. The cost is the same as everywhere in this family: more parameters, more estimation risk, and a correlation-targeting assumption that itself breaks in the deepest crises.
Using GARCH in a book
A variance forecast is an input, not a strategy:
- Position sizing. Scale inversely to forecast vol for
volatility targeting and risk-based sizing — size up in forecast-calm regimes, down before forecast-turbulent ones.
- Dynamic VaR/CVaR. Plug the conditional variance and the fitted skewed-t
quantile into VaR / CVaR for a risk estimate that tightens before storms rather than after.
- Vol trading. Compare the GARCH realized-vol forecast to option-implied vol; the
gap is the volatility risk premium — sell when implied is rich relative to forecast, buy when cheap.
- Regime filter. Use elevated forecast vol to throttle or pause strategies that
bleed in turbulence.
Honest limits
GARCH is a parametric, single-regime model of a latent process. Its persistence estimate can be inflated by unmodeled structural breaks, masquerading as long memory. It forecasts the level of variance but not jumps — a GARCH model will be caught flat by a true discontinuity and only ramp up afterward. The one-day forecast is reliable; the multi-day term structure depends entirely on the persistence estimate, which is the least stable parameter. And the whole apparatus assumes the conditional distribution shape is stable, which crises violate. Treat GARCH as a living estimate: use an asymmetric variant with fat-tailed errors, scale your inputs, validate with QLIKE on a walk-forward, refit on a sane cadence, and blend it with model-free realized vol rather than trusting any single number. It is the best cheap forecast of the second moment you will get — and the second moment, unlike the first, is genuinely forecastable. Pair it with disciplined volatility measurement and you have a rigorous risk foundation.
