CCC-GARCH in R¶
Multivariate volatility, part 1b: the constant-correlation R cross-check¶
This validates the from-scratch CCC (Bollerslev 1990) in R. The dedicated multivariate packages — rmgarch, MTS — segfault in their compiled cores on this Windows/R build, and even repeated rugarch univariate fits are unstable here (the R analogue of PyMC's scan problem). CCC is simple enough to not need them: it is a univariate GARCH per series plus the sample correlation of the standardized residuals. So we implement the two step directly in base R (optim) — which reproduces the Python engine to the digit.
Data: EUR/GBP/JPY/CHF vs USD daily returns, fx_returns.csv (2003–2024) — the same series as ccc_python.ipynb.
In [1]:
# --- univariate GARCH(1,1), Gaussian QMLE, in base R (mirror of ccc.py garch11) ---
garch11 <- function(r) {
r <- r - mean(r); v <- var(r); n <- length(r)
filt <- function(o, a, b) { h <- numeric(n); h[1] <- v
for (t in 2:n) h[t] <- o + a * r[t - 1]^2 + b * h[t - 1]; h }
nll <- function(p) { o <- p[1]; a <- p[2]; b <- p[3]
if (o <= 0 || a < 0 || b < 0 || a + b >= 0.9999) return(1e10)
h <- filt(o, a, b); 0.5 * sum(log(2 * pi * h) + r^2 / h) }
best <- NULL
for (x0 in list(c(0.05 * v, 0.05, 0.90), c(0.10 * v, 0.10, 0.85))) {
op <- optim(x0, nll, method = "Nelder-Mead", control = list(reltol = 1e-10, maxit = 20000))
if (is.null(best) || op$value < best$value) best <- op
}
o <- best$par[1]; a <- best$par[2]; b <- best$par[3]; h <- filt(o, a, b)
list(omega = o, alpha = a, beta = b, persist = a + b, z = r / sqrt(h))
}
d <- read.csv("fx_returns.csv"); dates <- as.Date(d$date)
ccy <- c("eur", "gbp", "jpy", "chf"); R <- as.matrix(d[, ccy]); Tn <- nrow(R); N <- length(ccy)
Z <- matrix(NA_real_, Tn, N)
cat("Univariate GARCH(1,1) per currency (stage 1):\n")
for (i in 1:N) { g <- garch11(R[, i]); Z[, i] <- g$z
cat(sprintf(" %-4s omega %.4f alpha %.4f beta %.4f persist %.3f\n",
toupper(ccy[i]), g$omega, g$alpha, g$beta, g$persist)) }
Rbar <- cor(Z) # stage 2: constant conditional correlation
cat("\nConstant conditional correlation R (stage 2):\n")
dimnames(Rbar) <- list(toupper(ccy), toupper(ccy)); print(round(Rbar, 3))
cat("\ncross-check -> from-scratch Python: EUR-CHF .68, EUR-GBP .62 (European bloc), JPY .23-.43 (identical)\n")
Univariate GARCH(1,1) per currency (stage 1):
EUR omega 0.0013 alpha 0.0461 beta 0.9528 persist 0.999
GBP omega 0.0042 alpha 0.0541 beta 0.9348 persist 0.989
JPY omega 0.0034 alpha 0.0630 beta 0.9325 persist 0.996
CHF omega 0.0026 alpha 0.0423 beta 0.9576 persist 1.000
Constant conditional correlation R (stage 2):
EUR GBP JPY CHF
EUR 1.000 0.624 0.303 0.678
GBP 0.624 1.000 0.234 0.483
JPY 0.303 0.234 1.000 0.426
CHF 0.678 0.483 0.426 1.000
cross-check -> from-scratch Python: EUR-CHF .68, EUR-GBP .62 (European bloc), JPY .23-.43 (identical)
In [2]:
options(repr.plot.width = 12, repr.plot.height = 4.4)
rollcor <- function(x, y, w) { n <- length(x); o <- rep(NA_real_, n)
for (t in w:n) o[t] <- cor(x[(t - w + 1):t], y[(t - w + 1):t]); o }
w <- 126; idx <- setNames(1:N, ccy)
pairs <- list(c("eur", "chf"), c("eur", "jpy"), c("gbp", "chf")); cols <- c("firebrick", "steelblue", "seagreen")
rc <- lapply(pairs, function(p) rollcor(R[, p[1]], R[, p[2]], w))
plot(dates, rc[[1]], type = "n", ylim = c(-0.8, 1), xlab = "", ylab = "126-day rolling correlation",
main = "CCC in R: rolling correlation swings far from the constant CCC value (dashed)")
abline(h = 0, col = "grey70")
for (k in seq_along(pairs)) {
a <- pairs[[k]][1]; b <- pairs[[k]][2]
lines(dates, rc[[k]], col = cols[k], lwd = .6)
abline(h = Rbar[idx[a], idx[b]], col = cols[k], lty = 2, lwd = 1.1)
}
legend("bottomleft", sapply(pairs, function(p) paste(toupper(p), collapse = "-")),
col = cols, lwd = 1.5, bty = "n", cex = .85, ncol = 3)
Results¶
- R reproduces the from-scratch CCC exactly. Same near-integrated univariate GARCHs and the identical constant correlation matrix — the tight European bloc (EUR-CHF 0.68, EUR-GBP 0.62) and the loosely-attached safe-haven JPY (0.23–0.43), the Bollerslev picture.
- The same refutation, independently in R. The 126-day rolling correlations swing dramatically around the constant CCC values (dashed) — EUR-JPY collapsing in 2008, EUR-CHF breaking at the 2015 SNB de-peg — confirming that the constant-correlation assumption fails, and motivating DCC (
dcc_R.ipynb).
Reference¶
- Bollerslev, T. (1990). Modelling the coherence in short-run nominal exchange rates. Review of Economics and Statistics 72, 498–505.