The Analyst's Path

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 readPart of Risk Systems in Code: VaR, ES, Stress & ScenarioOpen in course mode

Advertisement

The most conceptually honest of the three methods makes literally no claim about the shape of the return distribution. 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.

How is historical simulation VaR calculated?

The exact procedure, and precision matters here, because "sort and take a percentile" hides a rounding decision that two honest engineers can implement two different ways and get two different numbers, is: sort your n returns ascending (worst first), and take k = ceil((1 - confidence) * n) of the worst observations as your "tail." VaR is the k-th worst observation (sign-flipped to a positive loss number); ES is the mean of all k worst observations (also sign-flipped). Floor k at 1, there is always at least one tail scenario, however high the confidence.

Walk it by hand on a small, illustrative 20-day sample for a rupee-denominated, NIFTY-linked desk (every figure below is illustrative, a constructed teaching sample, not a real trading history; R3's synthetic-data honesty applies throughout this module exactly as it does everywhere else in the program):

Advertisement

returns = [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]

Sorted 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]. At 90% confidence, k = ceil(0.10 × 20) = 2: VaR is the 2nd-worst return, 4.10%; ES is the mean of the worst two (-5.2% and -4.1%), 4.65%. At 95% confidence, k = ceil(0.05 × 20) = 1: VaR is simply the single worst observation, 5.20%, and because there is only one observation in the tail, VaR and ES coincide exactly at 5.20%. That coincidence is not a coding bug; it is the correct, honest behaviour of the method when the tail sample is razor-thin, and it is exactly why historical simulation on a short sample is dangerous at very high confidence levels, your "99% VaR" on 20 days of data is really being read off a single worst observation, which is an extremely noisy estimate of a genuinely rare event. This is the entire content of code pack exercise QD2.02-e01, whose hidden tests run this exact procedure against seeded return series on both nse-eod and us-eod flavoured data (R6) and against an independently-written reference implementation, so a solution that hardcodes the visible 20-day answer above cannot pass.

Common mistake: a floating-point trap in the ceiling step

Notice one floating-point trap baked into the "ceil" step itself: computing (1 - 0.95) * 20 in ordinary binary floating point does not give you a clean 1.0, it gives 1.0000000000000009, a hair above the integer boundary, purely from how 0.95 is represented in binary. Feed that directly into math.ceil and you silently get 2 instead of the mathematically intended 1, which is why the code pack's specification calls for subtracting a tiny epsilon (1e-9) before the ceiling. This is not a pedantic detail, it is a live, silent numeric bug of exactly the flavour CS2.06 trained you to hunt for, now showing up in your own quant code rather than someone else's.

Advertisement

Part of the course

This page is part of Risk Systems in Code: VaR, ES, Stress & Scenario (QD2.02), in Phase 12: Finance Plus, AI and the quant-code track. The module's flashcards and its quiz are in the app.