aboutsummaryrefslogtreecommitdiffstats
path: root/python/analytics/cms_spread.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/analytics/cms_spread.py')
-rw-r--r--python/analytics/cms_spread.py402
1 files changed, 0 insertions, 402 deletions
diff --git a/python/analytics/cms_spread.py b/python/analytics/cms_spread.py
deleted file mode 100644
index fa61c7cf..00000000
--- a/python/analytics/cms_spread.py
+++ /dev/null
@@ -1,402 +0,0 @@
-from . import cms_spread_utils
-from .cms_spread_utils import h_call, h_put
-import datetime
-import matplotlib.pyplot as plt
-import numpy as np
-import pandas as pd
-import re
-from math import exp, sqrt, log, pi
-from .black import bachelier
-from quantlib.time.api import (
- 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.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.termstructures.volatility.api import (
- VolatilityType,
- SwaptionVolatilityMatrix,
-)
-from quantlib.cashflows.linear_tsr_pricer import LinearTsrPricer
-from quantlib.quotes import SimpleQuote
-
-from quantlib.math.matrix import Matrix
-from scipy import LowLevelCallable
-from scipy.integrate import quad
-from scipy.interpolate import RectBivariateSpline
-from scipy.special import roots_hermitenorm
-from yieldcurve import YC
-from utils.db import dawn_engine, serenitas_pool
-
-__all__ = ["CmsSpread"]
-
-
-_call_integrand = LowLevelCallable.from_cython(cms_spread_utils, "h1")
-
-
-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"
- )
- with conn.cursor() as c:
- c.execute(sql_str, (fixing_date,))
- try:
- fixing1, fixing2 = next(c)
- 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"
- )
- with conn.cursor() as c:
- c.execute(sql_str, fixing_date)
- fixing_date, fixing1, fixing2 = next(c)
-
- date = Date.from_datetime(fixing_date)
- fixing1 = float(fixing1)
- fixing2 = float(fixing2)
- return date, fixing1, fixing2
-
-
-def build_spread_index(tenor1, tenor2):
- yc = YieldTermStructure()
- USISDA1 = UsdLiborSwapIsdaFixAm(Period(tenor1, Years), yc)
- USISDA2 = UsdLiborSwapIsdaFixAm(Period(tenor2, Years), yc)
- spread_index = SwapSpreadIndex(f"{tenor1}-{tenor2}", USISDA1, USISDA2)
- return spread_index, yc
-
-
-def get_swaption_vol_data(
- source="ICPL", vol_type=VolatilityType.ShiftedLognormal, date=None
-):
- if vol_type == VolatilityType.Normal:
- table_name = "swaption_normal_vol"
- else:
- table_name = "swaption_lognormal_vol"
- sql_str = f"SELECT * FROM {table_name} WHERE source=%s "
- if date is None:
- sql_str += "ORDER BY date DESC LIMIT 1"
- params = (source,)
- else:
- sql_str += "AND date=%s"
- params = (source, date)
- conn = serenitas_pool.getconn()
- with conn.cursor() as c:
- 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
-
-
-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.0, 20.0, 25.0, 30.0]
- return RectBivariateSpline(tenors, tenors[-14:], surf)
-
-
-def get_swaption_vol_matrix(date, data, vol_type=VolatilityType.ShiftedLognormal):
- # figure out what to do with nan
- 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]]
- )
- swap_tenors = option_tenors[-14:]
- 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.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)
- vol_type = VolatilityType(atm_vol.volatility_type)
- if isinstance(rho, float):
- rho = SimpleQuote(rho)
- cmsspread_pricer = LognormalCmsSpreadPricer(pricer, rho, yc)
- end_date = Date.from_datetime(maturity)
- pay_date = spread_index.fixing_calendar.advance(end_date, 0, Days)
- start_date = end_date - Period(1, Years)
- end_date = Date.from_datetime(maturity)
- # we build an in arrear floored coupon
- # see line 38 in ql/cashflows/capflooredcoupon.hpp
- # The payoff $P$ of a floored floating-rate coupon is:
- # \[ P = N \times T \times \max(a L + b, F). \]
- # 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.0,
- -cap,
- floor=0.0,
- day_counter=Actual365Fixed(),
- is_in_arrears=True,
- )
- capped_floored_cms_spread_coupon.set_pricer(cmsspread_pricer)
- return capped_floored_cms_spread_coupon
-
-
-def plot_surf(surf, tenors):
- xx, yy = np.meshgrid(tenors, tenors[-14:])
- fig = plt.figure()
- 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
-):
- """ price cap spread option without convexity adjustment
-
- vol_type Normal is the only supported one at the moment"""
- maturity = Date.from_datetime(maturity)
- fixing_date = spread_index.fixing_calendar.advance(maturity, units=Days)
- forward = spread_index.fixing(fixing_date)
- date, surf = get_swaption_vol_data(date=date, vol_type=vol_type)
- 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.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):
- 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_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
- adjusted_gamma = cms_gamma.rate
- adjusted_beta = cms_beta.rate
- 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
- )
- 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)
-
-
-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,
- ):
- """ tenor1 < tenor2"""
- self._value_date = value_date
- if maturity is None:
- maturity = Date.from_datetime(value_date) + option_tenor
- else:
- maturity = Date.from_datetime(maturity)
- spread_index, self.yc = build_spread_index(tenor2, tenor1)
- self.yc.link_to(YC(evaluation_date=value_date, extrapolation=True))
- cal = spread_index.fixing_calendar
- fixing_date = cal.adjust(maturity, ModifiedFollowing)
- payment_date = cal.advance(fixing_date, 2, Days)
- accrued_end_date = payment_date
- accrued_start_date = accrued_end_date - Period(1, Years)
- 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.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.cms1.set_pricer(self._cms_pricer)
- self.cms2.set_pricer(self._cms_pricer)
- self._params = get_params(self.cms1, self.cms2, atm_vol)
- self._x, self._w = roots_hermitenorm(20)
- self.conditional1 = conditional1
- self.conditional2 = conditional2
-
- @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,),
- )
- 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,
- )
- else:
- instance = CmsSpread(
- r.expiration_date,
- tenor1,
- tenor2,
- r.strike * 0.01,
- value_date=r.trade_date,
- notional=r.amount,
- )
- return instance
-
- @property
- def corr(self):
- return self._corr.value
-
- @corr.setter
- def corr(self, val):
- self._corr.value = val
-
- @property
- def value_date(self):
- return self._value_date
-
- @value_date.setter
- 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
- )
- 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)
-
- @property
- def pv(self):
- 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]
- val, _ = quad(_call_integrand, -np.inf, bound, args=args)
- 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)
- )