Tail Risk Backtesting: When the Test Improves and the Model Does Not
I changed 3 things about my own backtest at once, the expected-shortfall diagnostic improved, and my changelog credited the model. Running the experiment that separates them shows the reference distribution did about 11 times the work. Re-scored against 6 nulls with the conditional ES statistic held fixed, the same 5 models go from 0 of 10 model-and-level cells passing to 10 of 10.
Version history
- v0.1 · 2026-08-26 · first draft, from the de-confounding factorial and null sensitivity runs of 2026-08-25
- v1.0 · 2026-09-02 · first public release. Content unchanged from 0.1; the version marks the paper as issued rather than circulating for review.
- v1.1 · 2026-09-28 · corrections after external review. The ES statistic was misnamed: what 1.0 called Z2 is the conditional statistic Acerbi and Székely call Z1, and it is renamed throughout; every number is unchanged. § 06 no longer attributes Z1 missing an excess breach rate to sample size; Z1 cannot see breach frequency by design, and the paired count test caught it. § 04 and Fig. 02 add a re-run of the six-null sweep with the unconditional Z2 and 2 known-broken models, and the headline is narrowed to the conditional statistic. § 01 distinguishes the ES zones from the Basel traffic light, Recipe 6.1 question 3 names the kind of breakage, the section map in the introduction is corrected, and § 06 and the closing panel link the scoreboard and its methodology manual, now at kwantil.com/var-backtest.
- v1.2 · 2026-11-12 · correction: the exceedance counts across the 10 model-and-level cells run from 17 to 79, not 24 to 79; the GARCH model has 17 at the 99% level. One of the 5 models, the joint VaR and ES variant, was also corrected in the code after an integrity check found it stating an expected shortfall shallower than its own VaR; its ES statistics changed, its zone is the same in every cell of the sweep, and no count or figure in this paper moved.
A validation metric can improve because the model improved or because the reference it is graded against moved, and nothing in the improvement itself distinguishes the two. This audits a case where it was the second, using the conditional Acerbi-Székely statistic (their Z1). Three results. First, a factorial separating a copula change from a reference change shows the copula moved the statistic by +0.0096 while the reference moved the acceptance threshold by -0.104, so the test did roughly 11 times the work of the model; against the original reference the improved model still fails. Second, re-scoring 5 methods at 2 alpha levels against 6 references, with every statistic held fixed, moves Z1 from 0 of 10 model-and-level cells passing to 10 of 10. Re-run with the unconditional Z2 on the same simulated paths, the threshold moves as much but the verdicts do not: the same 6 of 10 pass under every parametric reference. Third, Z1 cannot see an excess number of breaches by construction, and the exceedance-count test it is designed to be paired with caught the baseline that breaches at twice its stated rate. What Z1 has left to detect is tail depth, and at 17 to 79 exceedances it detects that only weakly.
- Calibration background/calibration · v1.1 · tutorial
I have been building a backtest of 5 tail-risk models. In May I changed 3 things about it at once and the expected-shortfall diagnostic improved sharply. My development log recorded that the change to the model had done the work.
That attribution was wrong. The factorial that shows it took 2 runs and had been sitting there the whole time.
Separating the changes shows the improvement came almost entirely from the reference distribution I was grading against, not from the model I had credited. The statistic barely moved. The bar moved.
None of this is exotic. The test is standard and the null is a reasonable choice. The trap is available to anyone who has ever improved a metric and assumed the thing being measured improved with it. I am writing it up because a moved threshold leaves no trace in the statistic, and because the check that catches it costs 2 runs.
The argument runs in the order the evidence forced: what the factorial separates (§ 02 and § 03), how far the verdict moves when only the reference changes (§ 04), why the construction rewards claiming fat tails (§ 05), and what the test can detect at all (§ 06). § 05 includes the part where I had already tried to correct one piece of this and got the direction backwards, and § 06 is what I changed as a result.
A Test That Grades Its Own Reference
Expected shortfall is hard to test, and the standard answer simulates from the model being tested. That is correct; it is also where the trouble starts.
A VaR model promises that losses will exceed a stated level on 1% of days. Counting exceedances tests that promise directly, and 2,172 trading days of counting is a reasonable amount of evidence. Expected shortfall is harder. It asks how bad the bad days were, and there are only about 24 of them at α = 0.01, so any statistic is an average over a couple of dozen numbers.
The Acerbi-Székely conditional statistic, which they call Z₁, takes the realised loss on each exceedance day, divides by the expected shortfall the model predicted that morning, averages over the exceedance days, and adds 1. A correctly specified model gives 0 on average. Negative means realised losses ran deeper than predicted.
The same paper defines an unconditional Z₂, which divides the same sum by the number of exceedances the model expected (Tα) instead of the number that happened. The difference matters later in this article. Z₁ averages over the exceedances themselves, so it cannot see whether there were too many of them, and Acerbi and Székely pair it with an exceedance-count test for that reason. Z₂ responds to both the number and the depth of the exceedances. § 02 to § 05 are about Z₁; § 04 also re-runs the sweep with Z₂.
1Version 1.0 of this paper, and the code it ran on, called the conditional statistic Z₂. The numbers were right and the name was wrong. The changelog records the correction.Here is the subtle part: the question is what counts as too negative, and there is no table to look it up in. The standard construction simulates the answer: draw many alternative loss paths from the model’s own predicted distribution, compute Z₁ for each, and take the 5th percentile as the green threshold.
The zones in this article are defined as follows: green when the statistic is above the 5th percentile of those simulated values, red when it is below the 0.1th percentile, and yellow between the two. These are my own zones for an ES statistic. They are not the Basel traffic light, which counts VaR exceptions over 250 days.
The reference distribution is generated from the model under test. That is not a flaw. The hypothesis being tested is “the distribution this model claimed is the right one”, so the null has to encode the model’s claim. A specification test necessarily conditions on the specification.
The flaw is elsewhere: the simulated null is not the model’s actual predictive distribution. It is a moment-matched approximation to it, and the approximation has a free parameter.
In my implementation the null draws from a Student-t whose degrees of freedom come from the median excess kurtosis the model predicted over the reporting window, via ν = 4 + 6/κ. Read back in plain words: the fatter the tails a model claims, the fewer degrees of freedom its null gets, and the wider the reference distribution it is graded against. That is a defensible engineering choice. It is also the whole ballgame, and I did not treat it as a modelling decision when I changed it.
3 Changes at Once, and the One I Credited
A t-copula, a Student-t null, and a new method shipped in the same revision. 2 of them push the verdict the same way.
The May revision did 3 things. It swapped a Gaussian copula for a t-copula in the base model, so joint extreme moves carried the tail thickness of the marginals; it swapped a Gaussian null for the moment-matched Student-t, on the grounds that a Gaussian reference over-rejects when the truth is fat-tailed. And it added a fifth method.
The expected-shortfall verdict improved. My changelog recorded that the copula and the null “did most of the work”, with the copula named first, and that the plain conformal method had reached green on its own merits.
2 of those 3 changes push Z₁ toward passing, and the design does not separate them. A t-copula does not lower the bar; it widens the modelled joint tail, which moves the statistic. A Student-t null does not move the statistic; it widens the reference distribution, which lowers the bar. Both make green likelier, for entirely different reasons, and I had run them together.
The reasoning that felt obvious at the time: the number got better, the changes were principled, therefore the changes worked. Each step is fine. The conclusion does not follow, because “the number got better” is consistent with the model improving and with the test getting easier, and nothing in the observation distinguishes them.
Here is the tell, and it was sitting in the changelog the whole time: the null was one of the things I changed, and a changed null changes the verdict without touching the model.
Separating them needs a factorial: both copulas crossed with both nulls, everything else fixed. That takes 2 backtest runs, because the null is applied after the fact and costs nothing to vary. It was not hard. It simply was not done, because the result already looked right.
The Null Did 11 Times the Work
Reading down a column isolates the model change. Reading across a row isolates the test change. The two effects are not the same size, and not by a little.
copula, down a column +0.0096 moves the statistic
null, across a row -0.1039 moves the threshold
Both changes shipped together, and the improvement was credited to the copula. The column comparison is the copula's actual contribution. Against the original Gaussian null the t-copula run is still yellow, so the model change on its own never reached green.
Holding the null fixed and switching only the copula moves the statistic by +0.0096, from −0.0004 to +0.0092. Holding the copula fixed and switching only the null moves the green threshold by −0.104, from +0.019 to −0.085.
The change to the test did roughly 11 times what the change to the model did.
Here is the stronger version, in the bottom-left cell. Scored against the original Gaussian null, the t-copula run is still yellow at α = 0.01 and still red at α = 0.025. The model change on its own never reached green at either level.
2The copula change is not worthless. It is a better description of joint tail behaviour and I would make it again. The claim here is narrower and only about attribution: it did not cause the result I attributed to it.6 Nulls, 10 Verdicts
If the threshold is doing the work, it is worth seeing how far it can be pushed by choices that all look reasonable.
So I re-scored every model against 6 nulls, holding the model, the data, the VaR and the ES fixed. The statistic cannot move under this treatment. Only the threshold can.
The dashed rule is the observed statistic. It is identical in all 6 rows, because the model, the data, the VaR and the ES are all held fixed. Only the shaded reject region moves, and it is what decides the verdict on the right. Both statistics are scored on the same simulated paths. Conformal-PID predicts a median excess kurtosis of 1.47 and breaches 24 times in 2,172 days against about 22 expected; the exceedance-count test gives p = 0.673.
For Z₁, across the 10 model-and-level cells (5 models at 2 confidence levels), the green counts are as follows:
- Gaussian null: 0 green, 3 yellow, 7 red.
- Student-t at df 15: 5 green.
- The shipped moment-matched t, df ≈ 8.1: 6 green.
- Student-t at df 5: 10 green.
- Vol-adjusted empirical bootstrap: 10 green.
Here is the whole sweep read through one model. For Conformal-PID at α = 0.01 the statistic is +0.0092 throughout. The threshold runs from +0.019 under a Gaussian null, where the verdict is yellow at p = 0.025, to −0.085 under the shipped null, where it is green at p = 0.656, to −0.190 at df 5, where p = 0.942. Same number, 3 different published answers.
With the conditional statistic, the zone grades the reference distribution someone chose for the model, not the model itself.
The obvious question is whether this is a property of ES backtesting or of the statistic I used. Acerbi and Székely report that the critical value of Z₂ stays roughly fixed across Student-t tails from 3 to 100 degrees of freedom, which would make it much less exposed to the choice of null. So I re-ran the sweep with Z₂, scored on the same simulated paths, and added 2 models I know are broken. Switch the statistic in Fig. 02 to see it.
The re-run gives 3 results:
- The threshold of Z₂ moves as much as the threshold of Z₁. For Conformal-PID at α = 0.01 it runs from −0.353 under the Gaussian null to −1.052 at df 5. That does not contradict Acerbi and Székely, whose result assumes the null is the model’s exact predictive distribution. Replicated in this code with exact nulls over 2,172 days, the 5th percentile of Z₂ stays between −0.44 and −0.36 from df 3 to Gaussian at α = 0.01, while that of Z₁ moves from −0.23 to −0.04. The nulls in this sweep are moment-matched approximations whose breach rates do not match the model’s own VaR, and Z₂, which responds to the number of breaches, moves with that mismatch.
- The verdicts of Z₂ barely move. Under each of the 5 parametric nulls, the same 6 of the 10 cells are green. The other 4 are red, or yellow at df 5. Those 4 are the Historical and Parametric baselines, which the exceedance-count test also rejects (p ≤ 0.015). They miss the threshold by more than the threshold travels.
- The empirical null passes everything under both statistics. That includes a model breaching at 2.5 times its stated rate. Its residual pool is built from the realised series, so a frequency error in the model is copied into the reference.
The 2 broken models separate what each statistic can see:
- VaR × 0.8. Conformal-PID with its VaR, ES and predicted scale all multiplied by 0.8. It breaches 54 times against about 22 expected, and the exceedance-count test rejects it at p < 0.001. Z₂ puts it in red under all 5 parametric nulls. Z₁ is −0.035 against Conformal-PID's +0.009 and passes under the shipped null at p = 0.276, because it averages over the breaches and cannot see how many there were.
- Gaussian tail. A Gaussian whose VaR matches Conformal-PID's, so it breaches on the same 24 days, but whose ES is the thinner Gaussian one. Z₂ passes it under all 6 nulls. Z₁ flags it only under its own Gaussian null, yellow at α = 0.01 and red at α = 0.025, and the Gaussian null puts the working Conformal-PID model in exactly the same zones. The flag does not discriminate.
The headline therefore holds in a narrower form than version 1.0 stated. Under both statistics the threshold depends on the null. The verdict depended on it heavily for Z₁ and hardly at all for Z₂ in this data, because the models Z₂ rejects breach too often by a wide margin. Neither statistic, scored against a simulated null, caught the thin-tailed model with the right breach rate.
The empirical bootstrap passes everything, and it is tempting to call it the neutral choice because it makes no parametric assumption about shape. It is not neutral. Its residual pool is drawn from the same realised series the test then evaluates, so it is distribution-free about shape but not independent of the data. That is a different circularity, not an absence of one.
It is reported here because it brackets the parametric nulls from the permissive side, which is useful. It is not the correct null. There is no correct null on the list.
The Test Rewarded the Claim, Not the Performance
Because the null is calibrated from the model's own predicted kurtosis, a model that claims fatter tails is graded more leniently.
The degrees of freedom in the null come from the model’s own predicted excess kurtosis. A model claiming fat tails produces a wide null and a threshold far below zero. A model claiming Gaussian tails produces a tight one and a threshold close to zero.
The Parametric EWMA baseline makes this stark. It predicts a Gaussian portfolio distribution, so its median predicted excess kurtosis is exactly 0, and the guard that requires κ > 0.1 sends it back to a Gaussian null. Switch panels in Fig. 02 to see it.
- Conformal-PID, claimed κ = 1.47, null df 8.08, green threshold −0.0847.
- Parametric EWMA, claimed κ = 0.00, Gaussian null, green threshold −0.0451.
Here is what that pair of thresholds measures: not how the 2 models performed, but the kurtosis each of them claimed. The model claiming fatter tails is judged against a threshold about twice as permissive.
An early version of my methodology carried a caveat saying exactly this: a model understating tail thickness gets an artificially easy test. A later revision withdrew it. My argument was that a thinner claimed tail tightens the threshold, which makes the test harder, not easier, so the caveat had the sign backwards.
That argument identifies a real channel. There is a second channel running the other way: understating tails shrinks the ES denominator, which pushes the observed Z₁ down and makes failure likelier. I asserted the second dominated. The measurement says the first does, and by a wide margin.
The original caveat was right, my withdrawal of it was wrong, and it is reinstated. I am recording this because the failure mode is the same one as the article’s: I reasoned about which effect was larger instead of measuring it, twice, in opposite directions.
What the Test Can Actually Detect, and What I Changed
The conditional statistic is blind to how often a model breaches by design. At a few dozen exceedances it sees tail depth only weakly.
Under the vol-adjusted empirical null, all 10 cells are green for Z₁. That includes the Parametric baseline, which fails the exceedance-count test at p < 0.001 and breaches at 2.07% against a 1% target. Its Z₁ of −0.167 sits comfortably inside a realistically shaped null: threshold −0.233, p = 0.464.
Version 1.0 of this paper read that as a limit of sample size, and that reading was wrong. Z₁ averages over the exceedance days, so it cannot detect an excess number of them at any sample size. Acerbi and Székely say so, and pair it with an exceedance-count test for exactly this reason. Here the pair worked as designed: the count test rejected the Parametric baseline, which Z₁ was never meant to do. The VaR × 0.8 model in § 04 shows the same thing under controlled conditions.
What is left for Z₁ to detect is tail depth, and there the sample size does bind. With 17 to 79 exceedances, Z₁ is an average over a few dozen numbers. In § 04 it could not separate the thin-tailed Gaussian model from the working Conformal-PID model: it flagged both under the Gaussian null and passed both under the df 5, df 8 and empirical nulls. The red and yellow Z₁ zones in § 04 are generated mainly by the narrowness of the parametric null, not by detected misspecification.
Passing the ES test is not evidence that the model is adequate.
3 questions, none of which need new data.
- What is the reference? If a verdict comes from comparing a statistic to a threshold, write down where the threshold came from. If it was estimated, it is a model, and it has assumptions.
- Did I change the reference and the thing being measured in the same revision? If so the attribution is unidentified. The fix is a factorial, and if the reference is applied after the fact it is nearly free.
- What does the test do to a model I know is broken, and broken in which way? Score deliberately bad models, one for each kind of error the test is supposed to catch. A model with too many breaches and a model with the right breach count but a thin tail test different things: Z₁ is blind to the first, and in § 04 Z₂ was blind to the second. If the test passes a model broken in the way it is meant to catch, it is not carrying the weight you are putting on it. This is the cheapest check here. Run with only one kind of broken model, it also produced the wrong reading of § 06 in version 1.0.
The obvious temptation was to drop the metric, or to quietly fix the attribution before anything shipped. I did neither. The zone stays in the code, demoted to a weak consistency check with this sensitivity attached to it, and the verdict was tested against its removal: the same models pass with or without that gate. The gate was never carrying anything.
The methodology manual ships carrying the corrected attribution under a heading saying what the earlier conclusion got wrong, and a tuning register recording, per choice, what was selected on which data. Here is its worst row: the May revision was made after seeing an unfavourable result on the same dataset, which cannot be distinguished from specification search using that dataset alone. Shipping the register is the point. A methodology that only records the decisions that flatter it is not a record.
This is one implementation scored on one dataset. It does not license 3 conclusions:
- The Acerbi-Székely tests are bad. The construction here follows the standard recipe, and the weakness is in a moment-matched approximation I chose.
- Every ES statistic is equally exposed to the null. In § 04 the threshold of Z₂ moved as much as that of Z₁, but its verdicts did not, and with an exact null its critical value is close to fixed.
- ES backtesting is hopeless. What this does show is what 2,172 days buys for tail depth at α = 0.01 with the conditional statistic: not enough to defend an adequacy claim. For tail depth alone, the way out is far more data or a pooled cross-sectional design.
The practical recommendation is Z₂ read together with an exceedance-count test, with the null specification reported beside the verdict.
What generalises is narrower and duller. Every adaptive component in a stack is a place where the score can move without the model moving, and a reference distribution estimated from the data is an adaptive component whether or not anyone calls it one.
Every figure here regenerates from public price data with 2 commands, seeded. The code and the reproduction instructions are in the analysis folder of the research repository. The five models’ full scoreboard is at kwantil.com/var-backtest, and its methodology manual carries the tuning register and the parts that do not flatter it.
If you run a model whose uncertainty estimates are load-bearing, Recipe 6.1 is where I would start. I take this work independently: matthew@mtanti.com, with terms on the services page.
@article{tanti2026,
author = {Matthew Tanti},
title = {Tail Risk Backtesting: When the Test Improves and the Model Does Not},
journal = {Kwantil},
year = {2026},
number = {KW-2026-04},
version = {1.2},
url = {https://kwantil.com/papers/did-the-model-get-better/}
}Matthew Tanti (2026). Tail Risk Backtesting: When the Test Improves and the Model Does Not. Kwantil, KW-2026-04, v1.2.