Why volatility, not price
Daily price changes are close to unpredictable, but their size is not. Large moves cluster together, and calm periods last. That makes volatility forecastable in a way returns are not, and volatility is what risk limits, position sizing and Value at Risk depend on. zpew forecasts it and does not try to forecast prices.
Models
Every model forecasts the next day's variance, σ2t+1, using returns up to and including day t. Returns are daily log returns, rt.
Realised volatility (the baseline)
The mean of the last n squared returns, with n = 21 as the baseline and n = 63 as a slower variant.
σ2t+1 = (1/n) Σi=0n−1 r2t−i
EWMA
The RiskMetrics exponentially weighted moving average, which weights recent returns more heavily. zpew uses the standard daily value λ = 0.94 and seeds the recursion with the variance of an initial window.
σ2t+1 = λ σ2t + (1 − λ) r2t
GARCH(1,1)
Variance reverts towards a long-run level, ω / (1 − α − β), at a speed the data decides.
σ2t+1 = ω + α r2t + β σ2t, ω > 0, α ≥ 0, β ≥ 0, α + β < 1
The parameters maximise the Gaussian log-likelihood using zpew's own Nelder-Mead optimiser, started from a grid of nine points with the best fit kept. Fits that end on the α + β = 1 boundary are counted and reported. Every fit is cross-checked against the Python arch library.
ℓ(ω, α, β) = −½ Σt [ ln σ2t + r2t / σ2t ]
Out-of-sample evaluation
Each model is estimated on a window of 1,000 daily returns and refitted every 20 trading days, updating its variance daily in between. The window either slides forward or expands from the start, and both are reported. A forecast for day t + 1 never sees data after day t, and a test enforces this by corrupting future data and checking that no past forecast changes.
Proxies for true volatility
True daily volatility cannot be observed, so forecasts are compared with two proxies: the squared return, which is standard but noisy, and the Parkinson range estimator, which uses the day's high and low and is less noisy.
hPt = (ln(Ht / Lt))2 / (4 ln 2)
Loss functions
For a proxy h and a forecast σ̂2, zpew reports the mean squared error and QLIKE. QLIKE ranks forecasts correctly even when the proxy is noisy, and penalises under-prediction more than over-prediction. Days with a zero proxy are left out of QLIKE, where it is undefined.
MSE = (h − σ̂2)2 QLIKE = h / σ̂2 − ln(h / σ̂2) − 1
Each model's losses are expressed as a percentage improvement over the RV21 baseline, then pooled across assets as a mean, a median and the number of assets where the model wins.
Value at Risk backtest
Each variance forecast becomes a one-day Value at Risk under a zero-mean normal assumption, at 95% and 99%. A breach is a day whose return falls below −VaR.
VaRt+1 = zp σ̂t+1, z0.95 ≈ 1.645
If the model is right, breaches should happen on about 5% of days at 95%. The Kupiec proportion-of-failures test asks whether the observed breach rate is consistent with that, using a likelihood ratio with one degree of freedom.
Data
zpew runs on fixed local snapshots of daily prices, each recorded with its source, date range and SHA-256 hash. Snapshot dates, prices and OHLC relationships are validated on load, and gaps longer than the trading calendar allows are reported. Every published result records the commit, compiler, config and snapshot hashes that produced it.
The screenshots on this site use simulated data, generated by a fat-tailed GARCH(1,1) process with a fixed seed, not market prices.
Limitations
- All three models are univariate and assume a symmetric response to good and bad news.
- VaR uses a normal distribution, which understates the fat tails of real returns.
- The proxies are themselves noisy measures of true volatility.
- Differences between models are reported, but their statistical significance is not tested yet.