BEKK-GARCH — modelling the covariance directly (from scratch)¶
Multivariate volatility, part 3: Engle & Kroner (1995)¶
CCC and DCC both split the covariance into volatilities and a correlation, $H_t=D_tR_tD_t$. BEKK takes the opposite approach — it models the covariance matrix $H_t$ itself, in a quadratic form engineered so that $H_t$ is positive-definite by construction at every $t$: $$H_t = CC' + A'\,\varepsilon_{t-1}\varepsilon_{t-1}'\,A + B'\,H_{t-1}\,B,$$ with $C$ lower-triangular (so $CC'$ is p.d.). Each term is a p.d. matrix or a congruence/outer-product of one, so their sum is always a valid covariance matrix — no constraints on correlations to police, no risk of an indefinite $H_t$. This is BEKK's signature guarantee and the reason it is the natural model when positive-definiteness matters most.
The price is parameters. In the full BEKK, $A$ and $B$ are dense $N\times N$ matrices — $2N^2$ dynamic parameters, which explodes with the number of assets (the "$N^2$ curse"). The diagonal BEKK ($A=\text{diag}(a_1,\dots,a_N)$, $B=\text{diag}(b_1,\dots,b_N)$) tames it, giving the element-wise recursion
$$H_{t}[i,j] = (CC')[i,j] + a_i a_j\,\varepsilon_{t-1,i}\varepsilon_{t-1,j} + b_i b_j\,H_{t-1}[i,j].$$
We fit the diagonal BEKK from scratch (bekk.py) on the same DJIA & NASDAQ returns as DCC, so the two are directly comparable.
Data: equity_returns.csv (yfinance ^DJI, ^IXIC, 1993–2024).
The model and estimation¶
Guaranteed positive-definiteness. Write $H_t=CC'+ (A'\varepsilon_{t-1})(A'\varepsilon_{t-1})'\!/\dots$ — more precisely each term is of the form $M'XM$ with $X\succeq0$, which is itself $\succeq0$; adding the strictly-p.d. $CC'$ makes $H_t\succ0$. No parameter restrictions are needed for validity — the algebraic form does the work. Contrast DCC, where one must keep $R_t$ a correlation matrix by rescaling $Q_t$.
Stationarity (diagonal case): $a_i^2+b_i^2<1$ for each series — the direct analogue of $\alpha+\beta<1$ in univariate GARCH. Note $a_i,b_i$ enter squared, so the news impact is $a_i^2$ and the persistence is $b_i^2$.
Estimation is Gaussian MLE. With $\theta=(c_{11},c_{21},c_{22},a_1,a_2,b_1,b_2)$ the log-likelihood is
$$\ell=-\tfrac12\sum_t\Big[N\log2\pi+\log|H_t|+\varepsilon_t'H_t^{-1}\varepsilon_t\Big],$$
maximised numerically; bekk.py filters $H_t$ forward and evaluates the $2\times2$ determinant/quadratic in closed form for speed.
import numpy as np, pandas as pd, matplotlib.pyplot as plt
import bekk as BK
df = pd.read_csv("equity_returns.csv", parse_dates=["date"], index_col="date")
dates = df.index; R = df.values
fit = BK.bekk_fit(R)
a, b, C, corr, H = fit["a"], fit["b"], fit["C"], fit["corr"], fit["H"]
aic = -2 * fit["loglik"] + 2 * 7
mle_rho = np.load("mle_rho.npy") # DCC path from dcc_python
print("Diagonal BEKK (Engle-Kroner 1995), DJIA-NASDAQ:")
print(" intercept C (lower-tri): [%.3f, 0; %.3f, %.3f]" % (C[0, 0], C[1, 0], C[1, 1]))
print(" a (news): DJI %.3f NASDAQ %.3f -> news impact a^2 = %.3f / %.3f" % (a[0], a[1], a[0] ** 2, a[1] ** 2))
print(" b (persistence): DJI %.3f NASDAQ %.3f -> persist b^2 = %.3f / %.3f" % (b[0], b[1], b[0] ** 2, b[1] ** 2))
print(" loglik %.1f AIC %.0f (7 params) [DCC AIC 37439, CCC AIC 38846]" % (fit["loglik"], aic))
print("\nParameter-count contrast (the N^2 curse), N assets:")
for N in (2, 5, 10, 30):
print(" N=%2d: full BEKK %4d diagonal %3d scalar %3d | DCC %3d"
% (N, 2*N*N + N*(N+1)//2, 2*N + N*(N+1)//2, 2 + N*(N+1)//2, 3*N + N*(N-1)//2 + 2))
print("\nBEKK implied correlation ranges [%.2f, %.2f] mean %.2f (DCC: [%.2f, %.2f] mean %.2f)"
% (corr.min(), corr.max(), corr.mean(), mle_rho.min(), mle_rho.max(), mle_rho.mean()))
print("corr(BEKK rho, DCC rho) = %.3f" % np.corrcoef(corr, mle_rho)[0, 1])
crises = {"dot-com": "2000-03-10", "GFC": "2008-09-15", "COVID": "2020-03-16"}
fig, ax = plt.subplots(1, 2, figsize=(15, 4.6))
cma = pd.Series(corr, index=dates).rolling(21, center=True).mean().values
dma = pd.Series(mle_rho, index=dates).rolling(21, center=True).mean().values
ax[0].plot(dates, cma, color="darkgreen", lw=1.0, label="BEKK (models $H_t$ directly)")
ax[0].plot(dates, dma, color="darkred", lw=1.0, alpha=.8, label="DCC (models $R_t$)")
for lab, dt in crises.items(): ax[0].axvline(pd.Timestamp(dt), color="0.6", lw=.7, ls=":")
ax[0].set_ylim(0.3, 1.0); ax[0].set_ylabel("conditional correlation (21-day avg)"); ax[0].legend(fontsize=8, loc="lower right")
ax[0].set_title("BEKK and DCC recover the same correlation dynamics\n(different mechanisms, same story)")
for l in ax[0].get_xticklabels(): l.set_rotation(20)
ax[1].plot(dates, H[:, 0, 1], color="purple", lw=.4); ax[1].axhline(0, color="0.6", lw=.6)
ax[1].set_ylabel("conditional covariance $h_{12,t}$")
ax[1].set_title("BEKK models the covariance itself\n(guaranteed a valid p.d. $H_t$ every day)")
for l in ax[1].get_xticklabels(): l.set_rotation(20)
plt.tight_layout(); plt.show()
Diagonal BEKK (Engle-Kroner 1995), DJIA-NASDAQ: intercept C (lower-tri): [0.112, 0; 0.105, 0.066] a (news): DJI 0.276 NASDAQ 0.260 -> news impact a^2 = 0.076 / 0.067 b (persistence): DJI 0.956 NASDAQ 0.962 -> persist b^2 = 0.913 / 0.925 loglik -18682.5 AIC 37379 (7 params) [DCC AIC 37439, CCC AIC 38846] Parameter-count contrast (the N^2 curse), N assets: N= 2: full BEKK 11 diagonal 7 scalar 5 | DCC 9 N= 5: full BEKK 65 diagonal 25 scalar 17 | DCC 27 N=10: full BEKK 255 diagonal 75 scalar 57 | DCC 77 N=30: full BEKK 2265 diagonal 525 scalar 467 | DCC 527 BEKK implied correlation ranges [-0.16, 0.99] mean 0.80 (DCC: [-0.00, 0.96] mean 0.78) corr(BEKK rho, DCC rho) = 0.976
Reading the figure¶
- Left — BEKK and DCC agree. The two conditional-correlation paths (21-day averaged) are almost indistinguishable — correlation 0.976 between them — despite arriving there by different routes: DCC through a correlation recursion, BEKK through a covariance recursion. Same economic story (low in the tech divergence, high in every crisis), two different parameterisations.
- Right — BEKK gives the covariance directly. $h_{12,t}$ is a model output, not a byproduct: it spikes with the 2008 and 2020 crises and stays positive throughout, with a guaranteed-valid $H_t$ every day. The per-asset estimates ($a_i^2\approx0.07$ news, $b_i^2\approx0.92$ persistence, $a_i^2+b_i^2\approx0.99$) mirror the univariate GARCHs.
Results — the positive-definite guarantee vs the $N^2$ curse¶
- On two assets, diagonal BEKK actually wins. AIC 37379 beats DCC (37439) and crushes CCC (38846), with only 7 parameters. Modelling the covariance directly, with per-asset news/persistence, is a slightly better description of this bivariate system.
- But it does not scale. The printed table is the whole trade-off: the full BEKK's dynamic parameters grow as $2N^2$ — 11 at $N=2$ but 2265 at $N=30$ — while DCC's correlation dynamics stay at two scalars $(a,b)$ no matter how many assets. Diagonal BEKK ($2N$ dynamic params) is a compromise, but even it is dominated by DCC once $N$ is large. This is exactly why DCC, not BEKK, is the workhorse for large portfolios.
- Where each belongs. BEKK for a handful of assets where the covariance structure and a guaranteed-p.d. $H_t$ are paramount; DCC when $N$ is large. The factor-SV capstone (Part 4) takes a third route — a low-rank factor structure — to get both p.d. covariances and scalability.
References¶
- Engle, R. F. & Kroner, K. F. (1995). Multivariate simultaneous generalized ARCH. Econometric Theory 11, 122–150.
- Bauwens, L., Laurent, S. & Rombouts, J. V. K. (2006). Multivariate GARCH models: a survey. J. Applied Econometrics 21, 79–109.
Next: bekk_R.ipynb — the base-R cross-check.