I am running a compiled from source SWIG python 1.16 version of QuantLib.
I have been trying to calibrate a heston model following this example.
I am only using the QL calibration at the moment to test it out before trying others.
I need time dependent parameters so I am using PiecewiseTimeDependentHestonModel
.
Here is the relevant portion of my code.
Helper functions :
def tenor2date(s, base_date=None,ql=False):
# returns a date from a tenor and a base date
if base_date is None:
base_date = datetime.today()
num = float(s[:-1])
period = s[-1].upper()
if period == "Y":
return_date = base_date + relativedelta(years=num)
elif period == "M":
return_date = base_date + relativedelta(months=num)
elif period == "W":
return_date = base_date + relativedelta(weeks=num)
elif period == "D":
return_date = base_date + relativedelta(days=num)
else:
return_date = base_date
if ql:
return Date(return_date.strftime("%F"),"yyyy-mm-dd")
else:
return return_date
def setup_model(yield_ts, dividend_ts, spot, times,init_condition=(0.02, 0.2, 0.5, 0.1, 0.01)):
theta, kappa, sigma, rho, v0 = init_condition
model = ql.PiecewiseTimeDependentHestonModel(yield_ts, dividend_ts, ql.QuoteHandle(ql.SimpleQuote(spot)), v0, ql.Parameter(), ql.Parameter(),
ql.Parameter(), ql.Parameter(), ql.TimeGrid(times))
engine = ql.AnalyticPTDHestonEngine(model)
return model, engine
def setup_helpers(engine, vol_surface, ref_date, spot, yield_ts, dividend_ts):
heston_helpers = []
grid_data = []
for tenor in vol_surface:
expiry_date = tenor2date(tenor, datetime(ref_date.year(), ref_date.month(), ref_date.dayOfMonth()), True)
t = (expiry_date - ref_date)
print(f"{tenor} : {t / 365}")
p = ql.Period(t, ql.Days)
for strike, vol in zip(vol_surface[tenor]["strikes"], vol_surface[tenor]["volatilities"]):
print((strike, vol))
helper = ql.HestonModelHelper(p, calendar, spot, strike, ql.QuoteHandle(ql.SimpleQuote(vol / 100)), yield_ts, dividend_ts)
helper.setPricingEngine(engine)
heston_helpers.append(helper)
grid_data.append((expiry_date, strike))
return heston_helpers, grid_data
Market data :
vol_surface = {'12M': {'strikes': [1.0030154025220293, 0.9840808634190958, 0.9589657270688433, 0.9408279805370683, 0.9174122318462831, 0.8963792435025802, 0.8787138822765832, 0.8538712672800733, 0.8355036501980958], 'volatilities': [6.7175, 6.5, 6.24375, 6.145, 6.195, 6.425, 6.72125, 7.21, 7.5625], 'forward': 0.919323}, '1M': {'strikes': [0.9369864196692815, 0.9324482223892986, 0.9261255003380027, 0.9213195223581382, 0.9150244003650484, 0.9088253068972495, 0.9038936313900919, 0.897245676067657, 0.8924388848562849], 'volatilities': [6.3475, 6.23375, 6.1075, 6.06, 6.09, 6.215, 6.3725, 6.63125, 6.8225], 'forward': 0.915169}, '1W': {'strikes': [0.9258809998009043, 0.9236526412979602, 0.920487656155217, 0.9180490618315417, 0.9148370595017086, 0.9116231311263782, 0.9090950947170667, 0.9057357691404444, 0.9033397443834199], 'volatilities': [6.7175, 6.63375, 6.53625, 6.5025, 6.53, 6.6425, 6.77875, 6.99625, 7.1525], 'forward': 0.914875}, '2M': {'strikes': [0.9456173410343232, 0.9392447942175677, 0.9304717860942596, 0.9238709412876663, 0.9152350197527926, 0.9068086964842931, 0.9000335970840222, 0.8908167643473346, 0.884110721680849], 'volatilities': [6.1575, 6.02625, 5.8825, 5.8325, 5.87, 6.0175, 6.1975, 6.48875, 6.7025], 'forward': 0.915506}, '3M': {'strikes': [0.9533543407827232, 0.945357456067501, 0.9343646071178692, 0.9261489737826977, 0.9154251386183144, 0.9050707394248945, 0.8966770979707913, 0.8851907303568785, 0.876803402158318], 'volatilities': [6.23, 6.09125, 5.93, 5.8725, 5.915, 6.0775, 6.28, 6.60375, 6.84], 'forward': 0.915841}, '4M': {'strikes': [0.9603950279333742, 0.9509237742916833, 0.9379657828957041, 0.928295643018581, 0.9156834006905108, 0.9036539552069216, 0.8938804229269658, 0.8804999196762403, 0.870730837142799], 'volatilities': [6.3175, 6.17125, 6.005, 5.94375, 5.985, 6.15125, 6.36, 6.69375, 6.9375], 'forward': 0.916255}, '6M': {'strikes': [0.9719887962018352, 0.9599837798239937, 0.943700651576822, 0.9316544554849711, 0.9159768970939797, 0.9013018796367052, 0.8892904835162911, 0.8727031923006017, 0.8605425787295339], 'volatilities': [6.3925, 6.22875, 6.04125, 5.9725, 6.01, 6.1875, 6.41375, 6.78625, 7.0575], 'forward': 0.916851}, '9M': {'strikes': [0.9879332225745909, 0.9724112749400833, 0.951642771321364, 0.936450663789222, 0.9167103888580063, 0.8985852649047051, 0.8835274087791912, 0.8625837214139542, 0.8472311260811375], 'volatilities': [6.54, 6.34875, 6.1325, 6.055, 6.11, 6.32, 6.5875, 7.01625, 7.32], 'forward': 0.918086}}
spotDates = [ql.Date(1,7,2019), ql.Date(8,7,2019), ql.Date(1,8,2019), ql.Date(1,9,2019), ql.Date(1,10,2019), ql.Date(1,11,2019), ql.Date(1,1,2020), ql.Date(1,4,2020), ql.Date(1,7,2020)]
spotRates = [0.9148, 0.914875, 0.915169, 0.915506, 0.915841, 0.916255, 0.916851, 0.918086, 0.919323]
udl_value = 0.9148
todaysDate = ql.Date("2019-07-01","yyyy-mm-dd")
settlementDate = ql.Date("2019-07-03","yyyy-mm-dd")
and the script itself:
ql.Settings.instance().evaluationDate = todaysDate
dayCounter = ql.Actual365Fixed()
interpolation = ql.Linear()
compounding = ql.Compounded
compoundingFrequency = ql.Annual
times = [(x - spotDates[0]) / 365 for x in spotDates][1:]
discountFactors = [-log(x / spotRates[0]) / (times[i]) for i, x in enumerate(spotRates[1:])]
fwdCurve = ql.ZeroCurve(spotDates, [0] + discountFactors, dayCounter, calendar, interpolation, compounding, compoundingFrequency)
fwdCurveHandle = ql.YieldTermStructureHandle(fwdCurve)
dividendCurveHandle = ql.YieldTermStructureHandle(ql.FlatForward(settlementDate, 0, dayCounter))
hestonModel, hestonEngine = setup_model(fwdCurveHandle, dividendCurveHandle, udl_value, times)
heston_helpers, grid_data = setup_helpers(hestonEngine, vol_surface, todaysDate, udl_value, fwdCurveHandle, dividendCurveHandle)
lm = ql.LevenbergMarquardt(1e-8, 1e-8, 1e-8)
hestonModel.calibrate(heston_helpers, lm, ql.EndCriteria(500, 300, 1.0e-8, 1.0e-8, 1.0e-8))
When I run the last line I get the following error message :
RuntimeError: time (1.42466) is past max curve time (1.00274)
I do not understand how it can try to price things beyond 1Y as both the helpers and the forwards curve are defined on the same set of dates.
In case it helps someone, posting here the answer I got from the quantlb mailing :
specifying the maturity in days
t = (expiry_date - ref_date)
print(f"{tenor} : {t / 365}")
p = ql.Period(t, ql.Days)
might have an counterintuitive effect here as the specified calendar is used to calculate the real expiry date. If the calendar is e.g. ql.UnitedStates then this takes weekends and holidays into consideration,
ql.UnitedStates().advance(ql.Date(1,1,2019),ql.Period(365, ql.Days))
=> Date(12,6,2020)
whereas
ql.NullCalendar().advance(ql.Date(1,1,2019),ql.Period(365, ql.Days))
=> Date(1,1,2020)
hence I guess the interest rate curve is not long enough and throws the error message.
So the fix is to make sure to use ql.NullCalendar()
accross.