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.