Skip to content

JesseRai/cfa-var-backtesting-engine

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CFA VaR Backtesting Engine

This project backtests Value at Risk (VaR) models for a five-asset portfolio built from the earlier CFA practical projects.

The portfolio uses daily returns from AAPL, MSFT, JPM, GLD, and SPY. The baseline portfolio value is GBP 100,000. The engine compares each one-day VaR forecast with the next realized portfolio return, counts VaR exceptions, calculates the Kupiec proportion of failures test, and assigns a Basel traffic-light classification.

What I Built

I started with a historical simulation VaR backtesting engine and then tested whether model changes improved calibration:

  • Baseline historical VaR with a 250-trading-day lookback at 95% confidence
  • Student's t VaR with fatter tails at 95% confidence
  • Historical VaR with a longer 500-trading-day lookback
  • Historical VaR with a 365-trading-day lookback tested at 90%, 95%, 97.5%, and 99% confidence

The goal was to see which model produced exception counts closest to the expected number of exceptions, while also checking the Basel traffic-light result.

Repository Structure

.
├── data/
│   └── README.md
├── outputs/
│   ├── model_comparison_summary.csv
│   └── var_backtest_summary.csv
├── src/
│   ├── backtesting.py
│   ├── data_loader.py
│   ├── main.py
│   ├── model_comparison.py
│   └── var_model.py
└── tests/
    └── test_backtesting.py

The source code is dependency-free and uses only the Python standard library.

How to Run

Add a data/portfolio_returns.csv file with this structure:

Date,Portfolio Return
2021-01-05,0.00609736854985427
2021-01-06,-0.006077101687306279

Run the baseline model:

python3 src/main.py

Run the model comparison sweep:

python3 src/model_comparison.py

Run the tests:

python3 -m unittest discover -s tests

Backtesting Method

For each model, the engine:

  1. Loads daily portfolio returns from CSV.
  2. Uses a rolling lookback window to estimate the next one-day VaR.
  3. Compares the next realized return with the VaR estimate.
  4. Flags an exception when the actual return is worse than the VaR forecast.
  5. Summarizes exception counts, expected exceptions, exception rate, Kupiec p-value, average VaR loss, average expected shortfall loss, and Basel traffic-light status.

The Kupiec test is used to judge statistical calibration. A higher p-value means the observed exception count is closer to what the model predicted.

Results

Main 95% VaR Model Comparison

Model Lookback Confidence Exceptions Expected Exceptions Exception Rate Kupiec p-value Basel
Historical VaR 250 95% 54 50.20 5.38% 0.5865 Red
Student's t VaR 250 95% 58 50.20 5.78% 0.2698 Red
Historical VaR 500 95% 20 37.70 2.65% 0.0012 Yellow
Historical VaR 365 95% 32 44.45 3.60% 0.0442 Yellow

The best statistical fit at the standard 95% confidence level was the 250-day historical VaR model. Its exception count of 54 was closest to the expected 50.20 exceptions, and it had the strongest Kupiec p-value.

The Student's t model was designed to add fatter tails, but on this dataset it did not improve performance. It produced more exceptions than the baseline and a lower Kupiec p-value.

The 500-day lookback and 365-day lookback at 95% were both more conservative, but they produced too few exceptions relative to expectation. This made their Kupiec p-values weaker.

365-Day Confidence Sweep

Confidence Exceptions Expected Exceptions Exception Rate Kupiec p-value Basel
90% 72 88.90 8.10% 0.0515 Red
95% 32 44.45 3.60% 0.0442 Yellow
97.5% 19 22.23 2.14% 0.4776 Yellow
99% 12 8.89 1.35% 0.3196 Green

Within the 365-day confidence sweep, the 97.5% model had the strongest Kupiec p-value. The 99% model achieved the best Basel traffic-light result because it landed in the green zone, but it was also more conservative and produced larger average VaR losses.

Findings

The best overall model depends on the criterion:

  • Best statistical fit at 95% VaR: baseline 250-day historical VaR
  • Best alternate calibration tested: 365-day historical VaR at 97.5%
  • Best Basel traffic-light result: 365-day historical VaR at 99%

My main conclusion is that the baseline 250-day historical VaR is the best fit for a standard 95% VaR backtest, because its observed exceptions are closest to the expected exception count. However, if the goal is to optimize Basel traffic-light treatment, the 365-day 99% model is more favorable because it achieves a green result.

This creates a useful risk-management trade-off: the statistically best-fitting model is not necessarily the model with the best regulatory traffic-light classification.

About

A Python-based CFA practical project that backtests portfolio Value at Risk (VaR) models using historical simulation, Student’s t VaR, different rolling lookback windows, Kupiec exception testing, and Basel traffic-light analysis to compare model fit and regulatory performance.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages