Stochastic volatility: a Value-at-Risk number you can defend
Difficulty
★★★★☆
Time
4-5 hours
Data
S&P 500 daily closes and log-returns, May 2008 - December 2013
Skills
Designing posterior predictive checks with your own test statistics · heavy-tailed likelihoods · a latent state per observation (random-walk log-volatility) · centred vs non-centred time series · reading low ESS and divergences instead of ignoring them · one-step-ahead forecasts from a state-space model · sequential updating without refitting · back-testing a risk number
You have joined the market-risk desk of a small asset manager. Every evening the desk
publishes a one-day 99% Value-at-Risk (VaR) for its S&P 500 book: the loss that
tomorrow's return should exceed on only 1 day in 100. Capital is held against that number,
and the regulator back-tests it the simple way - by counting the days on which the loss
was worse than the VaR.
The desk's current model is one line: daily returns are Normal with a constant mean and a
constant volatility, estimated from history. The head of risk suspects it is wrong in two
ways at once - "too relaxed the week everything falls apart, and far too scared for years
afterwards" - and asks you for a replacement, plus the evidence that it is better.
You will build the model on data up to 30 June 2011 and be judged on the 2.5 years
that follow, which the model never sees during fitting.
Each task states what to deliver, not how. Write your code in the YOUR CODE HERE cells.
Stuck? h.hint("task3") reveals hints one level at a time: nudge → approach → code skeleton.
Try to get by on nudges.
h.check("task3", vol_of_vol=...) compares your numbers with the reference solution.
A full worked solution lives in notebooks/solutions/. Open it only when you are done (or truly stuck).
importloggingimportarvizasazimportmatplotlib.pyplotaspltimportnumpyasnpimportpandasaspdimportpymcaspmimportpytensor.tensorasptfromscipyimportstatsfrompymc_challengesimportHints,data# PyTensor 3.3 logs a long but harmless "Rewrite failure" traceback when it compiles# pm.GaussianRandomWalk with more than 127 steps. The model is unaffected; keep the log quiet.logging.getLogger("pytensor.graph.rewriting.basic").setLevel(logging.CRITICAL)RANDOM_SEED=2008rng=np.random.default_rng(RANDOM_SEED)az.style.use("arviz-variat")h=Hints("C07")h.tasks()
C07 - Stochastic volatility: a Value-at-Risk number you can defend
task0 - What do returns look like? (3 hints, 2 check(s))
task1 - The desk's model and checks that expose it (3 hints, 2 check(s))
task2 - Fat tails (3 hints, 1 check(s))
task3 - Latent log-volatility and a sampler in trouble (3 hints, 2 check(s))
task4 - Does the volatility path make sense? (3 hints, 2 check(s))
task5 - Tomorrow's VaR (3 hints, 2 check(s))
task6 - Back-test and recommend (3 hints, 2 check(s))
change is the daily log-return of the index. Returns are easier to read (and to put
priors on) in percent, so that is the unit used everywhere below, including in the
checks: ret = 100 * change.
The file runs to 2019. We use only the first 5.5 years: a latent volatility per trading
day makes the model grow with the data, and 800 training days keep each fit to about a
minute. train is all you may fit on; test is for Tasks 5-6 only.
data.describe("sp500")sp500=data.load("sp500")sp500["ret"]=100*sp500["change"]train=sp500.loc[:"2011-06-30"]test=sp500.loc["2011-07-01":"2013-12-31"]print(f"train: {train.index[0].date()} to {train.index[-1].date()}, {len(train)} days")print(f"test: {test.index[0].date()} to {test.index[-1].date()}, {len(test)} days")train.head()
sp500: Daily close and log-return ('change').
source: S&P 500 index daily closes (2008-2019), via pymc-examples.
url: https://raw.githubusercontent.com/pymc-devs/pymc-examples/main/examples/data/SP500.csv
train: 2008-05-02 to 2011-06-30, 798 days
test: 2011-07-01 to 2013-12-31, 629 days
Deliver (on train) the evidence for or against each half of the desk's assumption:
Normal: compare the distribution of returns with a Normal of the same mean and standard
deviation, in a way that shows the tails. Report the excess kurtosis.
Constant volatility: plot the autocorrelation function of the returns and of their
absolute values, side by side. What does each say?
Your turn Work this task in the notebook.
#Task 1 · The desk's model, and checks that expose it
Fit the desk's model: $r_t \sim \text{Normal}(\mu, \sigma)$, both constant.
Deliver
The fit, with priors that make sense for daily returns in percent.
A posterior predictive check built on test statistics you choose yourself - at
least three, each aimed at something a VaR number depends on. For each one: the
distribution of the statistic over replicated datasets, the observed value, and the
posterior predictive p-value $P(T(y^\text{rep}) \ge T(y))$.
One sentence per statistic: what does the model get wrong, and would a density overlay
(az.plot_ppc_dist) have shown it?
The textbook fix for fat tails is a Student-t likelihood. Keep $\mu$ and the scale constant.
Deliver the fit, the same predictive checks as in Task 1, and a verdict: which
failures are fixed, which are not, and is any statistic now wrong in the other direction?
Explain what the posterior of the degrees of freedom $\nu$ is telling you.
Model the log-volatility as a latent random walk: each day has its own volatility,
tomorrow's is today's plus a small shock, and returns are Student-t around $\mu$ with that
day's scale. That is one latent variable per trading day - about 800 parameters.
Deliver
The model, with a prior on the random walk's innovation scale (the "vol of vol") that
you can defend: how much can volatility plausibly change in one day?
Your first fit and an honest reading of its diagnostics. Do not stop at "no
divergences": check r_hat and ESS of every parameter, and find which one is in
trouble.
A diagnosis (why is this posterior hard to sample?), a fix, and a final fit with clean
diagnostics. If a fix trades one problem for another, show that too, and show that your
final answer did not change with the sampler settings.
Budget: a fit takes about a minute. If you need more than three or four, think rather
than wait.
A plot of the posterior volatility path $e^{v_t}$ (mean and a credible band) over the
training period, on top of the absolute returns, with the dates you would expect to
matter marked - at least the Lehman bankruptcy (15 Sep 2008) and the "flash crash"
(6 May 2010). Report the posterior mean of $e^{v_t}$ on its peak day and on the last
training day.
The predictive checks of Task 1 for this model. Then think about what that check does
and does not prove here, given that the replications reuse the fitted path, and add a
check that does not have this weakness.
It is the evening of 30 June 2011, the last training day.
Deliver the one-day-ahead 99% VaR for 1 July 2011 - the 1% quantile of the posterior
predictive distribution of tomorrow's return, in percent (a negative number) - under the
desk's model and under yours. Tomorrow's volatility is not in your posterior: get it there.
Propagate all the uncertainty (parameters, today's volatility, tomorrow's shock).
Now the verdict. For every day in test you need the VaR that would have been
published the evening before, using only returns up to that evening.
Deliver
The daily VaR series of your model and of the desk's model over the test period, in
one plot with the realised returns, exceedances marked.
The number of exceedances of each against the expected 1%, when they happened, and
the average VaR (a proxy for the capital the desk must hold).
A recommendation to the head of risk in a short paragraph - including what your model
still gets wrong.
Refitting an 800-parameter model 629 times is not an option, and fitting once on
train + test would let each day's volatility estimate peek at the future. Find a way to
update your belief about today's volatility day by day that is cheap and does not
cheat, and state clearly which shortcut you are taking.
Leverage. Let the shock to tomorrow's volatility be negatively correlated with
today's return, or give the returns a skewed distribution. Does the lower-tail coverage
in the back-test improve, and what happens to the upper tail?
Mean reversion. Replace the random walk by an AR(1) log-volatility,
$v_t = \bar v + \phi\,(v_{t-1} - \bar v) + \sigma_v \epsilon_t$. How well is $\phi$
identified, and does it change the 10-day-ahead VaR more than the 1-day-ahead one?
Check the shortcut. Refit the model on all data up to the end of 2012 and compare
its volatility on the last day, and its parameters, with what the particle filter
carried forward from June 2011.
Expected shortfall. Regulators have moved from VaR to the expected loss beyond the
VaR. You have the predictive draws: compute it, and think about how you would back-test it.
h.progress()
C07: 0/21 hints used, 13/13 checks passed.
Reference solution
A complete, executed solution with commentary on every task — including the ways the obvious model fails. Try the hint ladder first.