HAR-RV in PyMC — the Bayesian view¶
The PPL corner of the HAR trio (from-scratch → PyMC → R)¶
HAR is an ordinary linear regression, so there is no scan and no latent state — PyMC's NUTS samples it in seconds. What Bayes adds over the from-scratch OLS is twofold: the full joint posterior of the coefficients (which should reproduce the conjugate result exactly), and a clean model comparison — is the daily/weekly/monthly cascade really justified, or would a single daily lag do? We answer that with an information criterion (LOO) on the two nested models.
import numpy as np, pandas as pd, matplotlib.pyplot as plt
import pymc as pm, arviz as az
df = pd.read_csv("spx_rv_ret.csv", parse_dates=["date"])
l = np.log(df["rv"].values * 1e4) # log realized variance (%^2)
idx = np.arange(22, len(l))
d = l[idx - 1]; w = np.array([l[t - 5:t].mean() for t in idx]); m = np.array([l[t - 22:t].mean() for t in idx])
y = l[idx]; ntr = int(0.6 * len(y)) # same train split as the from-scratch notebook
d, w, m, y = d[:ntr], w[:ntr], m[:ntr], y[:ntr]
with pm.Model() as har: # HAR is a plain linear regression
c = pm.Normal("c", 0, 1)
bd = pm.Normal("bd", 0, 1); bw = pm.Normal("bw", 0, 1); bm = pm.Normal("bm", 0, 1)
sig = pm.HalfNormal("sigma", 1)
pm.Normal("y", mu=c + bd * d + bw * w + bm * m, sigma=sig, observed=y)
idata_har = pm.sample(1500, tune=1000, chains=2, cores=1, target_accept=0.9,
random_seed=1, idata_kwargs={"log_likelihood": True})
print("PyMC HAR posterior (train):")
print(az.summary(idata_har, var_names=["c", "bd", "bw", "bm", "sigma"])[["mean", "sd"]].to_string())
print("cross-check from-scratch OLS: bd 0.255 bw 0.491 bm 0.198")
PyMC HAR posterior (train):
mean sd
c -0.0269 0.0131
bd 0.255 0.0264
bw 0.49 0.044
bm 0.199 0.036
sigma 0.5408 0.0083
cross-check from-scratch OLS: bd 0.255 bw 0.491 bm 0.198
The posterior means land on the OLS estimates (as they must for a linear-Gaussian model with flat priors) — a reassuring end-to-end check. Now the Bayesian value-add: is the cascade justified? Fit the nested daily-only model and compare.
with pm.Model() as ar1: # nested: drop weekly + monthly
c = pm.Normal("c", 0, 1); bd = pm.Normal("bd", 0, 1); sig = pm.HalfNormal("sigma", 1)
pm.Normal("y", mu=c + bd * d, sigma=sig, observed=y)
idata_ar1 = pm.sample(1500, tune=1000, chains=2, cores=1, target_accept=0.9,
random_seed=1, idata_kwargs={"log_likelihood": True})
cmp = az.compare({"HAR (d+w+m)": idata_har, "AR1 (daily only)": idata_ar1})
ecol = [c for c in cmp.columns if c.startswith("elpd") and c != "elpd_diff"][0]
print("Information-criterion comparison (higher elpd = better):")
print(cmp[["rank", ecol, "elpd_diff", "dse"]].to_string())
fig, ax = plt.subplots(1, 2, figsize=(13, 4.6))
post = idata_har.posterior
ols = {"bd": 0.255, "bw": 0.491, "bm": 0.198}
cols = {"bd": "steelblue", "bw": "seagreen", "bm": "firebrick"}
labs = {"bd": "daily $\\beta_d$", "bw": "weekly $\\beta_w$", "bm": "monthly $\\beta_m$"}
for k in ["bd", "bw", "bm"]:
ax[0].hist(post[k].values.ravel(), bins=45, density=True, alpha=.55, color=cols[k], label=labs[k])
ax[0].axvline(ols[k], color=cols[k], ls="--", lw=1.5)
ax[0].set_yticks([]); ax[0].set_xlabel("coefficient"); ax[0].legend(fontsize=9)
ax[0].set_title("PyMC/NUTS HAR posterior\n(dashed = from-scratch OLS — they agree)")
names = list(cmp.index); yb = np.arange(len(names))[::-1]
ax[1].errorbar(cmp[ecol].values, yb, xerr=cmp["se"].values, fmt="o", color="slateblue", capsize=4, ms=8)
ax[1].set_yticks(yb); ax[1].set_yticklabels(names); ax[1].set_ylim(-0.5, len(names) - 0.5)
ax[1].set_xlabel("ELPD (%s) — higher is better" % ecol.split("_")[-1])
ax[1].set_title("The weekly+monthly cascade is justified\n(HAR beats a daily-only AR(1))")
plt.tight_layout(); plt.show()
Information-criterion comparison (higher elpd = better):
rank elpd elpd_diff dse
HAR (d+w+m) 0 -1660.0 0.0 0.0
AR1 (daily only) 1 -1890.0 -230.0 22.0
Results¶
- NUTS reproduces the conjugate posterior. The PyMC coefficient posteriors sit exactly on the from-scratch OLS estimates (dashed lines) — as they must for a linear-Gaussian model with flat priors, but a clean end-to-end confirmation that both engines agree.
- The cascade is justified. The full HAR (daily + weekly + monthly) has an expected log predictive density about 230 higher than a daily-only AR(1) — roughly ten times its standard error. Dropping the weekly and monthly horizons costs real predictive accuracy: the heterogeneous structure earns its keep; it is not decoration.
The from-scratch notebook showed HAR forecasts better than GARCH out of sample; the Bayesian comparison here shows why the HAR form itself is right — each horizon carries genuine information about tomorrow's volatility.
References¶
- Corsi, F. (2009). A simple approximate long-memory model of realized volatility. J. Financial Econometrics.
- Vehtari, A., Gelman, A. & Gabry, J. (2017). Practical Bayesian model evaluation using leave-one-out cross-validation and WAIC. Statistics and Computing.
Companion: har_rv_R.ipynb (highfrequency::HARmodel).