Factor SV in R (factorstochvol)¶

Multivariate volatility, part 4b: the Bayesian package cross-check¶

This validates the from-scratch NumPyro factor SV with factorstochvol (Kastner) — the reference R implementation of Bayesian factor stochastic volatility, sampled by an efficient interweaving (ASIS) MCMC. Two independent Bayesian samplers — NumPyro (our own) and factorstochvol — fitting the same model on the same data is a strong cross-check. (factorstochvol runs through the Jupyter IR kernel used to execute this notebook; the segfault seen earlier was an artifact of a bare non-interactive Rscript launch, not the package.)

Data: sector ETFs XLF/XLK/XLE/XLV/XLU, etf_returns.csv (2004–2024) — the same series as factorsv_numpyro.ipynb.

In [1]:
.libPaths('C:/Users/user/R/win-library/4.6')
suppressMessages(library(factorstochvol))
d <- read.csv('etf_returns.csv'); dates <- as.Date(d$date)
nm <- c('XLF', 'XLK', 'XLE', 'XLV', 'XLU'); R <- as.matrix(d[, nm]); N <- length(nm)

set.seed(1)
fit <- fsvsample(R, factors = 1, draws = 3000, burnin = 1000, quiet = TRUE, runningstore = 6)

lam <- apply(fit$facload, 1, mean)                         # one-factor loadings, posterior mean
com <- fit$runningstore$com[, 1:N, 1]                      # communality (common-factor variance share), T x N (cols 1:N are the assets)
cormat <- fit$runningstore$cor[, , 1]                      # pairwise conditional correlations, T x (N(N-1)/2)
cat('factorstochvol (1 factor, 3000 draws), sector ETFs:\n')
cat('  loadings (posterior mean):', paste(sprintf('%s %.2f', nm, lam), collapse = '  '), '\n')
cat('  common-factor variance share (time-avg):', paste(sprintf('%s %.2f', nm, colMeans(com)), collapse = '  '), '\n\n')
cat('cross-check -> NumPyro factor SV loadings XLF 1.00 XLK .87 XLE .99 XLV .68 XLU .54\n')
cat('              NumPyro common share XLF .68 XLK .59 XLE .43 XLV .57 XLU .32\n')
cat('-> same structure: XLF/XLK most market-driven, XLU (utilities) most idiosyncratic.\n')
Warning message:
"package 'factorstochvol' was built under R version 4.6.1"
factorstochvol (1 factor, 3000 draws), sector ETFs:
  loadings (posterior mean): XLF 0.87  XLK 0.76  XLE 0.86  XLV 0.59  XLU 0.47 
  common-factor variance share (time-avg): XLF 0.67  XLK 0.59  XLE 0.42  XLV 0.57  XLU 0.32 

cross-check -> NumPyro factor SV loadings XLF 1.00 XLK .87 XLE .99 XLV .68 XLU .54
              NumPyro common share XLF .68 XLK .59 XLE .43 XLV .57 XLU .32
-> same structure: XLF/XLK most market-driven, XLU (utilities) most idiosyncratic.
In [2]:
options(repr.plot.width = 12, repr.plot.height = 4.4)
avgcor <- rowMeans(cormat)                                 # average pairwise conditional correlation over time
avgcor_ma <- filter(avgcor, rep(1/21, 21))
plot(dates, avgcor, type = 'l', col = rgb(.3,.5,.8,.35), lwd = .3, ylim = c(0, 1),
     xlab = '', ylab = 'avg pairwise correlation',
     main = 'Factor SV in R (factorstochvol): factor-driven correlation, sector ETFs')
lines(dates, avgcor_ma, col = 'navy', lwd = 1.3)
for (dt in c('2008-09-15', '2020-03-16')) abline(v = as.Date(dt), col = 'grey55', lty = 3)
legend('topright', c('avg correlation (daily post. mean)', '21-day avg'),
       col = c(rgb(.3,.5,.8,.6), 'navy'), lwd = c(1, 1.4), bty = 'n', cex = .85)
No description has been provided for this image

Results¶

  • factorstochvol reproduces the from-scratch factor SV. An independent Bayesian sampler recovers the same loading structure (XLF/XLK most market-driven, XLU the most idiosyncratic) and the same communality ranking — the share of each ETF's variance explained by the common factor. Exact numbers differ from the NumPyro fit because the two use different factor-scale identifications, but the economic content matches.
  • The factor drives the correlations. The model-implied average pairwise correlation swings widely and spikes at the 2008 and 2020 crises — the "diversification meltdown", here produced by an independent implementation of exactly the mechanism in factorsv_numpyro.ipynb: when the common volatility factor dominates, everything correlates.
  • Two Bayesian engines agree. NumPyro (with the Woodbury likelihood) and factorstochvol (with ASIS interweaving) — very different samplers — give the same picture, the strongest possible cross-check for the capstone model.

References¶

  • Kastner, G., Frühwirth-Schnatter, S. & Lopes, H. F. (2017). Efficient Bayesian inference for multivariate factor stochastic volatility models. J. Computational & Graphical Statistics 26, 905–917.
  • Aguilar, O. & West, M. (2000). Bayesian dynamic factor models and portfolio allocation. JBES 18, 338–357.