I have been working with the QuantLib Python package for some days now. Currently, I am working on calibrating a Hull White one-factor model for short rates. I am calibrating the model on the yield-curve and on swaption volatilities.
Given the current market circumstances, a large part of the yield curve is negative. This causes some issues with calibrating the Hull White one-factor model since the lognormal distribution cannot be negative. After some searching I found the possibility of specifying a shift. In the code below my SwaptionHelper initialization is shown including the shift.
Please note I am currently using a yield curve, however, when using a flatforward of -0.00478 I get similar results.
However, using the shift results in unrealistic output for a (a=0.0000389, sigma=0.0222).
I have been searching a lot for an answer, but cannot find decent information for situations with a negative rate. If anyone would be able to explain the usage of a shift or do a quick check on my code to see if anything is wrong would be so helpful.
helper = ql.SwaptionHelper(
ql.Period(int(maturity), ql.Years),
ql.Period(int(tenor), ql.Years),
volatility,
index,
fixedLegTenor,
fixedLegDayCounter,
floatingLegDayCounter,
term_structure,
ql.BlackCalibrationHelper.RelativePriceError,
ql.nullDouble(),
1.0,
ql.ShiftedLognormal,
0.05 #shift to make rates non-negative
)
Full code:
import csv
from QuantLib.QuantLib import SwaptionHelper
import matplotlib.pyplot as plt
import pandas as pd
import QuantLib as ql
def load_csv_input():
zero_curve = pd.read_csv('ZeroCurve.csv', delimiter=';')
dates = []
rates = []
for i in range(0,len(zero_curve)):
rates.append(float(zero_curve['Rate'][i]))
dates.append(ql.Date(int(zero_curve['Mat_Day'][i]),int(zero_curve['Mat_Month'][i]),int(zero_curve['Mat_Year'][i])))
return dates, rates
# Read the swaption volatilities from the csv file.
swaption_vols = pd.read_csv('SwaptionVol.csv', delimiter=';', index_col=0)
dates, rates = load_csv_input()
curve = ql.ZeroCurve(dates, rates, ql.Actual365Fixed())
term_structure = ql.YieldTermStructureHandle(curve)
model = ql.HullWhite(term_structure)
#engine = ql.TreeSwaptionEngine(model, 25)
engine = ql.JamshidianSwaptionEngine(model)
#engine = ql.G2SwaptionEngine(model, 10, 400)
index = ql.Euribor1Y(term_structure)
fixedLegTenor = ql.Period('1Y')
fixedLegDayCounter = ql.Actual360()
floatingLegDayCounter = ql.Actual360()
swaptions = []
ql.Settings.instance().evaluationDate = ql.Date(1, 10, 2020)
for maturity in swaption_vols.index:
for tenor in swaption_vols.columns:
volatility = ql.QuoteHandle(ql.SimpleQuote(swaption_vols.at[maturity,tenor]))
helper = ql.SwaptionHelper(
ql.Period(int(maturity), ql.Years),
ql.Period(int(tenor), ql.Years),
volatility,
index,
fixedLegTenor,
fixedLegDayCounter,
floatingLegDayCounter,
term_structure,
ql.BlackCalibrationHelper.RelativePriceError,
ql.nullDouble(),
1.0,
ql.ShiftedLognormal,
0.2 #shift to make rates non-negative
)
helper.setPricingEngine(engine)
swaptions.append(helper)
optimization_method = ql.LevenbergMarquardt(1.0e-8,1.0e-8,1.0e-8)
end_criteria = ql.EndCriteria(500000, 1000, 1e-6, 1e-8, 1e-8)
model.calibrate(swaptions, optimization_method, end_criteria)
params = model.params()
print(params)
Update
I have changed the volatility type from lognormal to normal, since the data contains normal volatilities. So this should be able to work with the negative values.
Now I am running into an issue where the calibration only works if I use a small selection of volatilities. My complete volatility matrix is 10x10 (1, 2, 3, 4, 5, 10, 15, 20, 25, and 30Y maturity and tenors). However, if I use more than the first 5x5 I get a "root not bracketed" error.
SwaptionVol.csv
is lognormal. Is that the case? Maybe it helps also if you could share the data. $\endgroup$