I have the following code:
import QuantLib as ql
import numpy as np
reference_date = ql.Date(25,11,2019)
ql.Settings.instance().evaluationDate = reference_date
dates_3M = [ql.Date(25,11,2019), ql.Date(27,2,2020), ql.Date(27,3,2020), ql.Date(27,4,2020)]
rates_3M = [0.003, 0.0032, 0.0031, 0.0029]
dates_ois = [ql.Date(25,11,2019), ql.Date(26,11,2019), ql.Date(4,12,2019), ql.Date(11,12,2019),
ql.Date(18,12,2019), ql.Date(27,12,2019), ql.Date(27,1,2020), ql.Date(27,2,2020),
ql.Date(27,3,2020), ql.Date(27,4,2020)]
rates_ois = [0.0034, 0.0033, 0.0032, 0.0032, 0.0031, 0.003, 0.0029, 0.0031, 0.003, 0.0031]
calendar = ql.TARGET()
curve_ois = ql.ZeroCurve(dates_ois, rates_ois, ql.Actual360(), calendar, ql.Linear(), ql.Compounded, ql.Annual)
curve_ois_handle = ql.YieldTermStructureHandle(curve_ois)
curve_3M = ql.ZeroCurve(dates_3M, rates_3M, ql.Actual360(), calendar, ql.Linear(), ql.Compounded, ql.Annual)
curve_3M_handle = ql.YieldTermStructureHandle(curve_3M)
startDate = reference_date + 2
endDate = calendar.advance(startDate, ql.Period('5M')
schedule = ql.Schedule(startDate, endDate, ql.Period(ql.Quarterly), calendar, ql.ModifiedFollowing, ql.ModifiedFollowing, ql.DateGeneration.Forward, False)
nom = 1000000
capRate = 0.01
cap = ql.Cap(ql.IborLeg([nominal], schedule, ql.Euribor3M(curve_3M_handle)), [capRate])
vola = ql.QuoteHandle(ql.SimpleQuote(0.3064))
engine = ql.BachelierCapFloorEngine(curve_ois_handle, vola)
cap.setPricingEngine(engine)
cap_npv = cap.NPV()
This gives me the output 9596.7536... Now I am trying
cap.impliedVolatility(9596.7536, curve_ois_handle, 0.2)
hoping I would get the original vola 0.3064. However, now I don't get the error as before, but the error "root not bracketed f[1e-07,4] -> [-9,596735e+03, -9.4199483+03]"
Now let's say instead I try the following:
tenors_vola = [ql.Period('2M'), ql.Period('4M'), ql.Period('6M'), ql.Period('8M')]
rates_vola = [0.16, 0.2, 0.22, 0.25]
strikes = [0,1]
rates_vola_new = [[item, item] for item in rates_vola]
vola_surf = ql.CapFloorTermVolSurface(2, calendar, ql.ModifiedFollowing, tenors_vola,
strikes, rates_vola_new)
tmp1 = ql.OptionletStripper1(vola_surf, ql.Euribor3M(curve_3M_handle), type=ql.Normal)
tmp2 = ql.StrippedOptionletAdapter(tmp1)
vola_handle = ql.OptionletVolatilityStructureHandle(tmp2)
engine = ql.BachelierCapFloorEngine(curve_ois_handle, vola_handle)
cap.setPricingEngine(engine)
print(cap.NPV())
I have vola date for different tenors but not for different strikes, so I assume my vola surface to be constant in the strike dimension. Now in this specific example I don't get this code to run because I get the error "not enough points to interpolate: at least 2 required, 1 provided." However in my actual code it works and there the only difference is, that my ois, euribor and vola data is much longer for many more years. But I cannot copy that data here. Could someone please adjust my code such that it is running, including in the end the
cap.impliedVolatility(price, curve_ois_handle, vola_guess)
And how would I interpret the resulting implied volatility in this case? Because the NPV is computed depending on all the volas in the curve. Would the result of cap.impliedVolatility(...) be some kind of average?
In your first example you're using the Bachelier engine, which means you're using normal volatility. When you're calling impliedVolatility
, though, it defaults to using lognormal (Black) volatility (you can see its declaration and its default parameters here). The inconsistency causes the error. If you specify that you want a normal implied volatility, as in
cap.impliedVolatility(cap_npv, curve_ois_handle, guess=0.2, type=ql.Normal)
you'll get back your input volatility.
In your second example, I'm not sure what should be adjusted since you say that it works with your data. In any case, the call to impliedVolatility
will give you the volatility that would give you the same price when used as a constant vol for all the caplets in the cap.