Branch: Quant-Finance Developer Stack (quant-dev) · Node QD2.02 · ~20 focused hours · Gate: Quiz ≥ 85% + code-pack ≥ 85%
Every desk that runs real money eventually gets asked the same blunt question by someone who controls the capital: how much could we lose? Not "what do we expect to make" (every pitch deck already answers that) but the other half of the sentence nobody puts on a slide. A portfolio manager who cannot answer that question with a specific, defensible number is not running a risk process; they are hoping. This module is where hoping stops being acceptable. You are going to build the actual numeric machinery (in code, run against real seeded data, graded by hidden tests) that a risk desk uses to turn "how much could we lose" into a number, a distribution, and a falsifiable claim about the future that gets checked against what actually happened.
Here is the whole module compressed into one sentence you will spend twenty hours earning the right to believe: a risk number is a testable prediction, not a fact, and the discipline that separates a real risk system from a decorative one is that someone actually goes back and tests it. Value at Risk (VaR) and Expected Shortfall (ES) are the two headline numbers of that prediction. They can be produced three structurally different ways (by replaying history, by assuming a distribution, or by simulating one) and each method is a different bet about what next quarter will look like. None of the three is "the" right answer; each is a different lie that is useful in a different way, and the only way to know which lie is currently hurting you is to keep score. That keeping-score step (VaR backtesting) is not a footnote to this module. It is graded here with the same weight as the VaR calculation itself, because a bank that computes VaR beautifully and never checks whether the breaches matched the promise is not meaningfully safer than a bank that skips VaR altogether.
Advertisement
You already know, from QD1.03, how to build a backtesting engine for a trading strategy's returns, the discipline of running a model against data it has never seen and refusing to trust a number until it has survived contact with reality. This module applies that exact same discipline one level up: instead of backtesting whether a strategy made money, you backtest whether a risk model's own promise held up. A 99% VaR model says "a loss this large should happen about 1% of the time." That is a claim with a expiration date built in, count the actual breaches over the next year, and either the model was honest or it wasn't. Everything else in this module (historical, parametric, and Monte Carlo VaR, Expected Shortfall, stress testing, and scenario analysis) exists to feed that one accountability loop with better and worse candidate numbers, and to give you the tools to know, in code, which candidate to trust.
One promise for the hours ahead: by the end, you will be able to take a raw return series for any book (a rupee-denominated equity desk, a dollar-denominated multi-asset fund, or the synthetic portfolio bundled with this module) and produce, from scratch, in Python, a full risk report: three independently-computed VaR/ES estimates that agree when the world behaves and disagree in a way that tells you something when it doesn't; a formal statistical verdict on whether your VaR model's own promise held over the last year of live use; and a stress and scenario table showing exactly what a handful of named, non-probabilistic "what if" stories would do to the book today. That is not a classroom exercise. It is, functionally, the daily output of a real market-risk function, smaller in scale, identical in mechanism.
Every exercise in this module lives in its code pack, code-packs/QD2.02.json (module id QD2.02, eight exercises QD2.02-e01 through QD2.02-e08), reachable from this node's Code tab. Every one of the eight ships a structured fallback (fill-in-the-blank, choose-the-correct-implementation, or order-the-steps) as its definition of record, so the pack is 100% completable even with zero runtime, live execution in the browser (via Pyodide, with numpy/scipy preloaded) is a bonus, never a requirement to pass the gate.
Learning objectives
By the end you can:
- State what Value at Risk and Expected Shortfall each actually claim (and, just as importantly, what they do not claim) and explain why a single confidence level and a single holding period are inseparable from the number itself.
- Implement historical simulation VaR and ES from a raw return sample with no distributional assumption, using a precisely-specified order-statistic procedure, and explain what it means for this method to have "no model" (and what that costs you).
- Implement parametric (variance-covariance / delta-normal) VaR and ES from a Normal-distribution assumption, using the closed-form quantile and tail-expectation formulas, and state exactly which assumption is doing all the work.
- Implement Monte Carlo VaR and ES by simulating a large seeded sample from an assumed model and reading the tail off the simulated sample, and demonstrate, numerically, that a correct Monte Carlo implementation converges to the parametric answer when the underlying model is the same.
- Compare the three methods on the same data, explain the specific conditions (fat tails, skew, regime change) under which they diverge, and justify which method you would trust for a specific job.
- Implement a full VaR backtest: count exceptions correctly (including the exact-boundary edge case), run the Kupiec proportion-of-failures test as a formal hypothesis test with a chi-square critical value, and classify a model's track record into the Basel traffic-light zones, and explain why too few exceptions is itself evidence against a model, not a reason to relax.
- Implement stress testing (a single named, non-probabilistic shock applied to a real book) and scenario analysis (several named shocks compared side by side), and explain why these exist alongside VaR/ES rather than instead of them.
- Name the two best-known theoretical limitations of VaR, that it is not, in general, a coherent risk measure (it can fail sub-additivity) and that it says nothing about the size of a loss beyond the threshold, and explain why Expected Shortfall was written into modern capital rules (FRTB) specifically to close those gaps.
- (Productivity objective: R10 duality.) Use an AI coding assistant to scaffold the repetitive, boilerplate-heavy parts of a risk pipeline (report formatting, a
pandasroll-forward loop, a plotting harness) after you can already derive and hand-verify every formula yourself, and catch the specific ways an AI-generated risk calculation goes silently wrong (a population-vs-sample variance mix-up, a hardcoded annualization factor, a0 * log(0)crash on a perfect VaR track record, anumpyboolean masquerading as a Python one in a report that a downstream system trusts).
The duality, stated once (R10). As in every module in this program, this node carries two objectives at once. The understanding objective (items 1–8) is what the mastery gate rewards; you earn the pass by deriving the numbers correctly, by hand and in code, and by clearing the code pack's hidden tests on both an India-flavoured and a US-flavoured dataset, not by having an assistant produce a risk report you cannot defend to a risk committee. The productivity objective (item 9) is the payoff you keep afterward: knowing how to point a copilot at risk-systems code without being quietly misled by it. A tool can never buy you a pass on the gate.
Prerequisites & connections
Builds on. QD1.03 (Building a Backtesting Engine) is this module's closest relative in spirit, you already internalised there that a model's number is worthless until it survives contact with data it has never seen, and that look-ahead bias is a silent killer; this module runs that exact discipline against a risk model's own promise instead of a strategy's returns. QD1.01's point-in-time panel discipline underlies every return series you will touch here, a VaR model back-tested on survivorship-biased data will look better than it is, for the same reason a backtested strategy does. From DA1.05 (Statistics in Code) you bring scipy/statsmodels fluency: comfort calling scipy.stats.norm and scipy.stats.chi2 without re-deriving them from scratch. This module's statistical theory is owned entirely by G1's QM branch, and this module builds on it without re-deriving it: QM1.01 (Probability, Distributions, Sampling & Estimation) is where the Normal distribution, its quantile function, and the whole idea of "read a probability off a curve" were properly derived, here you use norm.ppf and norm.pdf as already-understood tools, the way you'd use a calculator's sin button after a trigonometry class, not a mystery box. QM1.02 (Hypothesis Testing) is where the logic of a likelihood-ratio statistic, a chi-square critical value, and the very idea of "reject the null at 95% confidence" were built up from first principles, the Kupiec test in this module is a direct, applied instance of that machinery, not a new statistical idea. QM1.03 introduced Monte Carlo simulation as a concept; here you build a production-grade, seeded, deterministic Monte Carlo VaR engine from that concept. If any of norm.ppf, "chi-square critical value," or "Monte Carlo" feels shaky rather than merely unfamiliar-in-code, that is your cue to revisit the relevant QM node before this one's Practice set, not to push through. DV1.02 (Options, Binomial & BSM + the Greeks) is a soft companion: the parametric method's other name, delta-normal, comes directly from the idea of a linear (delta) approximation to a book's P&L that DV1.02 builds for an options position, you are not required to have taken it, but if you have, you will recognise exactly where "delta-normal" gets its name. FI1.02 (Duration, Convexity & Interest-Rate Risk) is the same kind of soft companion for a bond book: DV01/PVBP is itself a one-line linear risk sensitivity, and this module's parametric VaR is the same idea generalised to a full return distribution.
Feeds forward. QD2.03 (Performance & Attribution Analytics in Code) picks up immediately where this module's honesty ends: having quantified how much you could lose, QD2.03 quantifies how much of what you actually made was skill versus luck. Sharpe, Sortino, and the deflated Sharpe ratio all sit downstream of the same "a single lucky number proves nothing without a distribution behind it" instinct this module installs. QD2.04 (Reproducible Research & Deployment) takes the seeded, deterministic Monte Carlo engine you build in QD2.02-e03 as its running example of why seeds, config, and logging are not bureaucracy but the difference between a number you can defend and one you cannot reproduce. QD2.05, the region's capstone, requires a full risk section (VaR and ES, computed and VaR-backtested) as a non-negotiable, separately-graded line item; everything in this module is direct, load-bearing preparation for that requirement, and the capstone rubric will simply reuse the vocabulary (exceptions, Kupiec, traffic-light zone) this module defines precisely.
Core teaching content
Value at risk
The loss a portfolio should not exceed on a given proportion of days. Quote a VaR figure without its confidence level and you have quoted nothing, it is like reporting a distance without stating the units. A 99% VaR of 2 million tells you nothing about whether the bad 1% of days average a 2.1 million loss or a 200 million loss. Never let it be the only risk number in the room.
3 min read, on its own page
Historical simulation VaR
Historical simulation says: take the return sample you actually have, treat it as your best available picture of "what can happen," sort it, and read the loss threshold straight off the sorted list. It makes no assumption about the shape of the return distribution. The catch is the sample: on 20 days of data, a 99% VaR is read off the single worst day, which is a very noisy estimate of a rare event.
3 min read, on its own page
Parametric VaR
Parametric VaR assumes returns follow a Normal distribution with a mean and standard deviation estimated from data, and reads the loss threshold straight off that distribution's formula, with no sorting and no sample noise. It's also called the variance-covariance or delta-normal method. A parametric VaR is a mathematically exact answer to a question about a world slightly less scary than the one you actually trade in.
3 min read, on its own page
Monte Carlo VaR
Monte Carlo VaR draws a large number of random returns from an assumed model, then reads the loss threshold off that simulated sample the way historical simulation reads it off a real one. Monte Carlo is the method you reach for the moment the assumption you actually believe in stops having a convenient formula. With a seeded random generator the result is repeatable, and in the Normal case it should converge to the parametric answer as the draws grow.
3 min read, on its own page
Historical vs parametric vs Monte Carlo VaR
Historical VaR reads the loss threshold off past returns, while parametric VaR assumes a Normal distribution and calculates the threshold from its formula. Monte Carlo VaR simulates returns from an assumed model and reads the threshold off the simulated sample. On the same book the three should roughly agree when returns look Normal, so a large gap is worth investigating: a historical figure far above the parametric one points to fat tails or skew in the data.
4 min read, on its own page
VaR backtesting
VaR backtesting checks a VaR model against what happened: you count the days on which the loss was worse than the forecast, then test whether that count fits the confidence level. A forecast nobody checks against what happened is just a number on a slide. The Kupiec test rejects a model with too many exceptions, and it can reject one with none at all.
6 min read, on its own page
The Basel traffic-light zones: the same test, a graduated consequence
Kupiec's test gives one binary verdict: reject, or don't. Bank supervisors (this is a real, published regulatory mechanism, not a teaching invention) wanted something with a graduated consequence attached, so the Basel Committee's traffic-light approach classifies the same exception count into green (the model is behaving), yellow (heightened scrutiny, and typically a rising capital multiplier), or red (the model has most likely failed), derived from the same underlying binomial logic as Kupiec, read as a three-way cumulative-probability cut instead of a single likelihood-ratio cutoff.
The mechanism: let p = 1 - confidence and compute cum_prob = P(X <= n_exceptions) where X follows a Binomial(n_obs, p) distribution under the null that the model is correctly calibrated (scipy.stats.binom.cdf). Classify green if cum_prob < 0.95, yellow if 0.95 <= cum_prob < 0.9999, and red otherwise. Apply this to the Basel Committee's own canonical published setup: 250 trading days at 99% confidence (p = 0.01), and it reproduces the exact, famous boundary numbers you may already have half-remembered from finance reading: green for 0–4 exceptions, yellow for 5–9, red for 10 or more, because binom.cdf(4, 250, 0.01) ≈ 89.22% (comfortably under the 95% green ceiling) while binom.cdf(9, 250, 0.01) ≈ 99.97% (still under the 99.99% red floor) and binom.cdf(10, 250, 0.01) ≈ 99.995% (over it).
Here is the one design decision in code pack exercise QD2.02-e06 that the whole exercise exists to teach: 4 and 9 are not a rule to memorize and hardcode, they are the output of the general binomial-CDF method at one specific (n_obs, confidence) pair. Change the observation window to 250 days at 97.5% confidence instead of 99%, and the boundaries move entirely, the green ceiling shifts up to 10 exceptions (binom.cdf(10, 250, 0.025) ≈ 94.85%, still green) with yellow running through 16 and red starting at 17, because a 97.5% VaR model is supposed to be breached roughly two and a half times more often than a 99% one, so a larger raw count is entirely consistent with correct calibration. A solution that special-cased 4/9 (exactly the kind of "passes the one example I was shown, silently wrong everywhere else" defect this whole program trains you to distrust in AI-generated code, and just as capable of appearing in your own code when you reach for a memorized number instead of deriving it) reproduces the textbook table perfectly and then fails the moment the window or confidence changes, which is exactly what the hidden tests on this exercise check for.
Stress testing vs scenario analysis
Stress testing asks what one specific, named shock would do to a portfolio, with no probability attached. Scenario analysis runs several named shocks side by side, so a risk desk sees which ones hurt and which barely register. Both work out the portfolio impact as a weighted sum of position weights and shocked returns. Because neither attaches a probability, a shock doesn't have to be likely to be worth knowing about.
3 min read, on its own page
Delta, gamma and vega
Delta, gamma and vega are risk sensitivities. Delta is the change in a position's value for a one-unit move in the underlying price, and gamma is the change in delta for that same move. Vega is the change in value for a one-point move in implied volatility. A sensitivity is a local, one-factor statement. It is the first term of a Taylor expansion around today's price, and it is exactly right for an infinitesimal move and progressively wrong for a real one.
3 min read, on its own page
Risk limits
A real risk framework attaches a number to each measure and a stated consequence to each breach, and the four kinds of limit are chosen so that they fail in different ways. Measurement without a limit is commentary. A desk that cuts positions proportionally to the size of the breach will cut far too little and breach again the next day.
3 min read, on its own page
Who uses which measure, and why the answers differ
The same five measures appear across the industry with different weights, and the weighting is driven by what each institution's liabilities look like rather than by taste.
| Participant | The primary measure | Why that one | What they still watch |
|---|---|---|---|
| Bank trading desk | Expected shortfall, at 97.5%, under the current capital rules | Capital is set by a regulator against a defined tail, so the reported number is a binding cost | VaR for backtesting, the full Greek set per desk, stress and scenario limits |
| Asset manager | Tracking error against the mandate benchmark | The client's risk is underperforming the benchmark, not losing money in absolute terms | Absolute VaR where the mandate specifies it, position and liquidity limits |
| Pension fund | Surplus at risk, the funded ratio's own volatility | Assets alone are meaningless against a liability that moves with rates | Duration of assets against liabilities, scenario limits, drawdown |
| Insurer | Economic capital under a regulatory solvency standard | Solvency is a one-year ruin probability set in rules, not a trading horizon | Duration and convexity matching, catastrophe scenarios, liquidity |
One pattern runs through the table and it is the thing to carry. Each participant's primary measure is the one their binding external constraint is written in, and everything else on their screen is diagnostic. Ask which number, if it moved, would force somebody to do something, and you have found the measure that institution actually runs on. A learner who can answer that question for an unfamiliar firm has understood risk measurement better than one who can recite five formulas.
The limits of VaR itself: and why Expected Shortfall keeps rising in prominence
Close the loop on the theory with the two criticisms of VaR serious enough that they reshaped actual bank capital regulation, because "how experts think differently" (next section) depends on holding both in mind at once.
First, VaR is not, in general, a coherent risk measure, a technical property with a very unintuitive practical consequence: the VaR of a combined portfolio can be larger than the sum of the VaRs of its individual pieces held separately. Diversification is supposed to reduce risk, not manufacture it, so a risk measure that can violate this ("sub-additivity," in the formal language QM1.01/QM1.02 would use for the underlying probability theory) is a measure you cannot fully trust to rank-order combined risk correctly in every case, a known, published, and mathematically real failure mode, not a hypothetical one. Expected Shortfall, by contrast, is coherent (it never manufactures risk this way) which is one of the two reasons regulators increasingly prefer it.
Second, and this is the point Worked Example and Core-content section both already leaned on repeatedly: VaR is silent about the magnitude of a loss beyond its own threshold, while ES is defined to average exactly that tail. This pair of theoretical advantages is precisely why the post-2008 Fundamental Review of the Trading Book (FRTB) (the current global standard for bank market-risk capital) shifted the primary regulatory capital metric from VaR to a 97.5% Expected Shortfall. That single regulatory fact is the entire justification, in the real world, for why this module makes you implement ES as a first-class citizen alongside VaR in every single method (historical, parametric, Monte Carlo) rather than treating it as VaR's lesser-known cousin. It is not the lesser-known cousin. In current practice, for the banks that matter most, it is the primary number.
Common mistakes & how experts think differently
- "Our VaR model has never been breached, so it's clearly safe." Zero exceptions sounds like the best possible outcome to ordinary intuition. To the Kupiec test it is statistically equally suspicious as too many exceptions, a correctly-calibrated 99% VaR model is supposed to be breached roughly 1% of the time, and a model that is never breached most likely sits too conservatively, quietly costing capital efficiency every day it goes unchallenged. The expert's first reaction to a spotless VaR track record is not relief; it is "let's actually run the backtest," because "too safe" has a real, ongoing cost that a naive eyeball check will never surface.
- "We computed VaR, so we've covered our risk." VaR describes the ordinary case at some stated confidence, by construction, it says nothing about the tail beyond itself. A book can carry an unremarkable VaR number while hiding a catastrophic, low-probability exposure that only Expected Shortfall (or an explicit stress scenario) would reveal. The expert treats a VaR-only risk report as structurally incomplete, the same way a resume with no references is structurally incomplete, regardless of how good the number looks.
- "All three VaR methods should give the same answer, if they don't, something's broken." Agreement across historical, parametric, and Monte Carlo VaR is the expected case only when the underlying assumption (Normality, typically) genuinely fits the data. A real, material divergence between historical and parametric VaR is not a bug to squash, it is usually the fat-tails-and-skew signal the parametric method is structurally blind to. The expert reads a spread between methods as information about model risk, not as a defect to silence by picking whichever number is smallest.
- "A quantile is a quantile, the exact rounding convention for
kdoesn't matter." It does, and it is exactly the kind of silent boundary bug that survives every happy-path test: computingceil((1-confidence) * n)in ordinary floating point can push a clean integer boundary (like1.0) a hair over into the next integer purely from binary representation error, silently changing which observation your VaR reads off. The expert states the exact rounding convention in the contract up front (with the epsilon guard) and never assumes "sort and take a percentile" is a single unambiguous operation. - "The famous Basel 4/9 exception boundary is a fixed rule, I'll just hardcode it." It is not a rule; it is the output of a general method (the cumulative binomial probability of the observed exception count) at one specific, canonical setup (250 days, 99% confidence). Change the observation window or the confidence level even slightly and the boundary moves, sometimes substantially. The expert derives the zone from the actual binomial CDF every time and treats "4 and 9" as a fact to verify against that derivation, not a constant to paste into new code.
- "The parametric formula and the Monte Carlo simulation are basically the same thing, so I only need to test one." They are the same thing only under the shared Normal assumption, and even then a Monte Carlo estimate carries genuine sampling error that a closed-form parametric answer does not, comparing them is precisely how you validate the simulation code, not a redundant step to skip. The expert treats "does my Monte Carlo converge to the known-correct parametric answer" as a mandatory unit test on the simulation itself, every time, not a one-off sanity check performed once and forgotten.
- "A
numpyTrue/Falseis the same as a PythonTrue/False, it's a boolean either way."numpy.True_ is Trueevaluates toFalsein real Python, because the two are different objects entirely, and any downstream code relying on an identity check (is True) silently breaks on the numpy version without ever raising an error. The expert casts explicitly (bool(...)) at the boundary of any statistical library call whose result feeds a report, a database field, or another system, and never assumes a "truthy" value is the same as the correctly-typed boolean it appears to be. - "A stress scenario with one huge shock number is obviously the worst case." Total portfolio impact is a weighted sum across the entire book, not the size of the single loudest number in a shock table. A −60% shock on a position worth 10% of the book can lose less, in aggregate, than a −15% shock spread evenly across everything held. The expert always computes the actual weighted P&L for every scenario before calling one "the worst," and treats an unweighted eyeball scan of a shock table as a specific, named failure mode to avoid.
Worked examples
Worked example 1: Historical VaR and ES by hand (India, ₹)
A rupee-denominated, NIFTY-linked desk hands you 20 days of illustrative daily returns (synthetic teaching data, not a live position: R3): [0.011, -0.006, 0.014, -0.028, 0.007, -0.019, 0.022, -0.041, 0.003, -0.013, 0.016, -0.009, -0.052, 0.018, -0.024, 0.005, -0.017, 0.009, -0.033, 0.012]. Compute the 90% and 95% historical VaR and ES.
Solution. Step 1: sort. Ascending: [-0.052, -0.041, -0.033, -0.028, -0.024, -0.019, -0.017, -0.013, -0.009, -0.006, 0.003, 0.005, 0.007, 0.009, 0.011, 0.012, 0.014, 0.016, 0.018, 0.022]. Step 2: 90% confidence. n = 20, k = ceil(0.10 × 20) = 2. VaR is the 2nd-worst return: 4.10%. ES is the mean of the worst two (-5.2%, -4.1%): 4.65%. Step 3: 95% confidence. k = ceil(0.05 × 20) = 1. VaR is simply the single worst return: 5.20%. Because the tail contains exactly one observation, ES equals VaR exactly at 5.20%, not a coincidence to be suspicious of, but the mechanically correct behaviour of the method when k = 1. Read it. Confidence rose from 90% to 95% and VaR correctly rose too (4.10% → 5.20%), but notice k fell (2 → 1), because higher confidence means a smaller tail probability and therefore fewer qualifying observations, even though the resulting loss threshold is larger. Confusing "higher confidence" with "more tail observations" is a natural but backwards intuition to catch yourself making.
Worked example 2: Parametric VaR and ES across confidence levels (US, $)
A dollar-denominated, S&P-linked book has estimated daily parameters mu = 0.03%, sigma = 1.1%. Compute parametric VaR and ES at 95% and 99% confidence, and compare the ES/VaR ratio.
Solution. Step 1: 95%. alpha = 0.05, z = norm.ppf(0.05) = -1.6449. VaR = -(0.0003 + (-1.6449)(0.011)) = 0.01779 (1.779%). ES = -0.0003 + 0.011 × norm.pdf(-1.6449) / 0.05 = 0.02239 (2.239%). ES/VaR = 1.258. Step 2: 99%. alpha = 0.01, z = norm.ppf(0.01) = -2.3263. VaR = -(0.0003 + (-2.3263)(0.011)) = 0.02529 (2.529%). ES = -0.0003 + 0.011 × norm.pdf(-2.3263) / 0.01 = 0.02902 (2.902%). ES/VaR = 1.147. Read it. Both VaR and ES rose with confidence (1.779%→2.529% and 2.239%→2.902%), as they must, but the ratio between them shrank (1.258 → 1.147). At higher confidence the tail you are averaging over to get ES is thinner and sits closer to its own boundary (VaR), so the two numbers converge, exactly mirroring the k = 1 convergence you saw in Worked Example 1's historical method, via a completely different mechanism (a closed-form integral instead of an order statistic landing on a single observation).
Worked example 3: Monte Carlo convergence to the parametric answer
Using the same style of parameters as Worked Example 2 (mu = 0.02%, sigma = 1.5%, 99% confidence), run 20,000 seeded Normal draws and compare the resulting Monte Carlo VaR/ES to the exact parametric answer.
Solution. Step 1: parametric answer. z = norm.ppf(0.01) = -2.3263. VaR = -(0.0002 + (-2.3263)(0.015)) = 0.03470 (3.470%). ES = -0.0002 + 0.015 × norm.pdf(-2.3263)/0.01 = 0.03978 (3.978%). Step 2. Monte Carlo, seed fixed. Drawing 20,000 values from Normal(0.0002, 0.015) with a seeded generator and reading the tail off the sorted sample the same way historical simulation does gives VaR = 3.473%, ES = 3.966%. Step 3, the gap. Relative difference: |3.473 - 3.470| / 3.470 ≈ 0.11% on VaR, |3.966 - 3.978| / 3.978 ≈ 0.31% on ES, both comfortably inside the sampling noise expected from a 20,000-draw quantile estimator at this confidence level. Read it. This small residual gap is not a bug to chase to zero; it is the honest fingerprint of Monte Carlo sampling error, and it is exactly why the code pack's hidden tests for QD2.02-e03 use a tolerance band (not exact equality) when checking convergence, and exactly why doubling or tripling the draw count, which you can verify yourself, shrinks that gap further without ever eliminating it completely.
Worked example 4: A Kupiec backtest, including the counterintuitive "too few" rejection (US, $)
A dollar-denominated book runs a 95% one-day VaR model for 100 trading days. Ten exceptions occur. Run the Kupiec test at 95% significance, then compare against the hypothetical case of exactly 5 exceptions (matching the model's own claimed 5% rate) and the case of zero exceptions.
Solution. Step 1, the actual case, x = 10. p = 0.05, T = 100, p_hat = 10/100 = 0.10. Plugging into the likelihood-ratio formula gives LR = 4.131. The chi-square critical value at 95% significance with 1 degree of freedom is 3.841. Since 4.131 > 3.841, reject, the model's claimed 5% exception rate does not match its 10% observed rate at a statistically significant level. Step 2, the "textbook-perfect" case, x = 5. p_hat = 5/100 = 0.05, exactly matching the claimed rate. LR ≈ 0.000, as close to a perfect non-rejection as a real backtest produces. Step 3, the zero-exception case. Run the same 250-day, 99%-VaR setup from the Core content with x = 0: LR ≈ 5.025 > 3.841, also reject, despite a spotless track record. Read it. A risk manager who sees "only 10 breaches, not that bad" without running the actual test would miss a real, statistically significant miscalibration; a risk manager who sees "zero breaches, great" without running the test would miss an equally real miscalibration in the opposite direction. The test exists precisely because eyeballing an exception count invites both of these mistakes, and it makes both failure modes visible in the same number.
Worked example 5: Basel traffic-light zones at a non-canonical setup (India, ₹)
A rupee-denominated desk backtests its 97.5% VaR model (not the canonical 99%) over 250 trading days. Determine the traffic-light zone at 10, 11, and 17 exceptions, and contrast the boundaries against the familiar 99%-confidence 4/9 table.
Solution. Step 1, the general method. p = 1 - 0.975 = 0.025. binom.cdf(10, 250, 0.025) ≈ 94.85%, under the 95% green ceiling, so 10 exceptions is green at this setup. binom.cdf(11, 250, 0.025) ≈ 97.53%: over 95%, under 99.99%, so 11 exceptions is yellow. Continuing the cumulative probability out to binom.cdf(16, 250, 0.025) ≈ 99.98% (still yellow) and binom.cdf(17, 250, 0.025) ≈ 99.99% and beyond (red), the full boundary at this setup is green through 10, yellow 11 through 16, red at 17+. Step 2: contrast. At the canonical 99%-confidence, 250-day setup, the boundary sits at 4/9, not 10/16. Read it. A 97.5% VaR model is supposed to be breached roughly two and a half times more often than a 99% model (a claimed rate of 2.5% versus 1%), so a materially larger raw exception count is entirely consistent with correct calibration at this looser confidence level, which is exactly why hardcoding "4 and 9" as a universal rule, rather than deriving the zone from the binomial CDF at whatever (n_obs, confidence) a specific model actually uses, silently misclassifies a perfectly healthy 97.5% VaR model as far riskier than it is.
Worked example 6: Stress and scenario analysis on a mixed India/US book
A five-name book (NILAM (25%), BRPEAK (25%), TARANG (20%), CRSTNE (15%), PIPPAL (15%)) faces two named scenarios: Scenario A shocks four of the five names (NILAM -15%, BRPEAK -22%, TARANG -10%, CRSTNE -18%; PIPPAL unshocked); Scenario B is a broad rally touching all five (+5%, +3%, +6%, +2%, +4% respectively). Compute the portfolio-level impact of each and identify the worse scenario.
Solution. Step 1. Scenario A. 0.25(-0.15) + 0.25(-0.22) + 0.20(-0.10) + 0.15(-0.18) + 0.15(0.0) = -0.0375 - 0.055 - 0.02 - 0.027 + 0 = -0.1395, a 13.95% loss. Step 2. Scenario B. 0.25(0.05) + 0.25(0.03) + 0.20(0.06) + 0.15(0.02) + 0.15(0.04) = 0.0125 + 0.0075 + 0.012 + 0.003 + 0.006 = 0.041, a 4.10% gain. Step 3, the comparison. Scenario A is unambiguously worse; report both numbers, not just the worse one, since the gap between them (roughly 18 percentage points) is itself a statement about how asymmetric this book's exposure is. Read it. PIPPAL's unshocked status in Scenario A was not an oversight in the scenario design to "fix" by inventing a number for it, a scenario is allowed to leave a name untouched, and the exercise's contract treats a ticker absent from a shock dict as exactly that: unshocked, contributing zero, not an error and not a gap to fill with a guess.
Practice set
Work each problem fully before reading its solution. A calculator (or a REPL) is assumed; treat every dataset as synthetic/illustrative unless stated otherwise (R3).
P1 (guided). Historical VaR/ES, small sample. Ten daily returns: [0.02, -0.03, 0.01, -0.07, 0.015, -0.02, 0.005, -0.045, 0.03, -0.01]. Compute the 80% and 90% historical VaR and ES.
Solution. Sorted ascending: [-0.07, -0.045, -0.03, -0.02, -0.01, 0.005, 0.01, 0.015, 0.02, 0.03]. At 80%: k = ceil(0.20 × 10) = 2 → VaR is the 2nd-worst, 4.50%; ES is the mean of the worst two (-7%, -4.5%), 5.75%. At 90%: k = ceil(0.10 × 10) = 1 → VaR is the single worst, 7.00%, and ES coincides with it at 7.00%.
P2. Historical VaR/ES, all-losing sample (edge case). Sixteen returns, all negative: -0.5%, -1.0%, -1.5%, ..., -8.0% (in steps of 0.5%). Compute the 90% and 95% VaR and ES.
Solution. Sorted ascending, the worst is -8.0%, the mildest is -0.5%. At 90%: k = ceil(0.10 × 16) = 2 → VaR = 7.5% (2nd-worst is -7.5%), ES = mean(8.0%, 7.5%) = 7.75%. At 95%: k = ceil(0.05 × 16) = 1 (since 0.05 × 16 = 0.8, ceiling to 1) → VaR = ES = 8.0%. Every return being a loss changes nothing about the mechanics, the method never assumed gains had to be present.
P3. Why floating point matters. Explain, without running code, why math.ceil((1 - 0.95) * 20) can silently return 2 instead of the mathematically intended 1, and state the fix used in this module's code pack.
Solution. 1 - 0.95 is not exactly representable in binary floating point; the actual stored value is a hair above 0.05 (0.050000000000000044), so (1-0.95) * 20 evaluates to 1.0000000000000009, a whisker above the clean integer 1.0. math.ceil on that value returns 2, not 1. The fix is to subtract a tiny epsilon (1e-9) before taking the ceiling: ceil((1-confidence) * n - 1e-9), which absorbs exactly this class of floating-point boundary drift without materially changing any non-boundary case.
P4. Parametric VaR/ES at 90% confidence. mu = 0, sigma = 2%. Using z_{0.10} = -1.2816, compute the parametric VaR and ES.
Solution. VaR = -(0 + (-1.2816)(0.02)) = 0.02563 (2.563%). ES = -0 + 0.02 × norm.pdf(-1.2816) / 0.10. norm.pdf(-1.2816) ≈ 0.1755, so ES ≈ 0.02 × 0.1755 / 0.10 = 0.0351 (3.510%). ES/VaR ≈ 1.369, notably higher than the 99%-confidence ratio (≈1.146) computed elsewhere in this module, confirming the pattern that the ES/VaR ratio shrinks as confidence rises.
P5. Parametric VaR sensitivity to sigma. mu = 0, confidence = 99%. If sigma doubles from 1% to 2%, what happens to VaR? Prove it algebraically, not just numerically.
Solution. VaR(sigma) = -z × sigma (since mu = 0), which is exactly linear in sigma, doubling sigma exactly doubles VaR, with no other change needed to the formula. At sigma = 1%: VaR = 2.326%. At sigma = 2%: VaR = 4.653%, exactly double. This linearity is a direct, checkable consequence of the Normal-distribution assumption and is exactly the property code pack exercise QD2.02-e02's hidden tests verify (scaling sigma by a factor k must scale VaR - (-mu) by exactly k).
P6. Monte Carlo determinism. Two data scientists on different laptops both call monte_carlo_var_es(0.0, 0.02, 0.99, 20000, seed=7). Should their answers match exactly? Why or why not?
Solution. Yes, exactly: provided both use numpy.random.default_rng(seed) (the modern, stream-stable generator) rather than the legacy unseeded global state. default_rng with an explicit integer seed produces the identical sequence of draws for the identical sequence of calls, on any machine, which is precisely why the code pack's hidden tests can grade a Monte Carlo function deterministically at all, without a fixed seed, "the same inputs give the same output" would not hold, and no hidden test could ever pass reliably.
P7. Monte Carlo sample size and convergence. Would you expect a Monte Carlo VaR estimate from 1,000 draws to be closer to, or farther from, the exact parametric answer than one from 100,000 draws (same seed family, same underlying model)? Justify from the nature of a quantile estimator.
Solution. Farther, a quantile estimated from a smaller sample has more sampling noise, exactly like a poll of 1,000 people is noisier than a poll of 100,000. As the draw count grows, the empirical quantile converges toward the true quantile of the underlying distribution (here, the exact Normal quantile the parametric formula computes in closed form), so the gap between the Monte Carlo estimate and the parametric answer should shrink, on average, as the draw count rises, though any single run can still land closer or farther by chance, which is exactly why convergence is checked with a tolerance band across the expected behaviour, not a single run assumed to prove it.
P8. Comparing methods: diagnosis. On the same one-year return sample, historical 99% VaR comes out to 6.2%, while parametric 99% VaR (using the sample's own mean and standard deviation) comes out to 3.8%. What does this gap most likely indicate?
Solution. The historical method is reading a real, fat-tailed or skewed event straight out of the sample (a large realized loss during the year) that the Normal-distribution assumption behind the parametric method smooths away entirely, the classic fingerprint of excess kurtosis / fat tails in real return data that a Normal model cannot represent. The correct response is not to average the two numbers or discard the inconvenient one, but to treat the historical figure as the more honest one for a sample that has already lived through its own worst case, while recognising the parametric figure is telling you what the risk would look like if you believed returns were truly Normal.
P9. Count exceptions with a time-varying VaR forecast. realized = [-0.01, -0.02, 0.03, -0.06, 0.01, -0.025, -0.04, 0.02, -0.015, -0.05], and the VaR forecast is a constant 2% every day. How many exceptions occurred, and which days?
Solution. An exception requires realized[i] < -0.02. Checking each: -0.06 < -0.02 ✓, -0.025 < -0.02 ✓, -0.04 < -0.02 ✓, -0.05 < -0.02 ✓, the other days (-0.01, 0.03, 0.01, 0.02, -0.015) do not breach (note -0.015 is a loss but not worse than the -0.02 threshold). Four exceptions.
P10. The exactly-at-threshold edge case. A day's realized return is exactly -3.00% and that day's VaR forecast was exactly 3.00%. Is this an exception? Explain from the definition of VaR.
Solution. No. VaR is defined as a threshold the loss is not expected to exceed; "exceeded" means strictly worse, and a loss landing exactly on the threshold has met the model's claim, not broken it. The counting rule is realized < -var_forecast (strict inequality), not <=, and a solution using <= would over-count exceptions on any day landing precisely at the boundary, rare with continuous real-valued returns, but a real and specifically-tested edge case in the code pack.
P11. Kupiec test, does the desk pass? 252 trading days, a 95% VaR model, 13 exceptions occurred (the model's own claimed rate would predict about 0.05 × 252 ≈ 12.6). Compute the Kupiec LR statistic and state the verdict at 95% significance (critical value 3.841).
Solution. p = 0.05, T = 252, x = 13, p_hat = 13/252 ≈ 0.0516. Because the observed rate is almost exactly the claimed rate, the likelihood-ratio statistic comes out extremely small: LR ≈ 0.013, far below the 3.841 critical value. Do not reject, the model's calibration is well supported by this year's data.
P12. Kupiec test, a materially miscalibrated desk. Same 252-day, 95% VaR setup, but 25 exceptions occurred instead. Compute the verdict.
Solution. p_hat = 25/252 ≈ 0.0992, roughly double the claimed 5% rate. LR ≈ 10.11, well above 3.841. Reject, this desk's VaR model is significantly under-forecasting its true risk; a 95% VaR that is actually breached nearly 10% of the time is not a minor calibration slip.
P13. Traffic-light zone, standard setup. 250 trading days, 99% VaR, 7 exceptions. State the zone and the two adjacent Basel reference numbers that bracket it.
Solution. Yellow. The canonical 250-day, 99%-confidence boundary runs green through 4 exceptions, yellow from 5 through 9, and red from 10 onward; 7 falls inside the yellow band, between the green ceiling (4) and the red floor (10).
P14. Traffic-light zone, a shorter window. 100 trading days, 99% VaR, 3 exceptions. Using the general binomial-CDF method (do not assume the 250-day boundary applies), state the zone.
Solution. Yellow (borderline). binom.cdf(3, 100, 0.01) ≈ 98.16%, above the 95% green ceiling, below the 99.99% red floor, so the classification is yellow, even though 3 exceptions in 100 days (a 3% observed rate against a claimed 1%) might look mild at first glance; a shorter observation window gives the test less evidence to work with, which shifts its boundaries measurably compared to the familiar 250-day table.
P15. Stress test, unshocked names. Book: {"X": 0.4, "Y": 0.35, "Z": 0.25}. Scenario shocks only X (-25%) and Y (-5%); Z is untouched. Compute the portfolio P&L.
Solution. 0.4(-0.25) + 0.35(-0.05) + 0.25(0.0) = -0.10 - 0.0175 + 0 = -0.1175, an 11.75% loss. Z's absence from the shock dict means it contributes exactly zero, not an error and not a value to guess at.
P16. Scenario analysis, the "loudest number isn't the worst case" trap. Book: {"A": 0.10, "B": 0.90}. Scenario 1 shocks A by -80% and leaves B unshocked. Scenario 2 shocks both A and B by -12%. Which scenario is actually worse for the portfolio, and by how much?
Solution. Scenario 1: 0.10(-0.80) + 0.90(0.0) = -0.08 (an 8% loss). Scenario 2: 0.10(-0.12) + 0.90(-0.12) = 1.00 × (-0.12) = -0.12 (a 12% loss). Scenario 2 is worse, by 4 percentage points, despite containing no single shock anywhere near as dramatic as Scenario 1's −80%. The lesson is exactly the one this module's Core content and Common-mistakes section both name directly: total portfolio impact is a weighted sum across the whole book, and a milder shock spread across a position that is 90% of the portfolio outweighs a dramatic shock concentrated in a 10% sliver.
P17. Where the delta estimate becomes a lie. A long call is worth 6.583084 with delta 0.562903 and gamma 0.0262649, on a spot of 100. Estimate the position's value change for a 20% fall in spot using delta alone, then using delta and gamma together, then compare both against the full reprice of −6.099760. State the error of each and say, in one sentence, what makes the delta-only figure impossible on its face.
Solution. Delta only: 0.562903 × (−20) = −11.258059, an error of −5.158299 against the full reprice. Delta plus gamma: 0.562903 × (−20) + 0.5 × 0.0262649 × 20² = −11.258059 + 5.252972 = −6.005087, an error of +0.094673, roughly fifty times smaller. The delta-only figure is impossible on its face because a long call's maximum loss is the premium paid, 6.583084, and no fall in spot can cost the holder 11.258059 on a position that can be abandoned. Note the direction of the second-order estimate: repeat the exercise at +20% and delta-plus-gamma overshoots by 1.108020, because two terms of an expansion are still an expansion. A sensitivity is a small-move tool, and the honest way to size a 20% move is to reprice.
P18. Cure a VaR breach. A fund with ₹25,00,00,000 of net asset value runs a 2.00% one-day 99% VaR limit. Its equity sleeve is 42% of the book with a standalone VaR of 4.02% of net asset value; the rest of the book carries 1.21%; the two are correlated at 0.35. Compute portfolio VaR, state the breach in rupees and as a percentage of the limit, then size the equity sale that returns the book to exactly the limit. Explain why cutting the sleeve in proportion to the size of the breach would not work.
Solution. Portfolio VaR is sqrt((0.42 × 0.0402)² + 0.0121² + 2 × 0.35 × (0.42 × 0.0402) × 0.0121) = 2.396846% of net asset value, or ₹5,99,21,142 against a ₹5,00,00,000 limit. The breach is ₹99,21,142, which is 19.84% over the limit and 0.3968 percentage points of net asset value. Scaling the equity sleeve by 0.725126 returns portfolio VaR to 2.000000%, taking the sleeve from 42.00% of net asset value to 30.46%, a sale of ₹2,88,61,770. Proportional cutting fails because VaR combines in quadrature rather than additively: the sleeve has to shrink by 11.54 percentage points of the book, roughly 27% of its own value, to remove a 0.40-point overage, since correlation below one means the sleeve's marginal contribution to portfolio VaR is smaller than its standalone number. Cutting 19.84% of the sleeve, matching the size of the breach, leaves the book still over the limit and forces a second sale the next day at a worse price.
Applied mini-project
Build a one-page risk report for a synthetic book.
Using the synthetic-portfolio dataset bundled with this module (or a book of your own construction, clearly labeled synthetic per R3) and the functions you built across QD2.02-e01 through QD2.02-e08, produce a single, self-contained risk report covering:
- All three VaR/ES methods, side by side, at both 95% and 99% confidence, historical, parametric, and Monte Carlo (state your seed). Report the ES/VaR ratio at each confidence level and comment, in one paragraph, on whether the three methods agree, and if they diverge, which of the three diagnoses from this module's "Comparing the three methods" section best explains the gap.
- A full VaR backtest, using either a real rolling window of the dataset's own returns against your own historical VaR forecast, or a constructed illustrative exception count if a genuine rolling backtest is out of scope for the time you have: the raw exception count, the Kupiec LR statistic and verdict at 95% significance, and the Basel traffic-light zone, derived from the binomial CDF, not looked up from the 4/9 table, and explicitly labeled with the
(n_obs, confidence)pair you used. - One stress test and one three-scenario comparison on the same book, reporting the full table (not just the worst case) and naming, in a sentence, which scenario is worst and why the answer is not simply "the one with the biggest single shock number."
- A one-paragraph limitations section, naming at minimum: which distributional assumption your parametric/Monte Carlo numbers depend on, and one concrete way the real book could violate it; and the fact that VaR itself (at whichever confidence you led with) says nothing about the size of a loss beyond its own threshold, the exact reason you also reported ES.
Self-check rubric (score yourself, pass at 8/10 with no zero on any item): 2 points, all three VaR/ES methods correctly computed at both confidence levels, with a real seed stated for Monte Carlo; 2 points, the three-method comparison paragraph correctly diagnoses agreement or divergence rather than merely reporting three numbers; 2 points, the backtest section includes a real Kupiec LR + verdict and a traffic-light zone genuinely derived from the binomial CDF (not the memorized 4/9 pair, unless your setup is in fact 250 days at 99%); 2 points, the stress/scenario section reports the full table and correctly identifies the worst scenario by weighted impact, not by eyeballing the largest single shock; 2 points, the limitations paragraph names a real, specific assumption and explicitly distinguishes VaR's silence about tail magnitude from what ES was computed to answer.
Lab: open the QD2.02 code pack in this node's Code tab (code-packs/QD2.02.json) and clear all eight exercises (QD2.02-e01 through QD2.02-e08), every one ships a structured fallback, so the lab stays fully completable offline, with zero runtime, if the live Python engine is unavailable.
Reading & resources
Everything below is an anchor to build genuine understanding from and verify claims against, never a substitute for deriving the module's own formulas by hand and clearing the code pack. Free unless marked; level tags reflect the ramp from intuition to mechanism.
The core spine (VaR, ES, and the methods):
- Philippe Jorion, Value at Risk: The New Benchmark for Managing Financial Risk, the definitive, industry-standard treatment of historical/parametric/Monte Carlo VaR, backtesting, and the coherence critique; the closest thing this field has to a canonical textbook. [Paid] [Intermediate–Advanced]
- John Hull, Options, Futures, and Other Derivatives, the VaR and Expected Shortfall chapters, plus the broader derivatives context this module's DV1.02 cross-link draws on. [Paid] [Intermediate]
- Yves Hilpisch, Python for Finance: practical, code-first treatment of exactly this kind of quant-finance implementation work in Python. [Paid] [Intermediate]
Backtesting and the regulatory framework (primary sources):
- Paul Kupiec, "Techniques for Verifying the Accuracy of Risk Measurement Models" (1995), the original proportion-of-failures test paper this module implements directly. [Free] [Advanced]
- Basel Committee on Banking Supervision, Supervisory Framework for the Use of Backtesting in Conjunction with the Internal Models Approach, the original published source of the traffic-light zones and their exact cumulative-probability derivation. [Free] [Intermediate–Advanced]
- Basel Committee, Minimum Capital Requirements for Market Risk (the Fundamental Review of the Trading Book, FRTB), the current standard that shifted primary bank capital calculation from VaR to a 97.5% Expected Shortfall; read the rationale section for the coherence and tail-magnitude arguments this module's closing Core section summarizes. [Free] [Advanced]
Anti-overfitting and research discipline (soft companions to QD2.04):
- Marcos López de Prado, Advances in Financial Machine Learning, the backtest-overfitting and research-discipline chapters generalize directly to "don't trust a risk model until it's been genuinely stress-tested against data it hasn't seen." [Paid] [Advanced]
Free, code-first companions:
scipy.statsdocumentation fornormandchi2andbinom, the exact functions this module's code pack calls; read the docstrings forppf,pdf, andcdfdirectly rather than trusting a paraphrase. [Free] [Beginner–Intermediate]- NumPy's own documentation on
numpy.random.Generatoranddefault_rng, the authoritative source on why the modern seeded generator API guarantees reproducible streams where the legacy global state does not. [Free] [Intermediate]
This program's own cross-links (reference, don't duplicate):
- QM1.01 (Probability, Distributions, Sampling & Estimation), where the Normal distribution and its quantile function were derived from first principles.
- QM1.02 (Hypothesis Testing), where the likelihood-ratio statistic, chi-square distribution, and the logic of "reject the null" were built up; the Kupiec test is a direct application, not a new idea.
- QM1.03 (Time-Series, Monte Carlo & Intro ML-for-Finance), where Monte Carlo simulation was first introduced as a concept, ahead of this module's production-grade seeded implementation.
- QD1.03 (Building a Backtesting Engine), the sibling discipline of backtesting a strategy's returns rather than a risk model's promise; the "don't trust an untested number" ethic is shared whole.
Teach it back & journal
Feynman prompt. Explain to a colleague who has heard the term "VaR" at a conference but never computed one why the sentence "our 99% VaR is 2 million dollars" is incomplete without a confidence level and a holding period stated explicitly, and then explain, using the idea that VaR is silent about the tail beyond itself, why you would never sign off on a risk report that quoted VaR without also quoting Expected Shortfall. If your colleague can, unprompted, ask "but what if the model has literally never been breached, is that automatically good?" and you can answer with the Kupiec test's counterintuitive "too few is also rejected" result, you have installed this module's real content, not just its vocabulary.
Journal prompt. Pick one real, public claim you have seen, a bank's disclosed VaR figure in an annual report, a fund's stated risk limit, or a stress-test headline in financial news, and, without needing the underlying data to actually compute anything, write down: what confidence level and holding period is (or is not) explicitly stated; whether the source mentions Expected Shortfall at all, or only VaR; and whether any backtesting result (an exception count, a regulatory zone) is disclosed alongside the risk number, or whether the number is presented as if it needs no checking. Then write one paragraph on this: a risk number without a stated backtest is a promise with no track record attached, which single habit from this module (stating the confidence level and holding period every time, reporting ES alongside VaR every time, or asking "has this actually been backtested" before trusting any risk figure) would most change how a colleague reads your own risk work, and what will you do this week to make it automatic?