SigmaField

The brief

A risk desk reports a one-day 99% value at risk (VaR) on a $10,000,000 holding in one stock. Its system fits a normal model to the last 500 trading days. The head of risk has seen losses beat that figure more often than 1 day in 100, and wants a second opinion.

The stock's returns have fat tails, so a normal model and the sample disagree about the worst days. The desk needs both figures and a test of the normal one. It also needs to know how far the historical figure could move with another sample.

Your task

Each function takes returns, a list or array of daily log returns, a confidence level and the position value in dollars. VaR is a positive loss: V(1−eq)V(1 - e^{q}), where VV is the value and qq the return quantile at 1−1 - level.

  1. Write normal_var(returns, level=0.99, value=VALUE). Use q=rˉ+z sq = \bar{r} + z\,s, where rˉ\bar{r} is the mean return, zz is norm.ppf(1 - level) and ss is the sample standard deviation dividing by n−1n - 1.
  2. Write historical_var(returns, level=0.99, value=VALUE), with qq = np.quantile(returns, 1 - level).
  3. Write var_interval(returns, level=0.99, value=VALUE, n_boot=2000, seed=1): a 95% bootstrap percentile interval (low, high) for the historical VaR. Resample the days with replacement n_boot times, with np.random.default_rng(seed).
  4. Write backtest(returns, var, level=0.99, value=VALUE), which returns (exceedances, p_value). exceedances counts the days whose loss V(1−ert)V(1 - e^{r_t}) was larger than var. p_value is the chance of at least that many if the VaR is right.

Every function raises ValueError if level is not strictly between 0 and 1, or value is not positive. It raises it too for a missing return (NaN), and for fewer than 11−level\frac{1}{1 - \text{level}} returns, which leave no day beyond the level. backtest raises it when var is not positive.

The data

RETURNS holds the 500 daily log returns, oldest first, and VALUE the position. The returns are synthetic, drawn with a fixed seed from a fat-tailed distribution and rounded to 0.001%. The worst day lost 6.30%.

Hint: Where to start

Case study 5.3.5 reads both VaRs from a return series: one from the sample's 1% quantile, one from the normal quantile rˉ+zs\bar{r} + zs. Lesson 5.4.3 gives zz from norm.ppf. Use np.expm1(q) for eq−1e^q - 1.

Hint: How sure is the historical figure?

The 1% tail of 500 days rests on five or six of them. Lesson 5.8.2's bootstrap measures what that costs. Draw 500 days with replacement and recompute the VaR, many times; then read the 2.5% and 97.5% quantiles of the results.

Hint: Testing the normal model

If the VaR is right, each day beats it with probability 1−1 - level, so the count is binomial (lesson 5.2.2). Lesson 5.8.3 turns a count into a p-value: P(X≥k)P(X \ge k) is binom.sf(k - 1, n, p).