GARCH(1,1) in R — frequentist (rugarch) and Bayesian (bayesGARCH)¶

The R corner of the trio (from-scratch · PyMC · R)¶

Companion to garch_python.ipynb (from-scratch: MLE + Gauss–Hermite + importance sampling + Metropolis–Hastings + griddy Gibbs) and garch_pymc.ipynb (PyMC/NUTS). Here two standard R packages fit the same GARCH(1,1) on the same S&P 500 returns:

  • rugarch (Ghalanos) — the reference ML toolkit (ugarchspec / ugarchfit), Gaussian and Student-t.
  • bayesGARCH (Ardia) — MCMC for GARCH(1,1)-t via the Nakatsuma (2000) Metropolis-within-Gibbs scheme — the Bayesian-GARCH workhorse.

Frequentist — rugarch (maximum likelihood)¶

In [1]:
suppressMessages({library(rugarch); library(ggplot2)})
d <- read.csv('sp500ret.csv'); d$date <- as.Date(d$date); r <- d$ret
spec <- function(dist) ugarchspec(variance.model=list(model='sGARCH', garchOrder=c(1,1)),
                                  mean.model=list(armaOrder=c(0,0), include.mean=TRUE), distribution.model=dist)
fitN <- ugarchfit(spec('norm'), r, solver='hybrid')
fitT <- ugarchfit(spec('std'),  r, solver='hybrid')
cN <- coef(fitN); cT <- coef(fitT)
cat(sprintf('GARCH-N  omega %.4f  alpha %.3f  beta %.3f  persistence %.4f          AIC %.0f\n',
            cN['omega'], cN['alpha1'], cN['beta1'], cN['alpha1']+cN['beta1'], infocriteria(fitN)[1]*length(r)))
cat(sprintf('GARCH-t  omega %.4f  alpha %.3f  beta %.3f  persistence %.4f  nu %.1f  AIC %.0f\n',
            cT['omega'], cT['alpha1'], cT['beta1'], cT['alpha1']+cT['beta1'], cT['shape'], infocriteria(fitT)[1]*length(r)))
GARCH-N  omega 0.0138  alpha 0.089  beta 0.903  persistence 0.9925          AIC 15087
GARCH-t  omega 0.0061  alpha 0.063  beta 0.934  persistence 0.9970  nu 6.1  AIC 14683
In [2]:
# 1% VaR from GARCH-t (standardized-t quantile) and violation rate + conditional-vol plot
sig <- as.numeric(sigma(fitT)); mu <- cT['mu']; nu <- cT['shape']
VaR <- mu + sig * qdist('std', 0.01, mu=0, sigma=1, shape=nu)
cat(sprintf('1%% VaR violation rate = %.2f%%  (target 1.00%%)\n', 100*mean(r < VaR)))
options(repr.plot.width=10, repr.plot.height=4.2)
df <- data.frame(date=d$date, vol=sig*sqrt(252))
ggplot(df, aes(date, vol)) + geom_line(colour='firebrick', linewidth=.3) +
  labs(title='rugarch GARCH-t conditional volatility (annualized)', x='year', y='annualized vol %') +
  theme_minimal(base_size=13)
1% VaR violation rate = 1.07%  (target 1.00%)
No description has been provided for this image

Bayesian — bayesGARCH (MCMC)¶

The same GARCH(1,1)-t, now with full posteriors instead of point estimates + asymptotic SEs. Stationarity ($\alpha_1+\beta<1$) is imposed via addPriorConditions.

In [3]:
suppressMessages(library(bayesGARCH))
y <- d$ret - mean(d$ret)                                       # demeaned returns
set.seed(1)
mc <- bayesGARCH(y, control=list(n.chain=2, l.chain=2500, refresh=100000,
                                 addPriorConditions=function(p) p[2]+p[3] < 1))
sm <- window(mc, start=1001)                                   # discard 1000 burn-in
ps <- as.matrix(do.call(rbind, sm)); colnames(ps) <- c('alpha0','alpha1','beta','nu')
per <- ps[,'alpha1'] + ps[,'beta']
cat(sprintf('posterior means: omega %.4f  alpha %.3f  beta %.3f  nu %.1f\n',
            mean(ps[,'alpha0']), mean(ps[,'alpha1']), mean(ps[,'beta']), mean(ps[,'nu'])))
cat(sprintf('persistence alpha+beta: mean %.4f  95%% CI [%.4f, %.4f]\n', mean(per), quantile(per,.025), quantile(per,.975)))
cat(sprintf('rugarch ML persistence %.4f, nu %.1f  -> frequentist and Bayesian agree\n', cT['alpha1']+cT['beta1'], cT['shape']))
posterior means: omega 0.0107  alpha 0.076  beta 0.919  nu 6.0
persistence alpha+beta: mean 0.9942  95% CI [0.9873, 0.9994]
rugarch ML persistence 0.9970, nu 6.1  -> frequentist and Bayesian agree
In [4]:
options(repr.plot.width=10, repr.plot.height=4)
par(mfrow=c(1,2))
hist(per, breaks=40, col='steelblue', border='white', main='Posterior: persistence alpha+beta', xlab=expression(alpha+beta))
abline(v=mean(per), col='firebrick', lwd=2)
hist(ps[,'nu'], breaks=40, col='seagreen', border='white', main='Posterior: tail dof nu', xlab=expression(nu))
abline(v=mean(ps[,'nu']), col='firebrick', lwd=2)
par(mfrow=c(1,1))
No description has been provided for this image

Results¶

  • rugarch (ML): $\alpha+\beta\approx0.997$, $\nu\approx6$, Student-t strongly preferred by AIC; the 1% VaR violation rate is ≈1% — matching the Python arch fit to the decimal.
  • bayesGARCH (MCMC): the same GARCH(1,1)-t structure with full posteriors — persistence posterior just below 1 (near-integrated volatility, pinned by the stationarity prior) and $\nu\approx6$.
  • Four engines, one story. rugarch and bayesGARCH here, plus the from-scratch five-method notebook and PyMC, all agree on the GARCH(1,1)-t: near-integrated persistence (~0.99) and $\nu\approx6$, frequentist and Bayesian, Python and R. (bayesGARCH's individual $\omega$ and $\alpha/\beta$ split are pulled a little by its stationarity prior — its $\omega$ sits above the ML engines — but persistence and the tail index line up.)