Long execution time when pricing Swaption out from ql.SabrSwaptionVolatilityCube()
Pricing Swaptions out from ql.SabrSwaptionVolatilityCube() works fine when using small set of expiries and underlyings. However when I use 22 expiries and 14 underlyings it took me 200 seconds. Basically I've tried to extract vol directly from built ql.SabrSwaptionVolatilityCube() using smileSection() and ql.BachelierSwaptionEngine(), both taking similar time of 200 seconds. Result looks correct but just it is taking too long. Please can expert advise am I missing something please?
Hi, can you post a minimal test case please?
Dear @kp9991-git , apologies I missed your reply. Please see below code. FYI, I get roughly 0.3s for pricing IRS underlying but 3 mins for pricing OIS underlying. Hope this info helps, or appreciate if you could point out which part I was implementing wrongly.
import QuantLib as ql
CAL = ql.TARGET()
ATM_NORM_VOLS = (
(0.0086, 0.00128, 0.00195, 0.00269, 0.00327, 0.00361, 0.00387,
0.00409, 0.00427, 0.00443, 0.00488, 0.00504, 0.00508, 0.00504),
(0.0092, 0.00134, 0.00197, 0.00264, 0.00319, 0.00352, 0.00383,
0.00402, 0.00419, 0.00431, 0.00478, 0.00499, 0.00507, 0.00503),
(0.00112, 0.00153, 0.00210, 0.00276, 0.00327, 0.00353, 0.00384,
0.00408, 0.00426, 0.00445, 0.00486, 0.00505, 0.00509, 0.00510),
(0.00129, 0.00171, 0.00226, 0.00288, 0.00335, 0.00360, 0.00388,
0.00410, 0.00430, 0.00446, 0.00487, 0.00506, 0.00511, 0.00510),
(0.00146, 0.00187, 0.00246, 0.00301, 0.00342, 0.00369, 0.00393,
0.00413, 0.00432, 0.00449, 0.00489, 0.00510, 0.00513, 0.00515),
(0.00165, 0.00209, 0.00263, 0.00313, 0.00350, 0.00376, 0.00400,
0.00420, 0.00437, 0.00453, 0.00488, 0.00509, 0.00514, 0.00517),
(0.00209, 0.00253, 0.00300, 0.00340, 0.00370, 0.00395, 0.00419,
0.00434, 0.00450, 0.00464, 0.00493, 0.00510, 0.00513, 0.00519),
(0.00251, 0.00289, 0.00332, 0.00362, 0.00392, 0.00412, 0.00432,
0.00447, 0.00460, 0.00473, 0.00496, 0.00510, 0.00513, 0.00516),
(0.00340, 0.00366, 0.00392, 0.00411, 0.00432, 0.00445, 0.00461,
0.00472, 0.00480, 0.00490, 0.00503, 0.00513, 0.00513, 0.00512),
(0.00403, 0.00418, 0.00436, 0.00449, 0.00461, 0.00471, 0.00482,
0.00492, 0.00499, 0.00505, 0.00512, 0.00513, 0.00509, 0.00507),
(0.00440, 0.00448, 0.00460, 0.00471, 0.00484, 0.00491, 0.00499,
0.00507, 0.00514, 0.00519, 0.00516, 0.00514, 0.00506, 0.00502),
(0.00496, 0.00497, 0.00504, 0.00512, 0.00518, 0.00522, 0.00526,
0.00529, 0.00533, 0.00538, 0.00526, 0.00517, 0.00504, 0.00496),
(0.00539, 0.00537, 0.00540, 0.00542, 0.00544, 0.00545, 0.00545,
0.00544, 0.00544, 0.00549, 0.00531, 0.00518, 0.00501, 0.00491),
(0.00540, 0.00537, 0.00538, 0.00537, 0.00535, 0.00536, 0.00535,
0.00533, 0.00535, 0.00537, 0.00514, 0.00498, 0.00479, 0.00466),
(0.00528, 0.00524, 0.00526, 0.00523, 0.00522, 0.00523, 0.00520,
0.00519, 0.00518, 0.00518, 0.00495, 0.00474, 0.00454, 0.00438),
(0.00514, 0.00512, 0.00513, 0.00510, 0.00508, 0.00507, 0.00503,
0.00499, 0.00498, 0.00497, 0.00476, 0.00453, 0.00431, 0.00414),
(0.00496, 0.00496, 0.00497, 0.00495, 0.00495, 0.00492, 0.00486,
0.00479, 0.00474, 0.00471, 0.00451, 0.00429, 0.00408, 0.00392))
ATM_NORM_VOL_OPT_TENORS = (ql.Period(1, ql.Months), ql.Period(2, ql.Months),
ql.Period(3, ql.Months), ql.Period(6, ql.Months),
ql.Period(9, ql.Months), ql.Period(1, ql.Years),
ql.Period(18, ql.Months), ql.Period(2, ql.Years),
ql.Period(3, ql.Years), ql.Period(4, ql.Years),
ql.Period(5, ql.Years), ql.Period(7, ql.Years),
ql.Period(10, ql.Years), ql.Period(15, ql.Years),
ql.Period(20, ql.Years), ql.Period(25, ql.Years),
ql.Period(30, ql.Years))
ATM_NORM_VOL_SWAP_TENORS = (ql.Period(1, ql.Years), ql.Period(2, ql.Years),
ql.Period(3, ql.Years), ql.Period(4, ql.Years),
ql.Period(5, ql.Years), ql.Period(6, ql.Years),
ql.Period(7, ql.Years), ql.Period(8, ql.Years),
ql.Period(9, ql.Years), ql.Period(10, ql.Years),
ql.Period(15, ql.Years), ql.Period(20, ql.Years),
ql.Period(25, ql.Years), ql.Period(30, ql.Years))
BETA = [
[0.40, 0.42, 0.45, 0.46, 0.48, 0.49, 0.50, 0.52, 0.52],
[0.40, 0.43, 0.45, 0.46, 0.48, 0.49, 0.50, 0.52, 0.52],
[0.40, 0.44, 0.46, 0.47, 0.49, 0.50, 0.51, 0.53, 0.53],
[0.42, 0.44, 0.47, 0.48, 0.50, 0.51, 0.52, 0.54, 0.54],
[0.43, 0.45, 0.48, 0.49, 0.51, 0.52, 0.53, 0.55, 0.55],
[0.44, 0.46, 0.48, 0.50, 0.52, 0.53, 0.54, 0.56, 0.56],
[0.45, 0.47, 0.49, 0.50, 0.52, 0.53, 0.54, 0.56, 0.56],
[0.46, 0.48, 0.50, 0.51, 0.52, 0.54, 0.55, 0.57, 0.57],
[0.47, 0.49, 0.51, 0.52, 0.53, 0.55, 0.56, 0.58, 0.58],
[0.48, 0.50, 0.52, 0.53, 0.54, 0.56, 0.57, 0.59, 0.59],
[0.49, 0.51, 0.53, 0.54, 0.55, 0.57, 0.58, 0.60, 0.60],
[0.50, 0.52, 0.54, 0.55, 0.56, 0.58, 0.59, 0.61, 0.61],
]
NU = [
[0.90, 0.80, 0.70, 0.60, 0.55, 0.50, 0.45, 0.40, 0.38],
[0.85, 0.75, 0.65, 0.58, 0.53, 0.48, 0.44, 0.39, 0.37],
[0.80, 0.72, 0.62, 0.55, 0.50, 0.46, 0.42, 0.38, 0.36],
[0.70, 0.65, 0.58, 0.52, 0.48, 0.44, 0.40, 0.36, 0.34],
[0.60, 0.56, 0.50, 0.46, 0.43, 0.40, 0.37, 0.34, 0.32],
[0.55, 0.52, 0.47, 0.44, 0.41, 0.38, 0.35, 0.33, 0.31],
[0.50, 0.48, 0.44, 0.41, 0.38, 0.36, 0.34, 0.31, 0.29],
[0.46, 0.44, 0.41, 0.39, 0.36, 0.34, 0.32, 0.30, 0.28],
[0.42, 0.40, 0.38, 0.36, 0.34, 0.32, 0.30, 0.28, 0.26],
[0.40, 0.38, 0.36, 0.34, 0.32, 0.30, 0.28, 0.26, 0.24],
[0.38, 0.36, 0.34, 0.33, 0.31, 0.29, 0.27, 0.25, 0.23],
[0.36, 0.34, 0.33, 0.32, 0.30, 0.28, 0.26, 0.24, 0.22]
]
RHO = [
[-0.60, -0.55, -0.50, -0.48, -0.45, -0.42, -0.40, -0.35, -0.33],
[-0.58, -0.54, -0.50, -0.47, -0.44, -0.41, -0.39, -0.34, -0.32],
[-0.55, -0.52, -0.48, -0.46, -0.43, -0.40, -0.38, -0.33, -0.31],
[-0.52, -0.50, -0.46, -0.44, -0.41, -0.38, -0.36, -0.32, -0.30],
[-0.50, -0.48, -0.45, -0.43, -0.40, -0.37, -0.35, -0.31, -0.29],
[-0.48, -0.46, -0.43, -0.41, -0.38, -0.36, -0.34, -0.30, -0.28],
[-0.45, -0.44, -0.41, -0.39, -0.36, -0.34, -0.33, -0.29, -0.27],
[-0.43, -0.42, -0.40, -0.38, -0.35, -0.33, -0.32, -0.28, -0.26],
[-0.40, -0.39, -0.37, -0.36, -0.33, -0.32, -0.30, -0.27, -0.25],
[-0.38, -0.37, -0.35, -0.34, -0.32, -0.31, -0.29, -0.26, -0.24],
[-0.36, -0.35, -0.33, -0.32, -0.30, -0.29, -0.27, -0.25, -0.23],
[-0.34, -0.33, -0.31, -0.30, -0.28, -0.27, -0.25, -0.23, -0.21]
]
SMILE_OPT_TENORS = (ql.Period(1, ql.Months),
ql.Period(3, ql.Months),
ql.Period(6, ql.Months),
ql.Period(1, ql.Years),
ql.Period(2, ql.Years),
ql.Period(3, ql.Years),
ql.Period(5, ql.Years),
ql.Period(7, ql.Years),
ql.Period(10, ql.Years),
ql.Period(15, ql.Years),
ql.Period(20, ql.Years),
ql.Period(30, ql.Years))
SMILE_SWAP_TENORS = (ql.Period(6, ql.Months),
ql.Period(1, ql.Years),
ql.Period(2, ql.Years),
ql.Period(3, ql.Years),
ql.Period(5, ql.Years),
ql.Period(7, ql.Years),
ql.Period(10, ql.Years),
ql.Period(20, ql.Years),
ql.Period(30, ql.Years))
ZERO_COUPON_DATA = (
(ql.Period(1, ql.Days), 0.013),
(ql.Period(1, ql.Years), 0.013),
(ql.Period(2, ql.Years), 0.015),
(ql.Period(3, ql.Years), 0.016),
(ql.Period(4, ql.Years), 0.017),
(ql.Period(5, ql.Years), 0.019),
(ql.Period(10, ql.Years), 0.021),
(ql.Period(15, ql.Years), 0.024),
(ql.Period(20, ql.Years), 0.026),
(ql.Period(30, ql.Years), 0.029))
NORM_VOL_MATRIX = ql.SwaptionVolatilityMatrix(
CAL,
ql.ModifiedFollowing,
ATM_NORM_VOL_OPT_TENORS,
ATM_NORM_VOL_SWAP_TENORS,
ql.Matrix(ATM_NORM_VOLS),
ql.Actual365Fixed(),
False,
ql.Normal)
def build_estr_swap_idx(
projection_curve_handle):
return ql.OvernightIndexedSwapIndex("ESTR", ql.Period(1, ql.Years), 2, ql.EURCurrency(),
projection_curve_handle)
def build_nominal_term_structure(valuation_date, nominal_quotes):
dates, rates = zip(*[(CAL.advance(valuation_date, x[0]), x[1])
for x in nominal_quotes])
crv = ql.ZeroCurve(dates, rates, ql.Actual365Fixed())
crv.enableExtrapolation()
return crv
def sabr_parameters_guess(number_of_options, number_of_swaps, beta, nu, rho):
n_elements = number_of_options * number_of_swaps
guess = n_elements * [0]
for i in range(number_of_options):
for j in range(number_of_swaps):
guess[i * number_of_swaps + j] = [
ql.QuoteHandle(ql.SimpleQuote(0.1)),
ql.QuoteHandle(ql.SimpleQuote(beta[i][j])),
ql.QuoteHandle(ql.SimpleQuote(nu[i][j])),
ql.QuoteHandle(ql.SimpleQuote(rho[i][j]))]
return guess
def build_sabr_swaption_cube(
volatility_matrix,
spread_opt_tenors,
spread_swap_tenors,
strike_spreads,
vol_spreads,
beta,
nu,
rho,
swap_index_base,
short_swap_index_base=None,
vega_weighted_smile_fit=False,
is_parameter_fixed=(False, True, True, True),
is_atm_calibrated=True):
v_spreads = [[ql.makeQuoteHandle(v) for v in row]
for row in vol_spreads]
guess = sabr_parameters_guess(
len(spread_opt_tenors), len(spread_swap_tenors), beta, nu, rho)
cube = ql.SabrSwaptionVolatilityCube(
ql.SwaptionVolatilityStructureHandle(volatility_matrix),
spread_opt_tenors,
spread_swap_tenors,
strike_spreads,
v_spreads,
swap_index_base,
short_swap_index_base if short_swap_index_base else swap_index_base,
vega_weighted_smile_fit,
guess,
is_parameter_fixed,
is_atm_calibrated)
cube.enableExtrapolation()
return cube
class SwaptionVolatilityCube:
def __init__(self):
self.today = CAL.adjust(ql.Date.todaysDate())
ql.Settings.instance().evaluationDate = self.today
curve_handle = ql.RelinkableYieldTermStructureHandle()
curve = build_nominal_term_structure(self.today, ZERO_COUPON_DATA)
curve_handle.linkTo(curve)
self.yts = curve_handle
self.idx = ql.Estr(curve_handle)
self.swap_idx = build_estr_swap_idx(self.idx)
self.swap_engine = ql.DiscountingSwapEngine(curve_handle)
def tearDown(self):
ql.Settings.instance().evaluationDate = ql.Date()
def _calculate_prem(
self, cube, vol_type):
opt_tenor = ql.Period(1, ql.Years)
swap_tenor = ql.Period(10, ql.Years)
exercise_date = cube.optionDateFromTenor(opt_tenor)
fair_rate = cube.atmStrike(exercise_date, swap_tenor)
vol = cube.volatility(exercise_date, swap_tenor, vol_type)
start_date = CAL.advance(exercise_date, ql.Period(2, ql.Days))
underlying = ql.MakeOIS(
swap_tenor,
self.idx,
fair_rate,
effectiveDate=start_date,
terminationDate=None,
paymentFrequency=ql.Annual,
telescopicValueDates=True,
dateGenerationRule=ql.DateGeneration.Backward,
receiveFixed=True,
)
underlying.setPricingEngine(self.swap_engine)
swaption = ql.Swaption(underlying, ql.EuropeanExercise(exercise_date))
swaption.setPricingEngine(ql.BachelierSwaptionEngine(self.yts, ql.SwaptionVolatilityStructureHandle(cube)))
premium = swaption.forwardPrice()
return opt_tenor, swap_tenor, exercise_date, fair_rate, vol, premium
def _test_premium(self):
NORM_VOL_SPREADS = [[0.0] for _ in range(len(SMILE_OPT_TENORS) * len(SMILE_SWAP_TENORS))]
sabr_cube = build_sabr_swaption_cube(
NORM_VOL_MATRIX,
SMILE_OPT_TENORS,
SMILE_SWAP_TENORS,
[0.0],
NORM_VOL_SPREADS,
BETA,
NU,
RHO,
self.swap_idx)
print(self._calculate_prem(sabr_cube, ql.Normal))
if __name__ == '__main__':
SwaptionVolatilityCube()._test_premium()
@kp9991-git @lballabio do you think if we have a function called MakeOISwaption that has similar feature like telescopicValueDates will reduce the calibration time please?
I don't think it has anything to do with the swap schedule. If you call the function _test_premium() twice, the second time it's fast. It's just that on the first call it will calibrate the SABR cube. One possible optimisation that I see is to calibrate the cube partially on demand. Currently when you call it for the first time, it will calibrate all swap tenors and expiries. If you have a large cube but only need a handful of data points, this may not be necessary, particularly if some smiles take a while to converge. But perhaps you can talk to us about your use case? Are you building a cube to price a small number of swaptions, or is it an object that will need to be able to price any swaptions? If the latter, once built and calibrated, it should be quick. But if you are building it, pricing a small number of swaptions, then killing it, then yes there is a calibration overhead.
Thanks for your input @kp9991-git . I would need to use it for both use case. Yea I noticed the subsequent call is quick. But the problem is if one of the vols moved, the cube will recalibrate as it is first call. Any workaround on this would be much appreciated.
Yes you hit a nail on the head with this comment. An EOD vol surface is different from a real-time vol surface. It sounds like you are using it for real-time pricing, right? In the EOD case you can live with some slowness because you build the object once. But when live vols feed into a vol surface, then there are two problems, or even there: how to build minimally only what's changed; how to implement throttling; how to deal with bid/offer. Anyway, in your case it's the first problem above. How is your application designed with respect to the vols? Do you have a live feed from BBG or something like that, then when you receive a tick, or if a user clicks a button, you refresh vols, feed into the vol cube, rebuild it, reprice a swaption, right? If that's the case, then yes I think calculating SABR smile slices on demand will help address this issue
Yes, the use case is exactly what you mentioned - rebuild and reprice swaptions. I will try with your suggestion. Much appreciated on your inputs @kp9991-git .
This issue was automatically marked as stale because it has been open 60 days with no activity. Remove stale label or comment, or this will be closed in two weeks.