Convert adjusted closing prices to percentage returns, choose a mean and innovation distribution, and fit GARCH(1,1) by maximum likelihood. Interpret as the reaction to the most recent squared surprise, as the weight on the previous variance forecast, and as expected variance persistence. Then check the standardised residuals and evaluate forecasts on observations held out in time order.
GARCH updates a variance forecast in much the same way that a moving average updates a running estimate. It also accounts for what the model already expected. A 2% daily return is a larger surprise during a calm month than during a crisis. Dividing the fitted residual by the fitted conditional volatility shows how large the move was in forecast volatility units.
Read the likelihood and parameters together
For a constant mean , the model is
The optimiser evaluates a conditional likelihood using the innovation distribution for . For Gaussian innovations the contribution, up to a constant, is
A large miss costs more in the likelihood on a date when predicted variance is low, while the log term discourages making every variance forecast large. A standardised Student-t likelihood allows heavier tails. Its fitted degrees of freedom, often printed as
nu, govern tail weight, with smaller values above two giving heavier tails. When , the model has a finite long-run variance of under the usual stationary specification.Suppose a fit in percent units reports , and . The implied long-run variance is in percentage-point-squared units. If today's conditional variance is 1.00 and today's surprise is , the next forecast is , or daily volatility. Those numbers are illustrative. The code estimates its own values from a bundled historical S&P 500 price series.
When returns are entered in percentage points, is in squared percentage points. To report daily volatility, take the square root first. If the output is in percentage-point units, convert it to for a decimal-volatility calculation. To see whether the variance recursion is doing its job, divide the residuals by the fitted conditional standard deviation and check the squared standardised residuals for dependence. A close-to-one persistence estimate can produce a plausible in-sample fit while leaving the long-run variance very sensitive to small parameter changes.
Fit and diagnose the model in Python
import numpy as np
import arch.data.sp500
from arch import arch_model
from statsmodels.stats.diagnostic import acorr_ljungbox
prices = arch.data.sp500.load()["Adj Close"].sort_index()
returns_pct = 100 * prices.pct_change().dropna()
train = returns_pct.iloc[:-252]
fit = arch_model(train, mean="Constant", vol="GARCH", p=1, q=1,
dist="t").fit(disp="off")
print(fit.params)
print("Convergence flag:", fit.convergence_flag)
std_resid = fit.std_resid.dropna()
print(acorr_ljungbox(std_resid**2, lags=[10, 20]))
next_variance = fit.forecast(horizon=1, reindex=False).variance.iloc[-1, 0]
print("First held-out day volatility in percent:", np.sqrt(next_variance))For a full evaluation, roll the forecast origin forward and compare against a constant variance and an EWMA benchmark using the same information cutoff at every date.