Main Content

Historical Value-at-Risk Estimation with US Treasury Bonds

This example shows how to estimate the value at risk (VaR) for a portfolio of US Treasury bonds by using both the historical and filtered historical VaR methods. While this example uses treasury bonds as a typical asset type, you can apply this workflow to any fixed-rate bond with similar par yield data.

VaR is a statistical method that quantifies the risk level associated with a portfolio. The VaR is an estimate of the maximum loss a portfolio can realize over a specified time horizon and at a given confidence level.

To apply the workflow for this example, you:

1. Transform yield curve data into zero-curve data.

2. Create and price a portfolio of US Treasury bonds by using FixedBond (Financial Instruments Toolbox) in the Financial Instruments Toolbox™.

3. Calculate the VaR for the portfolio of US Treasury bonds over a series of trading days by using historical zero-curve changes.

4. Backtest the VaR results to validate the methodology by using the varbacktest object.

5. Improve the VaR performance by implementing a filtered version of the historical VaR method.

Load and Prepare the Data

Load par yield data for US Treasury bonds and then remove dates with NaN values. This example uses a preexisting data set, although you can load the same data using the FRED (Federal Reserve Economic Data) connection in Datafeed Toolbox™.

load Data_USYieldCurve.mat

% Take out last ~5 years of data. You need both the numerical data from
% "Data", as well as the dates from "dates".
Data = Data(4030:end,:);
Dates = dates(4030:end,:);

% Remove the NaN values.
[Data,badDataIndex] = rmmissing(Data,1);
Dates(badDataIndex,:) = [];

% When interest rates equal 0, the returns become infinite, so you must
% perturb the rates from zero. You can avoid this issue by using differences
% instead of returns, but the filtered historical VaR assumes return
% data, so use returns for the basic historical VaR.
ZeroInd = Data == 0;
Data(ZeroInd) = .00001;

Transform Par Yield Rates into Zero Curve Rates

The loaded data is for the par yield curve. The par yield curve is the yield at which a bond trades at a face value. However, a zero curve is a more useful curve type for bond pricing. A zero curve is the yield curve a bond type has if its structure is a single payment at maturity, Therefore, you can convert the par rates into zero rates using pyld2zero.

To use pyld2zero, get the settle dates from the imported data and create dates for each tenor on the rate curve for each day in the data set. Then, you can use these dates along with the par yield data as inputs for pyld2zero to construct an equivalent zero curve for each day.

% Initialize zero curves, one for each day in our data set.
n = size(Data,1);
ZeroCurvesData = zeros(n,size(Data,2));

% Convert the numerical dates into datetime variables,
% where each date is a settle date for an associated zero curve.
SettleDates = datetime(Dates,'ConvertFrom','datenum');

% Construct the tenor dates for the zero curves associated with the days in
% the data set. To do this, add a vector of durations (with entries 
% corresponding to each tenor) to each settle date.
ZeroTimes = [calmonths([1, 3, 6]) calyears([1 2 3 5 7 10 20 30])];
ZeroDates = SettleDates + ZeroTimes;

% Cycle through each day in the data set and use pyld2zero to construct a
% zero curve for the given day.
for i=1:n
    ZeroCurvesData(i,:) = pyld2zero(Data(i,:),ZeroDates(i,:)',SettleDates(i))';
end

Once you have a set of zero curves for each day in the data set, you can calculate the returns. This calculation gives you a set of daily changes to the zero curve. You can use these changes, when you want to simulate changes to a given zero curve during the VaR calculation. However, if you do not perturb the interest rates away from zero, some of the returns are infinite. Another way to handle this problem is to use differences instead of returns, but returns work better for the filtered historical method. This example uses returns for the basic historical method.

ZeroReturns = tick2ret(ZeroCurvesData);

Create Portfolio of Bonds

Before you can use the zero curves and zero curve returns to estimate VaR values, you need a portfolio of bonds. In MATLAB®, you can use Financial Instruments Toolbox™ to represent bonds as FixedBond (Financial Instruments Toolbox) objects using the fininstrument (Financial Instruments Toolbox) function. For more information, see Workflow to Price an Interest-Rate Instrument (Financial Instruments Toolbox).

To use fininstrument, select 'FixedBond' as the input type and use the 'Maturity' input argument to correspond to the 5-, 7-, and 10-year tenors. When pricing the bonds using finpricer (Financial Instruments Toolbox), use the zero curves from the par yield US Treasury bond data. You can consider these fixed-rate bonds as similar to the US Treasury bonds. Therefore, when using fininstrument, set the 'Period' value to 2 because US Treasury bonds make two payments per year.

Create a portfolio with a 10-year, 7-year, and 5-year bond.

% Create a 10-year bond.
CouponRate = 0.06;
Maturity = datetime(2006,1,1) + calyears(10);
Period = 2;
Treasury10YearBond = fininstrument("FixedBond",Maturity=Maturity,CouponRate=CouponRate, ...
    Period=Period,Principal=100,Name="10YearTreasury");

% Create a 7-year bond.
CouponRate = 0.05;
Maturity =  datetime(2006,1,1) + calyears(7);
Period = 2;
Treasury7YearBond = fininstrument("FixedBond",Maturity=Maturity,CouponRate=CouponRate, ...
    Period=Period,Principal=100,Name="7YearTreasury");

% Create a 5-year bond.
CouponRate = 0.045;
Maturity = datetime(2006,1,1) + calyears(5);
Period = 2;
Treasury5YearBond = fininstrument("FixedBond",Maturity=Maturity,CouponRate=CouponRate, ...
    Period=Period,Principal=100,Name="5YearTreasury");

MyPortfolio = [Treasury5YearBond, Treasury7YearBond, Treasury10YearBond];

Calculate Actual P&L Values

When you calculate the VaR values, you can evaluate the performance of the VaR methodology by comparing the VaR values to actual realized profit-and-loss values (P&L). You can use the following code to calculate the realized P&L values. Using the code, loop through the ZeroCurves vector, and for each day, create a MyPricer object using finpricer (Financial Instruments Toolbox) with the relevant zero curve. You can use the MyPricer object with the price (Financial Instruments Toolbox) function to price the bonds. You can then sum the prices of the bonds in the portfolio to derive the total value of the portfolio for a specific day.

n = size(Data,1);
PortfolioValues = zeros(n,1);
CurveBasis = 3;
for i = 1:n
    ZeroCurve = ratecurve('Zero',SettleDates(i),ZeroDates(i,:),ZeroCurvesData(i,:),Basis=CurveBasis); 
    MyPricer = finpricer("Discount",DiscountCurve=ZeroCurve);
    prices = price(MyPricer,MyPortfolio);
    PortfolioValues(i) = sum(prices);
end

Once you calculate the value of the portfolio for each trading day, you can derive the P&L values by differencing the daily portfolio values.

ActualPandL = diff(PortfolioValues);

% Plot the P&L values for visualization.
figure;
plot(SettleDates(1:end-1),ActualPandL);
title('P&L Over Time');

Figure contains an axes object. The axes object with title P&L Over Time contains an object of type line.

Initialize VaR Variables

Initialize the variables before beginning the VaR calculation.

% Set the VaR threshold (95% and 99%).
pVaR = [0.95 0.99];

% Set the number of historical zero-curve perturbations that you will consider
% when calculating each VaR value.
LookBackWindowSize = 250;

% Find number of trading days in the data set.
SampleSize = size(ZeroCurvesData,1);

% Select the day in the data set on to begin calculating VaR values.
DayBeforeStartIndex = 949;
TestWindowStart = DayBeforeStartIndex + 1;
TestWindow = TestWindowStart:SampleSize;

% Initialize the VaR variables.
Historical95 = zeros(length(TestWindow),1);
Historical99 = zeros(length(TestWindow),1);

Calculate Historical VaR

The code that follows loops through the zero-curve data and, on each day, the code uses the zero curve to calculate the daily VaR value. It makes this calculation by looking back at historical zero-curve returns, and using those returns to adjust the current zero curve, it changes the price of your portfolio. After running this code for each day in the lookback window, you obtain a set of simulated P&L values. Using these simulated P&L values, you can extract a percentile loss. For example, you can get the P&L loss at the 99th percentile for the 99% VaR.

for t = TestWindow
    
    % Get current zero curve and the differences from the Lookback Window.
    previousDayTenors = ZeroCurvesData(t-1,:);
    LookBackWindow = t-LookBackWindowSize-1:t-2;
    returns = ZeroReturns(LookBackWindow,:);

    % Calculate what the zero curve will be if the tenors undergo
    % historical changes.
    GeneratedZeroCurves = previousDayTenors.*(1+returns);

    % Get the settle date for the trading day.
    SettleDate = SettleDates(t,:);

    % Get the cashflows for each bond in the portfolio and use these
    % to price the bond with discount factors derived from a zero curve.
    CF = cashflows(MyPortfolio,SettleDate);

    % Initialize the discount factors that are derived from the historically 
    % simulated zero curve using a for loop.
    DF = zeros(LookBackWindowSize,height(CF));

    for i = 1:LookBackWindowSize
        ZeroCurve = ratecurve('Zero',SettleDate,ZeroDates(t,:),GeneratedZeroCurves(i,:));
        DF(i,:) = discountfactors(ZeroCurve,CF.Time);
    end

    % Derive the portfolio value for every scenario by multiplying the
    % cashflows by the discount factors and then summing the results.
    PortValue = sum(CF{:,:}'*DF')';

    % Calculate the simulated P&L values for each historical return. To do
    % this subtract yesterday's portfolio value from the simulated
    % portfolio values.
    SimulatedPandL = PortValue - PortfolioValues(t-1);
    
    % Find the relevant percentile loss for the 95% and 99% VaR values
    n = t-TestWindowStart + 1;
    Historical95(n) = hHistoricalVaR(SimulatedPandL,pVaR(1)); 
    Historical99(n) = hHistoricalVaR(SimulatedPandL,pVaR(2)); 

end

Backtest VaR Values

After calculating the VaR values for the portfolio, validate the VaR methodology by backtesting the results. Use varbacktest and the associated test functions to validate the performance of the VaR methodology.

Create a varbacktest object that contains the P&L values for the portfolio, along with the calculated 95% and 99% VaR values.

vbt = varbacktest(ActualPandL(TestWindow-1),[Historical95,Historical99],VaRLevel=[.95, .99],Time=SettleDates(TestWindow-1,:),VaRID={'Historical95','Historical99'});

Use plot to visualize the varbacktest results.

figure;
axes();
plot(vbt);

Figure contains an axes object. The axes object with title VaR Backtesting, xlabel Time contains 4 objects of type line. One or more of the lines displays its values using only markers These objects represent Portfolio, Historical95, Historical99, Exceptions.

The VaR exceptions cluster around the final months of the test window. This result is negative and suggests that the VaR exceptions are not independent. This can happen with the basic historical method when market behavior changes to a new regime because the historical VaR methodology is slow to update in such circumstances [1], [2]. Also, the VaR might have too many exceptions. To check if the number of VaR violations is a problem, investigate further by using the varbacktest summary and runtests functions.

SummaryTable = summary(vbt)
SummaryTable=2×10 table
    PortfolioID        VaRID         VaRLevel    ObservedLevel    Observations    Failures    Expected    Ratio     FirstFailure    Missing
    ___________    ______________    ________    _____________    ____________    ________    ________    ______    ____________    _______

    "Portfolio"    "Historical95"      0.95         0.91971           274            22         13.7      1.6058         79            0   
    "Portfolio"    "Historical99"      0.99          0.9708           274             8         2.74      2.9197        189            0   

The ObservedLevel column of the summary output suggests that the 95% confidence level is only 92%, while the 99% VaR was 97%. These results indicate the VaR calculation methodology is not sufficiently conservative. To check the VaR model performance statistically, use runtests.

TestTable = runtests(vbt)
TestTable=2×11 table
    PortfolioID        VaRID         VaRLevel      TL       Bin       POF       TUFF       CC       CCI       TBF       TBFI 
    ___________    ______________    ________    ______    ______    ______    ______    ______    ______    ______    ______

    "Portfolio"    "Historical95"      0.95      yellow    reject    reject    accept    accept    accept    reject    reject
    "Portfolio"    "Historical99"      0.99      yellow    reject    reject    accept    reject    accept    reject    reject

The Traffic Light rating for both VaR models is at yellow and, while some of the other varbacktest statistical tests accept the model, most reject it. This result again indicates that the VaR values are an underestimate. One of the tests that fails for both VaRs is the TBFI statistic test that checks if the time between failures is independent.

To run the TBFI test, use tbfi.

tbfi(vbt)
ans=2×14 table
    PortfolioID        VaRID         VaRLevel     TBFI     LRatioTBFI    PValueTBFI    Observations    Failures    TBFMin    TBFQ1    TBFQ2    TBFQ3    TBFMax    TestLevel
    ___________    ______________    ________    ______    __________    __________    ____________    ________    ______    _____    _____    _____    ______    _________

    "Portfolio"    "Historical95"      0.95      reject      47.986       0.0010898        274            22         1         2       4.5      10        79        0.95   
    "Portfolio"    "Historical99"      0.99      reject      33.218      5.6251e-05        274             8         1         3       5.5      25       189        0.95   

The PValues for the 95% and 99% VaRs are about 0.001 and 0.0001. This test indicates high confidence that the failures are not independent.

Investigate Market Trends

Why might the historical VaR method fail to match actual portfolio behavior? One possibility is that the LookbackWindow setting is too small. At 250 trading days, the LookbackWindow value represents approximately one year of data. One weakness of the historical approach to VaR estimation is that you need a large set of historical data to accurately estimate tail probabilities [1]. For example, with 250 observations, the 1% tail from a 99% VaR contains only two data points. You can address this problem by extending the LookbackWindow.

A more difficult problem to solve is the fact that market circumstances change over time. Interest rates can go through periods defined by an upward trend, downward trend, or high/low volatility. When one regime ends and another begins, the previous regime may dominate the historical simulation, leading to erroneous VaR estimates.

To see if the VaR exceptions are explained by changes in market circumstances, use the following code that uses a for-loop. This code finds the value of the portfolio on each day in the data set, where the date is held constant to match the first day in the test window. The date is held constant so that the time-to-maturity remains constant and therefore, the changes in the portfolio value are driven solely by the interest-rate curve.

n = size(Data,1);
PortfolioValuesForPlot = zeros(n,1);
CurveBasis = 3;
for i = 1:n
    ZeroCurve = ratecurve('Zero',SettleDates(TestWindow(1)),ZeroDates(1,:),ZeroCurvesData(i,:),Basis=CurveBasis); 
    MyPricer = finpricer("Discount",DiscountCurve=ZeroCurve);
    prices = price(MyPricer,MyPortfolio);
    PortfolioValuesForPlot(i) = sum(prices);
end

The test window for VaR calculation uses only the final 250 days of the data and LookbackWindow is also 250, so only the final 500 days are relevant to this analysis.

figure;
plot(SettleDates,PortfolioValuesForPlot);
hold on
xline(SettleDates(TestWindow(1)),'r');
xline(SettleDates(TestWindow(1)-LookBackWindowSize),'k');
legend({'Portfolio value','Start of test window','Start of lookback window'},"Location","northwest");
hold off

Figure contains an axes object. The axes object contains 3 objects of type line, constantline. These objects represent Portfolio value, Start of test window, Start of lookback window.

When you plot the portfolio values and look at the relevant 500 zero curves, you can see that the value of the portfolio is highly volatile and trends down for the first half of the initial LookbackWindow value. After this point, the portfolio trends flat, then upwards, until the end of the test window, when the volatility increases and the value goes down. This result explains why the exceptions cluster around the final portion of the test window, as this region experiences the greatest losses. This result also explains why the VaR estimate trends smaller over time. The first half of the initial lookback window contains the worst losses, and, as time moves forward, that period is removed from the calculations. The result is that the test and lookback windows overlay multiple market regimes and the basic historical VaR methodology is too slow to update, which causes the VaR estimate to be too high at first, and too low at the end.

Filtered Historical VaR

Apply the following two modifications to improve the historical VaR estimates:

1. Increase the size of the LookbackWindow.

2. Employ volatility weighting to adjust to changing market regimes. This methodology is sometimes referred to as a filtered historical VaR or filtered historical simulation VaR [2].

You can implement the first item by setting LookBackWindowSize to a larger number (450), and then proceed.

% Set the number of historical zero-curve perturbations that you will consider
% when calculating each VaR value.
LookBackWindowSize = 450;

The second item is the more difficult to implement because it requires calculating volatilities for the return series on a rolling basis and storing the results.

The idea behind filtered historical VaR is to scale each return value Ri by σTσi. Here, σT is the volatility for the return series calculated on the latest day T, while σi was the volatility of the same return series calculated on day i. By scaling the returns in such a manner, you can use the returns from previous days to simulate possible outcomes, but in a manner that updates more rapidly when the market trends change.

For example, if the latest volatility, σT, is greater than the volatility on day i, σi, then σTσi>1. The scaling causes Ri to increase in magnitude, and, therefore, make it more relevant to recent high-volatility market behavior.

To implement this VaR calculation method, you must first calculate the volatilities for LookbackWindow. Use the Exponentially Weighted Moving Average (EWMA) method for volatility calculation. (The lambda weight in this calculation is taken from an example in [3].)

TenorSigmas = zeros(LookBackWindowSize,length(ZeroCurvesData(1,:)));
EWMAlambda = 0.94;
n = 0;

for t = DayBeforeStartIndex-LookBackWindowSize+1:DayBeforeStartIndex

    n = n+1;
    LookBackWindow = t-LookBackWindowSize-1:t-2;
    returns = ZeroReturns(LookBackWindow,:);

    % Get the exponentially weighted covariance matrix.
    [~,ewmaCovMatrix] = ewstats(returns,EWMAlambda);

    % Remove the diagonal.
    TenorVariances = diag(ewmaCovMatrix,0);

    % Store the volatilities.
    TenorSigmas(n,:) = sqrt(TenorVariances);
    
end

Once you have the volatilities for the initial LookbackWindow, you can begin calculating the filtered historical VaR values.

% Initialize the VaR variables.
FilteredHistoricalVaR95 = zeros(length(TestWindow),1);
FilteredHistoricalVaR99 = zeros(length(TestWindow),1);

% Calculate the VaR.
for t = TestWindow

    % Get current zero curve and the differences from the lookback window.
    previousDayTenors = ZeroCurvesData(t-1,:);
    LookBackWindow = t-LookBackWindowSize-1:t-2;

    % Move each volatility back one day.
    TenorSigmas = circshift(TenorSigmas,-1,1);

    % Calculate the latest volatility.
    returns = ZeroReturns(LookBackWindow,:);
    [~,ewmaCovMatrix] = ewstats(returns,EWMAlambda);

    % Update the volatility vector.
    TenorVariances = diag(ewmaCovMatrix,0);
    TenorSigmas(end,:) = sqrt(TenorVariances)';

    % Calculate the scaling factor for each historical return.
    FilteredHistoricalSigmas = zeros(LookBackWindowSize,size(returns,2));
    for i = 1:LookBackWindowSize
        FilteredHistoricalSigmas(i,:) = TenorSigmas(end,:)./TenorSigmas(i,:);
    end

    % Calculate what the zero curve would be if the tenors undergo
    % historical changes with a scaling factor.
    GeneratedScaledZeroCurves = previousDayTenors.*(1+returns.*FilteredHistoricalSigmas);
    SettleDate = SettleDates(t,:);

    % Get the cashflows for each bond in the portfolio, and use this result to 
    % to price the bond with discount factors derived from a zero curve.
    CF = cashflows(MyPortfolio,SettleDate);

    % Initialize the discount factors, which are derived from the historically 
    % simulated zero curves in the for-loop.
    FilteredHistoricalDF = zeros(LookBackWindowSize,height(CF));

    for i = 1:LookBackWindowSize
        FilteredHistoricalZeroCurve = ratecurve('Zero',SettleDate,ZeroDates(t,:),GeneratedScaledZeroCurves(i,:));
        FilteredHistoricalDF(i,:) = discountfactors(FilteredHistoricalZeroCurve,CF.Time);
    end

    % Derive the portfolio value for every scenario by multiplying the
    % cashflows by the discount factors and then summing the results.
    FilteredHistoricalPortValue = sum(CF{:,:}'*FilteredHistoricalDF')';

    % Find the simulated P&L values.
    n = t-TestWindowStart + 1;
    FilteredHistoricalSimulatedPandL = FilteredHistoricalPortValue - PortfolioValues(t-1);

    % Save the VaR values.
    FilteredHistoricalVaR95(n) = hHistoricalVaR(FilteredHistoricalSimulatedPandL,pVaR(1)); 
    FilteredHistoricalVaR99(n) = hHistoricalVaR(FilteredHistoricalSimulatedPandL,pVaR(2)); 

end

Compare Filtered Historical VaR Results

Once you have the new VaR values for the test window, you can backtest the results and compare these results to the original VaR methodology:

vbtFiltered = varbacktest(ActualPandL(TestWindow-1),[FilteredHistoricalVaR95,FilteredHistoricalVaR99],"VaRLevel",[.95, .99],'Time',SettleDates(TestWindow-1,:),'VaRID',{'FilteredVaR95','FilteredVaR99'});
figure;
plot(vbtFiltered);

Figure contains an axes object. The axes object with title VaR Backtesting, xlabel Time contains 4 objects of type line. One or more of the lines displays its values using only markers These objects represent Portfolio, FilteredVaR95, FilteredVaR99, Exceptions.

vbtAllVaRs = varbacktest(ActualPandL(TestWindow-1),[Historical95,Historical99,FilteredHistoricalVaR95,FilteredHistoricalVaR99],"VaRLevel",[.95, .99, .95, .99],'Time',SettleDates(TestWindow-1,:),'VaRID',{'Historical95','Historical99','FilteredVaR95','FilteredVaR99'});
FilteredSummaryTable = summary(vbtAllVaRs)
FilteredSummaryTable=4×10 table
    PortfolioID         VaRID         VaRLevel    ObservedLevel    Observations    Failures    Expected    Ratio     FirstFailure    Missing
    ___________    _______________    ________    _____________    ____________    ________    ________    ______    ____________    _______

    "Portfolio"    "Historical95"       0.95         0.91971           274            22         13.7      1.6058         79            0   
    "Portfolio"    "Historical99"       0.99          0.9708           274             8         2.74      2.9197        189            0   
    "Portfolio"    "FilteredVaR95"      0.95         0.93066           274            19         13.7      1.3869          5            0   
    "Portfolio"    "FilteredVaR99"      0.99         0.97445           274             7         2.74      2.5547          5            0   

The results have clearly improved, especially for the 95% VaR. The VaR exceptions are not as heavily clustered in a single region and the VaR estimates do not consistently get lower as the lookback window moves. Lastly, the performance of the 95% VaR has gone from 92% to 93%, a significant if not complete improvement.

The test results are also improved for the 95% VaR. The traffic light test (tl) has gone from 'yellow' to 'green' and all of the varbacktest tests now list 'accept' rather than 'reject'. The time until first failure test (tuff) has switched from 'accept' to 'reject' for the 99% VaR, but given that the failure rate has improved and the independence test has improved, the overall results are better for the 99% VaR.

FilteredTestTable = runtests(vbtAllVaRs)
FilteredTestTable=4×11 table
    PortfolioID         VaRID         VaRLevel      TL       Bin       POF       TUFF       CC       CCI       TBF       TBFI 
    ___________    _______________    ________    ______    ______    ______    ______    ______    ______    ______    ______

    "Portfolio"    "Historical95"       0.95      yellow    reject    reject    accept    accept    accept    reject    reject
    "Portfolio"    "Historical99"       0.99      yellow    reject    reject    accept    reject    accept    reject    reject
    "Portfolio"    "FilteredVaR95"      0.95      green     accept    accept    accept    accept    accept    accept    accept
    "Portfolio"    "FilteredVaR99"      0.99      yellow    reject    reject    reject    accept    accept    reject    reject

The independence test (tbfi) is now accept for the 95% VaR, although it remains reject for the 99% VaR.

tbfi(vbtAllVaRs)
ans=4×14 table
    PortfolioID         VaRID         VaRLevel     TBFI     LRatioTBFI    PValueTBFI    Observations    Failures    TBFMin    TBFQ1    TBFQ2    TBFQ3    TBFMax    TestLevel
    ___________    _______________    ________    ______    __________    __________    ____________    ________    ______    _____    _____    _____    ______    _________

    "Portfolio"    "Historical95"       0.95      reject      47.986       0.0010898        274            22         1          2      4.5      10        79        0.95   
    "Portfolio"    "Historical99"       0.99      reject      33.218      5.6251e-05        274             8         1          3      5.5      25       189        0.95   
    "Portfolio"    "FilteredVaR95"      0.95      accept      27.838          0.0866        274            19         1          4        7      16        63        0.95   
    "Portfolio"    "FilteredVaR99"      0.99      reject      17.204        0.016126        274             7         4       4.25       10      68       110        0.95   

The p-value for the 95% VaR is now 0.087, which is significantly better than .001 from the basic historical method. The p-value for the 99% VaR has also improved, going from 0.0001 to 0.016.

There are options to further improve the VaR performance. In addition to scaling the returns based on historical volatilities, you can exponentially weight the simulated zero-curve returns based on age so that older returns contribute less than newer returns to the percentile calculations. This methodology is a form of probability weighting, as a return that is less likely should contribute less to the percentile calculation [1]. You can also adjust the lambda values for the volatility calculations or use a different model for the volatility calculations, such as a GARCH model. For a discussion on the strengths and weaknesses of the filtered historical VaR method and ways to improve upon it, see [2].

References

1. Alexander, C. Value-at-Risk Models. John Wiley & Sons, Ltd. 2008.

2. Gurrola-Perez, P. and D. Murphy. Filtered Historical Simulation Value-at-Risk Models and Their Competitors. Bank of England, 2015.

3. J.P. Morgan, Reuters. "RiskMetrics-Technical Document." Morgan Guaranty Trust Company of New York, 1996.

4. Rockafellar, R. T., and S. Uryasev. "Conditional Value-at-Risk for General Loss Distributions." Journal of Banking and Finance. Vol. 26, 2002, pp. 1443–1471.

Local Functions

function VaR = hHistoricalVaR(Sample,VaRLevel)
    % Compute historical VaR and ES
    % See [4] for technical details.

    % Convert to losses.
    Sample = -Sample;
    
    N = length(Sample);
    k = ceil(N*VaRLevel);
    
    z = sort(Sample);
    
    VaR = z(k);
end

Related Topics