DCC-GARCH — dynamic conditional correlation (from scratch)¶
Multivariate volatility, part 2: Engle (2002) — the workhorse¶
Part 1 refuted CCC: the conditional correlation between assets is emphatically not constant — it rises in crises and falls in calm. Engle's (2002) Dynamic Conditional Correlation is the fix, and it became the workhorse multivariate volatility model. It keeps CCC's clean split $H_t=D_t R_t D_t$ but lets the correlation matrix evolve: $$Q_t = (1-a-b)\,\bar Q + a\,z_{t-1}z_{t-1}' + b\,Q_{t-1},\qquad R_t=\text{diag}(Q_t)^{-1/2}\,Q_t\,\text{diag}(Q_t)^{-1/2},$$ where $z_t=D_t^{-1}\varepsilon_t$ are the volatility-standardized residuals and $\bar Q$ is their unconditional correlation. This is a scalar GARCH(1,1) on the correlation itself: $a$ is the news impact (how much yesterday's cross-product moves the correlation), $b$ the persistence, and $R_t$ is $Q_t$ rescaled to have unit diagonal so it is always a valid correlation matrix. CCC is the special case $a=b=0$ (then $R_t\equiv\bar Q$), so DCC strictly nests it and we can test one against the other.
Data. Daily log returns (%) on the Dow Jones Industrial Average and the NASDAQ Composite — equity_returns.csv (yfinance ^DJI, ^IXIC, 1993–2024, 8055 days) — the exact bivariate equity setting of Engle's original paper.
The two-step estimator¶
DCC is fit in the same two stages as CCC, adding one step for the correlation dynamics (Engle 2002):
- Univariate GARCH per series (identical to CCC's stage 1) → conditional variances $h_{it}$, standardized residuals $z_{it}=\varepsilon_{it}/\sqrt{h_{it}}$. This is
ccc.garch11, reused. - Correlation intercept $\bar Q = \tfrac1T\sum_t z_tz_t'$ — the unconditional correlation, plugged in by correlation targeting (so only two free parameters remain, exactly as GARCH variance-targets $\omega$).
- DCC parameters $(a,b)$ by maximizing the correlation part of the Gaussian log-likelihood, $$\ell_c(a,b)=-\tfrac12\sum_t\Big[\log|R_t|+z_t'R_t^{-1}z_t\Big],$$ subject to $a,b\ge0,\ a+b<1$ (stationarity of the correlation recursion).
The engine is dcc.py (dcc_fit, pair_corr); it also returns the full-model log-likelihood with $H_t=D_tR_tD_t$ so we can score DCC against CCC by AIC.
import numpy as np, pandas as pd, matplotlib.pyplot as plt
import dcc as D, ccc as C
df = pd.read_csv("equity_returns.csv", parse_dates=["date"], index_col="date") # DJIA & NASDAQ, 1993-2024
names = list(df.columns); dates = df.index; R = df.values
fit = D.dcc_fit(R); a, b = fit["a"], fit["b"]
rho = D.pair_corr(fit["Rt"], 0, 1) # DJIA-NASDAQ conditional correlation
cccfit = C.ccc_fit(R); const = cccfit["Rbar"][0, 1]
kd, kc = 3 * 2 + 1 + 2, 3 * 2 + 1
aic_d = -2 * fit["loglik"] + 2 * kd; aic_c = -2 * cccfit["loglik"] + 2 * kc
print("Stage 1 - univariate GARCH(1,1):")
for nm, f in zip(names, fit["fits"]):
print(" %-7s omega %.4f alpha %.4f beta %.4f persist %.3f" % (nm.upper(), f["omega"], f["alpha"], f["beta"], f["persistence"]))
print("\nStage 2 - DCC correlation dynamics:")
print(" a (news) %.4f" % a)
print(" b (persistence)%.4f" % b)
print(" a + b %.4f (correlation persistence)" % (a + b))
print("\nConditional correlation DJIA-NASDAQ:")
print(" DCC ranges [%.2f, %.2f], mean %.2f vs constant CCC %.2f" % (rho.min(), rho.max(), rho.mean(), const))
print("\nModel comparison (nests CCC at a=b=0):")
print(" CCC loglik %.1f AIC %.0f (%d par)" % (cccfit["loglik"], aic_c, kc))
print(" DCC loglik %.1f AIC %.0f (%d par) -> DCC better by %.0f AIC" % (fit["loglik"], aic_d, kd, aic_c - aic_d))
rho_ma = pd.Series(rho, index=dates).rolling(21, center=True).mean().values
crises = {"dot-com": "2000-03-10", "GFC": "2008-09-15", "COVID": "2020-03-16"}
fig, ax = plt.subplots(1, 2, figsize=(15, 4.6), gridspec_kw=dict(width_ratios=[2, 1.15]))
ax[0].plot(dates, rho, color="firebrick", lw=.35, alpha=.45, label="DCC $\\rho_t$ (daily)")
ax[0].plot(dates, rho_ma, color="darkred", lw=1.3, label="DCC $\\rho_t$ (21-day avg)")
ax[0].axhline(const, color="navy", ls="--", lw=1.2, label="constant CCC %.2f" % const)
for lab, dt in crises.items():
ax[0].axvline(pd.Timestamp(dt), color="0.5", lw=.8, ls=":")
ax[0].text(pd.Timestamp(dt), 0.36, lab, rotation=90, va="bottom", ha="right", fontsize=7, color="0.4")
ax[0].set_ylabel("conditional correlation"); ax[0].set_ylim(0.3, 1.0); ax[0].legend(fontsize=8, loc="lower right")
ax[0].set_title("DCC conditional correlation, DJIA-NASDAQ (Engle 2002)\nlow in the late-1990s tech divergence, high in every crisis")
for l in ax[0].get_xticklabels(): l.set_rotation(20)
for nm, f, col in zip(names, fit["fits"], ["steelblue", "darkorange"]):
ax[1].plot(dates, np.sqrt(252 * f["h"]), color=col, lw=.5, label=nm.upper())
ax[1].set_ylabel("annualized conditional vol (%)"); ax[1].legend(fontsize=8)
ax[1].set_title("Stage-1 conditional volatilities\n(the D_t feeding H_t = D_t R_t D_t)")
for l in ax[1].get_xticklabels(): l.set_rotation(20)
plt.tight_layout(); plt.show()
Stage 1 - univariate GARCH(1,1): DJI omega 0.0199 alpha 0.1136 beta 0.8696 persist 0.983 NASDAQ omega 0.0244 alpha 0.1007 beta 0.8877 persist 0.988 Stage 2 - DCC correlation dynamics: a (news) 0.0532 b (persistence)0.9391 a + b 0.9923 (correlation persistence) Conditional correlation DJIA-NASDAQ: DCC ranges [-0.00, 0.96], mean 0.78 vs constant CCC 0.79 Model comparison (nests CCC at a=b=0): CCC loglik -19415.9 AIC 38846 (7 par) DCC loglik -18710.4 AIC 37439 (9 par) -> DCC better by 1407 AIC
Reading the figure¶
- The correlation is strongly time-varying — exactly what CCC forbids. Left panel: the DCC $\rho_t$ (daily in light red, 21-day average in dark red) ranges from near 0 to 0.96 around the constant CCC value of 0.79.
- Low in the late-1990s tech divergence. Through 1998–2000 the "old economy" (DJIA) and "new economy" (NASDAQ) genuinely decoupled — tech soared while industrials lagged — and DCC captures the correlation falling well below its average. A single constant number would completely miss this.
- High in every crisis. After the dot-com crash the correlation jumps and stays near 0.9 for a decade; the Global Financial Crisis (GFC, 2008) and COVID (2020) pin it near its ceiling. This is the "diversification meltdown" made quantitative — when markets fall together, the covariance matrix knows it.
- $a+b=0.992$ — the correlation is as persistent as volatility itself: shocks to co-movement fade only slowly. Right panel shows the stage-1 conditional volatilities ($D_t$) that multiply $R_t$ into the full covariance $H_t$.
Results — DCC decisively beats its own null¶
- DCC beats CCC by 1407 AIC on the same data, for the cost of just two extra parameters $(a,b)$. Since CCC is exactly DCC at $a=b=0$, this is a clean nested-model verdict: constant correlation is rejected overwhelmingly.
- Two persistent scalars run the whole correlation matrix. DCC's genius is parsimony — no matter how many assets, the dynamics of the entire $R_t$ are governed by the same two numbers $(a,b)$, with $\bar Q$ targeted. That is why it scales to hundreds of assets where BEKK (Part 3) cannot.
- What the two-step QMLE leaves on the table. Stage 1 and stage 2 are estimated separately, and $\bar Q$ is plugged in rather than estimated jointly — so the reported $(a,b)$ carry no honest uncertainty, and the plug-in ignores estimation error in the GARCH stage. This is precisely where the Bayesian treatment earns its keep: a full posterior over $(a,b)$ and the whole correlation path. That is the next notebook.
References¶
- Engle, R. F. (2002). Dynamic conditional correlation: a simple class of multivariate GARCH models. J. Business & Economic Statistics 20, 339–350.
- Aielli, G. P. (2013). Dynamic conditional correlation: on properties and estimation. JBES 31, 282–299 (the cDCC consistency correction).
Next: dcc_numpyro.ipynb — Bayesian DCC, the full posterior over the correlation dynamics.