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 )
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)
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.