Value at Risk Backtest
Backtest a Value at Risk model against realized returns.
A Value at Risk estimate is only useful if it is actually well-calibrated against reality. This method builds a rolling, out-of-sample VaR path (each VaR estimate uses only the window_size returns preceding it, never the return it is compared against) and tests it with Kupiec’s Proportion of Failures test (is the overall breach rate consistent with alpha?) and/or Christoffersen’s independence test (are breaches spread out over time, or do they cluster together?).
Regulators backtest VaR on the number of exceptions over the last 250 trading days (the Basel traffic-light approach at 99% confidence), and Kupiec’s test formalises the same question. With few exceptions the test has little power, so a model that passes is not necessarily well calibrated. Clustered exceptions, which Christoffersen’s test detects, are the usual failure of historical VaR when volatility rises.
The formula is as follows (with the default distribution=”historic”, alpha=0.05 and window_size=252):
\[\text{VaR}_{t} = \operatorname{Quantile}(\text{Return}_{t-252},\; ...,\; \text{Return}_{t-1} \text{;} \alpha)\] \[\text{Breach}_{t} = 1 \;\text{if Return}_{t} < \text{VaR}_{t},\;\; \text{else 0}\] \[\text{Kupiec Statistic} = - 2 \cdot \text{ln} \left[(1 - \alpha) ^{n - x} \cdot \alpha ^{x} / ((1 - x / n) ^{n - x} \cdot (x / n) ^{x})\right]\] \[\text{Christoffersen Statistic} = - 2 \cdot \text{ln} \left[L(\pi) / L(\pi_{01},\; \pi_{11})\right]\]Where n is the number of out-of-sample VaR estimates and x the number of breaches. L(pi) is the likelihood of the breach sequence with a single breach probability pi, and L(pi_01, pi_11) that of a first-order Markov chain in which the breach probability depends on whether the previous period was a breach. Both statistics are compared with a χ² distribution with one degree of freedom for the p-value.
For more information about the methods, see the following papers:
- Kupiec, P.H. (1995). “Techniques for Verifying the Accuracy of Risk Measurement Models.” The Journal of Derivatives, 3(2), 73-84.
- Christoffersen, P.F. (1998). “Evaluating Interval Forecasts.” International Economic Review, 39(4), 841-862.
Also known as: VaR validation, VaR backtest, POF test.
Value at Risk Backtest in Python or via the MCP
Install the Finance Toolkit with:
pip install financetoolkit -U
Then call get_var_backtest as shown below.
from financetoolkit import Toolkit
toolkit = Toolkit(
["EURUSD", "USDJPY", "GBPUSD"],
api_key="FINANCIAL_MODELING_PREP_KEY",
start_date="2021-01-01",
end_date="2025-12-31",
)
toolkit.risk.get_var_backtest(test="kupiec")
Or, after setting up the Finance Toolkit MCP, ask:
Does a rolling one-year 95% historical Value at Risk pass Kupiec's backtest for the euro against the dollar, the dollar against the yen, the pound against the dollar and the S&P 500 from 2021 to 2025?
Which returns:
| EURUSD | USDJPY | GBPUSD | Benchmark | |
|---|---|---|---|---|
| Kupiec Statistic | 8.3686 | 1.8323 | 2.2039 | 0.0836 |
| P-Value | 0.0038 | 0.1759 | 0.1377 | 0.7725 |
Parameters
get_var_backtest accepts the following parameters.
| Parameter | Type | Description | Example |
|---|---|---|---|
period | str | The data frequency for returns (daily, weekly, monthly, quarterly or yearly). Defaults to “daily”, since window_size is expressed in return observations of this frequency (252 only means “about one year” when period is daily). | "weekly" |
distribution | str | The distribution to use for the rolling VaR estimates, one of “historic”, “gaussian”, “studentt” or “evt”. Defaults to “historic”. “studentt” and “evt” fit their distribution to every window, which takes minutes for years of daily data, where “historic” and “gaussian” take well under a second. | "studentt" |
alpha | float | The tail probability for the VaR estimates (e.g., 0.05 for the 95% confidence level). Defaults to 0.05. With “evt” it can be at most 0.05, since the Generalized Pareto Distribution is fitted above the 95th percentile of losses; a larger alpha gives an empty result (with a logged error). | 0.01 |
window_size | int | The rolling window size (in number of return observations) used to estimate each VaR value. Defaults to 252 (approximately one trading year of daily returns). | 52 |
test | str | Which test(s) to run, one of “kupiec”, “christoffersen” or “both”. Defaults to “both”. | "kupiec" |
rounding | int | None | The number of decimals to round the results to. Defaults to None. | 2 |