diff options
Diffstat (limited to 'python/analytics/cms_spread.py')
| -rw-r--r-- | python/analytics/cms_spread.py | 329 |
1 files changed, 228 insertions, 101 deletions
diff --git a/python/analytics/cms_spread.py b/python/analytics/cms_spread.py index e7d68cdc..b41aa07b 100644 --- a/python/analytics/cms_spread.py +++ b/python/analytics/cms_spread.py @@ -8,20 +8,29 @@ from math import exp, sqrt, log, pi from .black import bachelier, cnd_erf from numba import cfunc, types, float64, vectorize from quantlib.time.api import ( - Date, Period, Days, Months, Years, UnitedStates, Actual365Fixed, Following, - ModifiedFollowing) + Date, + Period, + Days, + Months, + Years, + UnitedStates, + Actual365Fixed, + Following, + ModifiedFollowing, +) from quantlib.cashflows.cms_coupon import CmsCoupon -from quantlib.cashflows.conundrum_pricer import ( - AnalyticHaganPricer, YieldCurveModel) +from quantlib.cashflows.conundrum_pricer import AnalyticHaganPricer, YieldCurveModel from quantlib.termstructures.yields.api import YieldTermStructure from quantlib.indexes.swap.usd_libor_swap import UsdLiborSwapIsdaFixAm from quantlib.experimental.coupons.swap_spread_index import SwapSpreadIndex -from quantlib.experimental.coupons.lognormal_cmsspread_pricer import \ - LognormalCmsSpreadPricer -from quantlib.experimental.coupons.cms_spread_coupon import \ - CappedFlooredCmsSpreadCoupon +from quantlib.experimental.coupons.lognormal_cmsspread_pricer import ( + LognormalCmsSpreadPricer, +) +from quantlib.experimental.coupons.cms_spread_coupon import CappedFlooredCmsSpreadCoupon from quantlib.termstructures.volatility.api import ( - VolatilityType, SwaptionVolatilityMatrix) + VolatilityType, + SwaptionVolatilityMatrix, +) from quantlib.cashflows.linear_tsr_pricer import LinearTsrPricer from quantlib.quotes import SimpleQuote @@ -35,8 +44,24 @@ from .db import dawn_engine, serenitas_pool __all__ = ["CmsSpread"] -@vectorize([float64(float64, float64, float64, float64, float64, float64, float64, - float64, float64)], cache=True, nopython=True) + +@vectorize( + [ + float64( + float64, + float64, + float64, + float64, + float64, + float64, + float64, + float64, + float64, + ) + ], + cache=True, + nopython=True, +) def h_call(z, K, S1, S2, mu_x, mu_y, sigma_x, sigma_y, rho): # conditionned on S2, integral wrt S1 # z = (y - mu_y) / sigma_y @@ -49,8 +74,24 @@ def h_call(z, K, S1, S2, mu_x, mu_y, sigma_x, sigma_y, rho): x = (u1 - u2) / v return 0.5 * (S1 * exp(u1 + 0.5 * v2) * cnd_erf(x + v) - Ktilde * cnd_erf(x)) -@vectorize([float64(float64, float64, float64, float64, float64, float64, float64, - float64, float64)], cache=True, nopython=True) + +@vectorize( + [ + float64( + float64, + float64, + float64, + float64, + float64, + float64, + float64, + float64, + float64, + ) + ], + cache=True, + nopython=True, +) def h_put(z, K, S1, S2, mu_x, mu_y, sigma_x, sigma_y, rho): # z = (y - mu_y) / sigma_y u1 = mu_x + rho * sigma_x * z @@ -62,11 +103,13 @@ def h_put(z, K, S1, S2, mu_x, mu_y, sigma_x, sigma_y, rho): x = (u2 - u1) / v return 0.5 * (Ktilde * cnd_erf(x) - S1 * exp(u1 + 0.5 * v2) * cnd_erf(x - v)) + _sig = types.double(types.intc, types.CPointer(types.double)) + @cfunc(_sig, cache=True, nopython=True) def _h1(n, args): - #z = (y - mu_y) / sigma_y + # z = (y - mu_y) / sigma_y z = args[0] K = args[1] S1 = args[2] @@ -83,14 +126,22 @@ def _h1(n, args): v = sigma_x * sqrt(1 - rho * rho) v2 = sigma_x * sigma_x * (1 - rho * rho) x = (u1 - u2) / v - return 0.5 * (S1 * exp(u1 + 0.5 * v2) * cnd_erf(x + v) - Ktilde * cnd_erf(x)) * exp(-0.5 * z * z) + return ( + 0.5 + * (S1 * exp(u1 + 0.5 * v2) * cnd_erf(x + v) - Ktilde * cnd_erf(x)) + * exp(-0.5 * z * z) + ) + _call_integrand = LowLevelCallable(_h1.ctypes) + def get_fixings(conn, tenor1, tenor2, fixing_date=None): if fixing_date: - sql_str = f'SELECT "{tenor1}y" ,"{tenor2}y" FROM USD_swap_fixings ' \ - 'WHERE fixing_date=%s' + sql_str = ( + f'SELECT "{tenor1}y" ,"{tenor2}y" FROM USD_swap_fixings ' + "WHERE fixing_date=%s" + ) with conn.cursor() as c: c.execute(sql_str, (fixing_date,)) try: @@ -98,8 +149,10 @@ def get_fixings(conn, tenor1, tenor2, fixing_date=None): except StopIteration: raise RuntimeError(f"no fixings available for date {fixing_date}") else: - sql_str = f'SELECT fixing_date, "{tenor1}y" ,"{tenor2}y" FROM USD_swap_fixings ' \ - 'ORDER BY fixing_date DESC LIMIT 1' + sql_str = ( + f'SELECT fixing_date, "{tenor1}y" ,"{tenor2}y" FROM USD_swap_fixings ' + "ORDER BY fixing_date DESC LIMIT 1" + ) with conn.cursor() as c: c.execute(sql_str, fixing_date) fixing_date, fixing1, fixing2 = next(c) @@ -118,8 +171,9 @@ def build_spread_index(tenor1, tenor2): return spread_index, yc -def get_swaption_vol_data(source="ICPL", vol_type=VolatilityType.ShiftedLognormal, - date=None): +def get_swaption_vol_data( + source="ICPL", vol_type=VolatilityType.ShiftedLognormal, date=None +): if vol_type == VolatilityType.Normal: table_name = "swaption_normal_vol" else: @@ -136,12 +190,12 @@ def get_swaption_vol_data(source="ICPL", vol_type=VolatilityType.ShiftedLognorma c.execute(sql_str, params) surf_data = next(c) serenitas_pool.putconn(conn) - return surf_data[0], np.array(surf_data[1:-1], order='F', dtype='float64').T + return surf_data[0], np.array(surf_data[1:-1], order="F", dtype="float64").T def get_swaption_vol_surface(date, vol_type): date, surf, _ = get_swaption_vol_data(date=date, vol_type=vol_type) - tenors = [1/12, 0.25, 0.5, 0.75] + list(range(1, 11)) + [15., 20., 25., 30.] + tenors = [1 / 12, 0.25, 0.5, 0.75] + list(range(1, 11)) + [15.0, 20.0, 25.0, 30.0] return RectBivariateSpline(tenors, tenors[-14:], surf) @@ -150,22 +204,34 @@ def get_swaption_vol_matrix(date, data, vol_type=VolatilityType.ShiftedLognormal calendar = UnitedStates() data = np.delete(data, 3, axis=0) / 100 m = Matrix.from_ndarray(data) - option_tenors = [Period(i, Months) for i in [1, 3, 6]] + \ - [Period(i, Years) for i in range(1, 11)] + \ - [Period(i, Years) for i in [15, 20, 25, 30]] + option_tenors = ( + [Period(i, Months) for i in [1, 3, 6]] + + [Period(i, Years) for i in range(1, 11)] + + [Period(i, Years) for i in [15, 20, 25, 30]] + ) swap_tenors = option_tenors[-14:] - return (SwaptionVolatilityMatrix(calendar, - Following, - option_tenors, - swap_tenors, - m, - Actual365Fixed(), - vol_type=vol_type)) + return SwaptionVolatilityMatrix( + calendar, + Following, + option_tenors, + swap_tenors, + m, + Actual365Fixed(), + vol_type=vol_type, + ) -def quantlib_model(date, spread_index, yc, cap, rho, maturity, mean_rev=0., - vol_type=VolatilityType.ShiftedLognormal, - notional=300_000_000): +def quantlib_model( + date, + spread_index, + yc, + cap, + rho, + maturity, + mean_rev=0.0, + vol_type=VolatilityType.ShiftedLognormal, + notional=300_000_000, +): date, surf = get_swaption_vol_data(date=date, vol_type=vol_type) atm_vol = get_swaption_vol_matrix(date, surf, vol_type) pricer = LinearTsrPricer(atm_vol, SimpleQuote(mean_rev), yc) @@ -184,11 +250,18 @@ def quantlib_model(date, spread_index, yc, cap, rho, maturity, mean_rev=0., # where $N$ is the notional, $T$ is the accrual time, $L$ is the floating rate, # $a$ is its gearing, $b$ is the spread, and $F$ the strike capped_floored_cms_spread_coupon = CappedFlooredCmsSpreadCoupon( - pay_date, notional, start_date, end_date, - spread_index.fixing_days, spread_index, 1., -cap, - floor=0., + pay_date, + notional, + start_date, + end_date, + spread_index.fixing_days, + spread_index, + 1.0, + -cap, + floor=0.0, day_counter=Actual365Fixed(), - is_in_arrears=True) + is_in_arrears=True, + ) capped_floored_cms_spread_coupon.set_pricer(cmsspread_pricer) return capped_floored_cms_spread_coupon @@ -196,12 +269,13 @@ def quantlib_model(date, spread_index, yc, cap, rho, maturity, mean_rev=0., def plot_surf(surf, tenors): xx, yy = np.meshgrid(tenors, tenors[-14:]) fig = plt.figure() - ax = fig.gca(projection='3d') + ax = fig.gca(projection="3d") ax.plot_surface(xx, yy, surf.ev(xx, yy)) -def globeop_model(date, spread_index, yc, strike, rho, maturity, - vol_type=VolatilityType.Normal): +def globeop_model( + date, spread_index, yc, strike, rho, maturity, vol_type=VolatilityType.Normal +): """ price cap spread option without convexity adjustment vol_type Normal is the only supported one at the moment""" @@ -212,37 +286,42 @@ def globeop_model(date, spread_index, yc, strike, rho, maturity, atm_vol = get_swaption_vol_matrix(date, surf, vol_type=vol_type) d = Date.from_datetime(date) T = Actual365Fixed().year_fraction(d, maturity) - vol1 = atm_vol.volatility(maturity, spread_index.swap_index1.tenor, 0.) - vol2 = atm_vol.volatility(maturity, spread_index.swap_index2.tenor, 0.) - vol_spread = sqrt(vol1**2 + vol2**2 - 2 * rho * vol1 * vol2) + vol1 = atm_vol.volatility(maturity, spread_index.swap_index1.tenor, 0.0) + vol2 = atm_vol.volatility(maturity, spread_index.swap_index2.tenor, 0.0) + vol_spread = sqrt(vol1 ** 2 + vol2 ** 2 - 2 * rho * vol1 * vol2) # normal vol is not scale independent and is computed in percent terms, so # we scale everything by 100. return 0.01 * yc.discount(T) * bachelier(forward * 100, strike * 100, T, vol_spread) -def get_cms_coupons(trade_date, notional, option_tenor, spread_index, - fixing_days=2): + +def get_cms_coupons(trade_date, notional, option_tenor, spread_index, fixing_days=2): maturity = Date.from_datetime(trade_date) + option_tenor fixing_date = spread_index.fixing_calendar.adjust(maturity, ModifiedFollowing) payment_date = spread_index.fixing_calendar.advance(fixing_date, fixing_days, Days) accrued_end_date = payment_date accrued_start_date = accrued_end_date - Period(1, Years) - cms_beta = CmsCoupon(payment_date, - notional, - start_date=accrued_start_date, - end_date=accrued_end_date, - fixing_days=fixing_days, - index=spread_index.swap_index2, - is_in_arrears=True) + cms_beta = CmsCoupon( + payment_date, + notional, + start_date=accrued_start_date, + end_date=accrued_end_date, + fixing_days=fixing_days, + index=spread_index.swap_index2, + is_in_arrears=True, + ) - cms_gamma = CmsCoupon(payment_date, - notional, - start_date=accrued_start_date, - end_date=accrued_end_date, - fixing_days=fixing_days, - index=spread_index.swap_index1, - is_in_arrears=True) + cms_gamma = CmsCoupon( + payment_date, + notional, + start_date=accrued_start_date, + end_date=accrued_end_date, + fixing_days=fixing_days, + index=spread_index.swap_index1, + is_in_arrears=True, + ) return cms_beta, cms_gamma + def get_params(cms_beta, cms_gamma, atm_vol): s_gamma = cms_gamma.index_fixing s_beta = cms_beta.index_fixing @@ -251,20 +330,35 @@ def get_params(cms_beta, cms_gamma, atm_vol): T_alpha = atm_vol.time_from_reference(cms_beta.fixing_date) mu_beta = 1 / T_alpha * log(adjusted_beta / s_beta) mu_gamma = 1 / T_alpha * log(adjusted_gamma / s_gamma) - vol_gamma = atm_vol.volatility(cms_gamma.fixing_date, cms_gamma.swap_index.tenor, s_gamma) - vol_beta = atm_vol.volatility(cms_beta.fixing_date, cms_beta.swap_index.tenor, s_beta) + vol_gamma = atm_vol.volatility( + cms_gamma.fixing_date, cms_gamma.swap_index.tenor, s_gamma + ) + vol_beta = atm_vol.volatility( + cms_beta.fixing_date, cms_beta.swap_index.tenor, s_beta + ) mu_x = (mu_gamma - 0.5 * vol_gamma ** 2) * T_alpha mu_y = (mu_beta - 0.5 * vol_beta ** 2) * T_alpha sigma_x = vol_gamma * sqrt(T_alpha) sigma_y = vol_beta * sqrt(T_alpha) - return (s_gamma, s_beta , mu_x, mu_y, sigma_x, sigma_y) + return (s_gamma, s_beta, mu_x, mu_y, sigma_x, sigma_y) class CmsSpread: - def __init__(self, maturity, tenor1, tenor2, strike, option_tenor=None, - value_date=datetime.date.today(), notional=100_000_000, - conditional1=None, conditional2=None, fixing_days=2, corr=0.8, - mean_reversion=0.1): + def __init__( + self, + maturity, + tenor1, + tenor2, + strike, + option_tenor=None, + value_date=datetime.date.today(), + notional=100_000_000, + conditional1=None, + conditional2=None, + fixing_days=2, + corr=0.8, + mean_reversion=0.1, + ): """ tenor1 < tenor2""" self._value_date = value_date if maturity is None: @@ -281,27 +375,34 @@ class CmsSpread: self.strike = strike self.notional = notional self.fixing_days = 2 - self.cms1 = CmsCoupon(payment_date, - self.notional, - start_date=accrued_start_date, - end_date=accrued_end_date, - fixing_days=fixing_days, - index=spread_index.swap_index2, - is_in_arrears=True) + self.cms1 = CmsCoupon( + payment_date, + self.notional, + start_date=accrued_start_date, + end_date=accrued_end_date, + fixing_days=fixing_days, + index=spread_index.swap_index2, + is_in_arrears=True, + ) - self.cms2 = CmsCoupon(payment_date, - notional, - start_date=accrued_start_date, - end_date=accrued_end_date, - fixing_days=fixing_days, - index=spread_index.swap_index1, - is_in_arrears=True) - date, surf = get_swaption_vol_data(date=value_date, - vol_type=VolatilityType.ShiftedLognormal) + self.cms2 = CmsCoupon( + payment_date, + notional, + start_date=accrued_start_date, + end_date=accrued_end_date, + fixing_days=fixing_days, + index=spread_index.swap_index1, + is_in_arrears=True, + ) + date, surf = get_swaption_vol_data( + date=value_date, vol_type=VolatilityType.ShiftedLognormal + ) atm_vol = get_swaption_vol_matrix(value_date, surf) self._corr = SimpleQuote(corr) self._μ = SimpleQuote(mean_reversion) - self._cms_pricer = AnalyticHaganPricer(atm_vol, YieldCurveModel.Standard, self._μ) + self._cms_pricer = AnalyticHaganPricer( + atm_vol, YieldCurveModel.Standard, self._μ + ) self.cms1.set_pricer(self._cms_pricer) self.cms2.set_pricer(self._cms_pricer) self._params = get_params(self.cms1, self.cms2, atm_vol) @@ -311,20 +412,35 @@ class CmsSpread: @staticmethod def from_tradeid(trade_id): - rec = dawn_engine.execute("SELECT " - "amount, expiration_date, floating_rate_index, strike, trade_date " - "FROM capfloors WHERE id = %s", (trade_id,)) + rec = dawn_engine.execute( + "SELECT " + "amount, expiration_date, floating_rate_index, strike, trade_date " + "FROM capfloors WHERE id = %s", + (trade_id,), + ) r = rec.fetchone() m = re.match(r"USD(\d{1,2})-(\d{1,2})CMS", r.floating_rate_index) if m: tenor2, tenor1 = map(int, m.groups()) if trade_id == 3: - instance = CmsSpread(r.expiration_date, tenor1, tenor2, r.strike * 0.01, - value_date=r.trade_date, notional=r.amount, - conditional1=0.025) + instance = CmsSpread( + r.expiration_date, + tenor1, + tenor2, + r.strike * 0.01, + value_date=r.trade_date, + notional=r.amount, + conditional1=0.025, + ) else: - instance = CmsSpread(r.expiration_date, tenor1, tenor2, r.strike * 0.01, - value_date=r.trade_date, notional=r.amount) + instance = CmsSpread( + r.expiration_date, + tenor1, + tenor2, + r.strike * 0.01, + value_date=r.trade_date, + notional=r.amount, + ) return instance @property @@ -343,8 +459,9 @@ class CmsSpread: def value_date(self, d: pd.Timestamp): self._value_date = d self.yc.link_to(YC(evaluation_date=d, extrapolation=True)) - date, surf = get_swaption_vol_data(date=d, - vol_type=VolatilityType.ShiftedLognormal) + date, surf = get_swaption_vol_data( + date=d, vol_type=VolatilityType.ShiftedLognormal + ) atm_vol = get_swaption_vol_matrix(d, surf) self._cms_pricer.swaption_volatility = atm_vol self._params = get_params(self.cms1, self.cms2, atm_vol) @@ -354,10 +471,20 @@ class CmsSpread: args = (self.strike, *self._params, self.corr) norm_const = 1 / sqrt(2 * pi) if self.conditional1 is not None: - bound = (log(self.conditional1 / self._params[1]) - self._params[3]) / self._params[-1] + bound = ( + log(self.conditional1 / self._params[1]) - self._params[3] + ) / self._params[-1] val, _ = quad(_call_integrand, -np.inf, bound, args=args) - return self.notional * norm_const * val * self.yc.discount(self.cms1.fixing_date) + return ( + self.notional + * norm_const + * val + * self.yc.discount(self.cms1.fixing_date) + ) else: - return self.notional * norm_const * \ - np.dot(self._w, h_call(self._x, *args)) * \ - self.yc.discount(self.cms1.fixing_date) + return ( + self.notional + * norm_const + * np.dot(self._w, h_call(self._x, *args)) + * self.yc.discount(self.cms1.fixing_date) + ) |
