1
$\begingroup$

I'm trying to calibrate a Hull and White model with constant volatility, mean reversion and theta such that the model can reproduce the initial Term Structure. I'm using this python code adapted from "The QuantLib Python Cookbook"

Here is the initial term structure (EUR OIS curve)

jours means days and taux means rate

jours;taux
1;-0.00472
7;-0.00472
14;-0.00471
30;-0.004719
60;-0.0047875
90;-0.00481
120;-0.004835
150;-0.00487
180;-0.00489
210;-0.00491
240;-0.004931
270;-0.004952
300;-0.00497
330;-0.0049875
360;-0.005
540;-0.005089
720;-0.0051225
900;-0.0051325
1080;-0.0050975
1440;-0.004946
1800;-0.004665
2160;-0.004275
2520;-0.003843
2880;-0.003375
3240;-0.0028865
3600;-0.00238
3960;-0.001885
4320;-0.00142
5400;-0.00027
7200;0.000655
9000;0.000775
10800;0.00056
12600;0.000351
14400;0.0001485
18000;-0.0002425
36000;-0.0002425

And the normal volatility surface for the Euribor 3M Swaptions

Expir.;1;2;3;4;5;7;10;12;15;20;25;30
30;0.02;0.03;0.03;0.03;0.03;0.03;0.03;0.03;3.86;9.6;9.32;7.2
90;0.02;0.03;0.03;0.03;0.03;0.03;0.03;0.04;5.51;10.11;9.49;7.32
180;0.02;0.03;0.03;0.03;0.03;0.03;0.03;0.03;6.66;10.83;9.71;7.48
270;0.02;0.02;0.03;0.03;0.03;0.03;0.03;0.02;7.12;11.23;9.91;7.64
360;0.02;0.02;0.03;0.03;0.03;0.03;0.02;0.02;6.77;11.59;10.1;7.78
720;0.02;0.02;0.02;0.03;0.02;0.03;0.02;6.04;11.68;12.81;10.76;8.31
1080;0.02;0.02;0.02;0.02;0.03;0.02;6.19;10.95;14.2;13.76;11.27;8.74
1440;0.01;0.02;0.02;0.02;0.02;4.38;12.72;15.45;15.79;14.36;11.56;8.99
1800;0.01;0.02;0.02;0.02;4.3;10.85;15.72;17.16;16.85;14.62;11.63;9.04
2160;0.01;0.02;3.23;8.08;13.52;15.65;18.2;18.73;17.67;14.72;11.56;8.97
2520;2.84;7.32;11.34;14.02;15.79;18.06;19.29;19.27;17.8;14.42;11.21;8.66
2880;9.74;13.93;17.01;18.52;19.41;20.43;20.33;19.72;17.71;13.97;10.74;8.22
3240;16.2;18.46;20.21;20.96;21.36;21.56;20.52;19.44;17.09;13.19;10.03;7.56
3600;21.37;20.4;20.94;21.45;21.73;21.56;19.92;18.54;16.02;12.12;9.09;6.69
4320;24.92;22.86;22.6;22.28;21.81;20.7;17.96;16.16;13.41;9.66;6.97;4.43
5400;22.36;20.06;19.29;18.42;17.45;15.61;12.43;10.6;8.18;5.25;2.98;0.48
7200;12.04;10.06;8.88;7.77;6.72;4.97;2.75;1.55;1.1;0.01;0.01;0.01

Here is the code I'm using

import QuantLib as ql
from QuantLib import *
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from collections import namedtuple
from pprint import pprint
import math
from scipy.interpolate import interp1d

zc = pd.read_csv('zc_auj.csv',sep=';')
matrice_vol = pd.read_csv('vol_auj.csv',sep = ';')



today = ql.Date(12,11,2020)
settlement = today+1
ql.Settings_instance().evaluationDate = today
spot_dates = [today + ql.Period(i,ql.Days) for i in zc['jours']]
spot_rates = [i for i in zc['taux']]
day_count = ql.Thirty360()
calendar = ql.France()
interpolation = ql.Linear()
compounding = ql.Compounded
compounding_frequency = ql.Annual
displacement = 0



spot_curve =ql.ZeroCurve(spot_dates,
                         spot_rates,
                         day_count,
                         calendar,
                         interpolation,
                         compounding,
                         compounding_frequency)

term_structure = ql.YieldTermStructureHandle(spot_curve)

index = ql.Euribor3M(term_structure)

CalibrationData = namedtuple("CalibrationData",
                             "start, length, volatility")


data = []
for i,v in enumerate(range(1,matrice_vol.shape[0]+1,1)):
    for j,w in enumerate(range(matrice_vol.shape[1]-1)):
        data.append(CalibrationData(int(matrice_vol.iloc[i][0]),int(matrice_vol.columns[j+1]),matrice_vol.iloc[i,j+1]/100))



    
def create_swaption_helpers(data, index, term_structure, engine):
    swaptions = []
    fixed_leg_tenor = ql.Period(3, ql.Months)
    fixed_leg_daycounter = ql.Actual360()
    floating_leg_daycounter = ql.Actual360()
    for d in data:
        vol_handle = ql.QuoteHandle(ql.SimpleQuote(d.volatility))
        helper = ql.SwaptionHelper(ql.Period(d.start,ql.Days),
                                   ql.Period(d.length,ql.Years),
                                   vol_handle,
                                   index,
                                   fixed_leg_tenor,
                                   fixed_leg_daycounter,
                                   floating_leg_daycounter,
                                   term_structure,
                                   ql.BlackCalibrationHelper.RelativePriceError,
                                   ql.nullDouble(),
                                   100,
                                   ql.Normal,
                                   0)
        helper.setPricingEngine(engine)
        swaptions.append(helper)
    return swaptions


def calibration_report(swaptions, data):
    columns = ["Model Price", "Market Price", "Implied Vol", "Market Vol", "Rel Error Price", "Rel Error Vols"]
    report_data = []
    cum_err = 0.0
    cum_err2 = 0.0
    for i, s in enumerate(swaptions):
        model_price = s.modelValue()
        market_vol = data[i].volatility
        black_price = s.blackPrice(market_vol)
        rel_error = model_price/black_price - 1.0
        implied_vol = s.impliedVolatility(model_price,1e-7, 1000, 0, 1)
        rel_error2 = implied_vol/market_vol-1.0
        cum_err += rel_error*rel_error
        cum_err2 += rel_error2*rel_error2
        report_data.append((model_price, black_price, implied_vol,market_vol, rel_error, rel_error2))
    
    print ("Cumulative Error Price: %7.5f" % math.sqrt(cum_err))
    print ("Cumulative Error Vols : %7.5f" % math.sqrt(cum_err2))
    return pd.DataFrame(report_data,columns= columns, index=['']*len(report_data))

model = ql.HullWhite(term_structure)
engine = ql.JamshidianSwaptionEngine(model)
swaptions = create_swaption_helpers(data,index,term_structure,engine)

optimization_method = ql.LevenbergMarquardt(10e-8,1.0e-8,1.0e-8)
end_criteria = ql.EndCriteria(10000, 100, 1e-6, 1e-8, 1e-8)
model.calibrate(swaptions, optimization_method, end_criteria)
a, sigma = model.params()
print ("a = %6.5f, sigma = %6.5f" % (a, sigma))

zeta = calibration_report(swaptions, data)

As a backtest I'm trying to price zero_coupon bonds using the calibrated model (as I mentionned theta such that I can exactly match the initial yield curve) but my results don't match.

When I execute the code I get a = 0.03319, sigma = 0.00023

Here's a plot of dicount factors derived from the Term_structure and those computed using the Hull and White formula for zero coupon bonds

enter image description here

Can someone tell me what I'm doing wrong ?

Thank you, Hilbert

$\endgroup$
5
  • $\begingroup$ Those don't really look like Normal vols and you are using the OIS curve to estimate Euribor 3M forwards? $\endgroup$ – David Duarte Nov 12 '20 at 15:34
  • $\begingroup$ They're extracted from Bloomberg VCUB EUR Bloomberg Cube as of today. And I'm using the EUR OIS term structure. What I'm trying to do is first calibrate the Hull and White model. Then i try to price zero-coupon bonds using this calibrated model in order to deduce the initial term structure (as a backtest), The problem is with the calibrated model my prices are completely different from those computed directly from the yield curve. $\endgroup$ – Hilbert Nov 12 '20 at 15:50
  • $\begingroup$ @DavidDuarte can you please explain what make you think these are not normal volatilites ? I'm still learning and very curious about. Thank you very much $\endgroup$ – Hilbert Nov 12 '20 at 18:49
  • $\begingroup$ For one, the top diagonal seems to be undefined which more or less coincides with negatives ATM rates... $\endgroup$ – David Duarte Nov 13 '20 at 14:39
  • $\begingroup$ @Hilbert could you place your code to produce the plot? This might help to understand the issue. $\endgroup$ – FunnyBuzer Nov 16 '20 at 8:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.