from __future__ import division import datetime import math import numpy as np import pandas as pd from pyisda.legs import ContingentLeg, FeeLeg from quantlib.settings import Settings from quantlib.time.api import Date, Actual365Fixed from termcolor import colored from pandas.tseries.offsets import BDay from dates import prev_immdate from db import dbconn from psycopg2 import DataError from pyisda.curve import SpreadCurve from .utils import previous_twentieth from scipy.optimize import brentq from yieldcurve import YC, ql_to_jp, roll_yc, rate_helpers serenitasdb = dbconn('serenitasdb') def g(index, spread, exercise_date, forward_yc = None): """computes the strike clean price using the expected forward yield curve """ if forward_yc is None: forward_yc = index._yc step_in_date = exercise_date + datetime.timedelta(days=1) exercise_date_settle = (pd.Timestamp(exercise_date) + 3* BDay()).date() sc = SpreadCurve(exercise_date, forward_yc, index.start_date, step_in_date, exercise_date_settle, [index.end_date], np.array([spread * 1e-4]), index.recovery) a = index._fee_leg.pv(exercise_date, step_in_date, exercise_date_settle, forward_yc, sc, True) return (spread - index.fixed_rate) * a *1e-4 class Index(object): """ minimal class to represent a credit index """ def __init__(self, start_date, end_date, recovery, fixed_rate, notional = 10e6): """ start_date : :class:`datetime.date` index start_date (Could be issue date, or last imm date) end_date : :class:`datetime.date` index last date recovery : recovery rate (between 0 and 1) fixed_rate : fixed coupon (in bps) """ self.fixed_rate = fixed_rate self.notional = notional self._start_date = start_date self._end_date = end_date self.recovery = recovery self._fee_leg = FeeLeg(self._start_date, end_date, True, 1, 1) self._default_leg = ContingentLeg(self._start_date, end_date, 1) self._trade_date = None self._yc = None self._sc = None self._risky_annuity = None self._spread = None self._price = None self.name = None @property def start_date(self): return self._start_date @property def end_date(self): return self._end_date @start_date.setter def start_date(self, d): self._fee_leg = FeeLeg(d, self.end_date, True, 1, 1) self._default_leg = ContingentLeg(d, self.end_date, 1) self._start_date = d @end_date.setter def end_date(self, d): self._fee_leg = FeeLeg(self.start_date, d, True, 1, 1) self._default_leg = ContingentLeg(self.start_date, d, 1) self._end_date = d @property def spread(self): if self._spread is not None: return self._spread * 1e4 else: return None def _update(self): self._sc = SpreadCurve(self.trade_date, self._yc, self.start_date, self._step_in_date, self._value_date, [self.end_date], np.array([self._spread]), self.recovery) self._risky_annuity = self._fee_leg.pv(self.trade_date, self._step_in_date, self._value_date, self._yc, self._sc, False) self._dl_pv = self._default_leg.pv( self.trade_date, self._step_in_date, self._value_date, self._yc, self._sc, self.recovery) self._pv = self._dl_pv - self._risky_annuity * self.fixed_rate * 1e-4 self._clean_pv = self._pv + self._accrued * self.fixed_rate * 1e-4 self._price = 100 * (1 - self._clean_pv) @spread.setter def spread(self, s): """ s: spread in bps """ if self.spread is None or s != self.spread: self._spread = s * 1e-4 self._update() @property def flat_hazard(self): sc_data = self._sc.inspect()['data'] ## conversion to continuous compounding return math.log(1 + sc_data[0][1]) @property def pv(self): return self.notional * self._pv @property def accrued(self): return - self.notional * self._accrued * self.fixed_rate * 1e-4 @property def days_accrued(self): return int(self._accrued * 360) @property def clean_pv(self): return self.notional * self._clean_pv @property def price(self): return self._price @price.setter def price(self, val): if self._price is None or math.fabs(val-self._price) > 1e-6: def handle(x, self, val): self._spread = x self._update() return val - self.price eta = 1.2 a = self.fixed_rate*1e-4 * 0.5 b = a * eta while True: if handle(b, self, val) > 0: break b *= eta self._spread = brentq(handle, a, b, args = (self, val)) self._update() @property def DV01(self): old_pv = self.pv self.spread += 1 dv01 = self.pv - old_pv self.spread -= 1 return dv01 @property def IRDV01(self): old_pv = self.pv old_yc = self._yc # for rh in self._helpers: # rh.quote += 1e-4 # self._yc = ql_to_jp(self._ql_yc) helpers = rate_helpers(self.currency) for rh in helpers: rh.quote += 1e-4 ql_yc = YC(helpers) self._yc = ql_to_jp(ql_yc) self._update() ## to force recomputation new_pv = self.pv # for r in self._helpers: # r.quote -= 1e-4 self._yc = old_yc self._update() return new_pv - old_pv @property def rec_risk(self): old_pv = self.pv old_recovery = self.recovery self.recovery = old_recovery - 0.01 self._update() pv_minus = self.pv self.recovery = old_recovery + 0.01 self._update() pv_plus = self.pv self.recovery = old_recovery self._update() return (pv_plus - pv_minus)/2 @property def jump_to_default(self): return self.notional * (1 - self.recovery) - self.clean_pv @property def risky_annuity(self): return self._risky_annuity - self._accrued @property def trade_date(self): if self._trade_date is None: raise AttributeError('Please set trade_date first') else: return self._trade_date @trade_date.setter def trade_date(self, d): settings = Settings() settings.evaluation_date = Date.from_datetime(d) self.start_date = previous_twentieth(d) # self._helpers = rate_helpers(self.currency) # self._ql_yc = YC(self._helpers) # self._yc = ql_to_jp(self._ql_yc) ql_yc = YC(currency = self.currency) self._yc = ql_to_jp(ql_yc) self._trade_date = d self._step_in_date = self.trade_date + datetime.timedelta(days=1) self._accrued = self._fee_leg.accrued(self._step_in_date) self._value_date = (pd.Timestamp(self._trade_date) + 3* BDay()).date() if self._spread is not None: self._update() @classmethod def from_name(cls, index, series, tenor, trade_date = datetime.date.today(), notional = 10e6): try: with serenitasdb.cursor() as c: c.execute("SELECT maturity, coupon FROM index_maturity " \ "WHERE index=%s AND series=%s AND tenor = %s", (index.upper(), series, tenor)) maturity, coupon = next(c) except DataError as e: raise else: recovery = 0.4 if index.lower() == "ig" else 0.3 instance = cls(trade_date, maturity, recovery, coupon) instance.name = "MARKIT CDX.NA.{}.{} {:%m/%y} ".format( index.upper(), series, maturity) if index.upper() in ["IG", "HY"]: instance.currency = "USD" else: instance.currency = "EUR" instance.notional = notional instance.trade_date = trade_date return instance def __repr__(self): if self.days_accrued > 1: accrued_str = "Accrued ({} Days)".format(self.days_accrued) else: accrued_str = "Accrued ({} Day)".format(self.days_accrued) s = ["{:<20}\t{:>15}".format("CDS Index", colored(self.name, attrs = ['bold'])), "", "{:<20}\t{:>15}".format("Trade Date", ('{:%m/%d/%y}'. format(self.trade_date))), "{:<20}\t{:>15.2f}\t\t{:<20}\t{:>10,.2f}".format("Trd Sprd (bp)", self.spread, "Coupon (bp)", self.fixed_rate), "{:<20}\t{:>15.2f}\t\t{:<20}\t{:>10}".format("1st Accr Start", self.spread, "Payment Freq", "Quarterly"), "{:<20}\t{:>15}\t\t{:<20}\t{:>10.2f}".format("Maturity Date", ('{:%m/%d/%y}'. format(self.end_date)), "Rec Rate", self.recovery), "{:<20}\t{:>15}\t\t{:<20}\t{:>10}".format("Bus Day Adj", "Following", "Day Count", "ACT/360"), "", colored("Calculator", attrs = ['bold']), "{:<20}\t{:>15}".format("Valuation Date", ('{:%m/%d/%y}'. format(self.trade_date))), "{:<20}\t{:>15}".format("Cash Settled On", ('{:%m/%d/%y}'. format(self._value_date))), "", "{:<20}\t{:>15.8f}\t\t{:<20}\t{:>10,.2f}".format("Price", self.price, "Spread DV01", self.DV01), "{:<20}\t{:>15,.0f}\t\t{:<20}\t{:>10,.2f}".format("Principal", self.clean_pv, "IR DV01", self.IRDV01), "{:<20}\t{:>15,.0f}\t\t{:<20}\t{:>10,.2f}".format(accrued_str, self.accrued, "Rec Risk (1%)", self.rec_risk), "{:<20}\t{:>15,.0f}\t\t{:<20}\t{:>10,.0f}".format("Cash Amount", self.pv, "Def Exposure", self.jump_to_default) ] return "\n".join(s) class ForwardIndex(object): def __init__(self, index, forward_date, ref_is_price = False): self.index = index self.forward_date = forward_date self.exercise_date_settle = (pd.Timestamp(forward_date) + 3* BDay()).date() self.df = index._yc.discount_factor(self.exercise_date_settle) self._ref_is_price = ref_is_price self._update() @property def forward_annuity(self): return self._forward_annuity @property def forward_pv(self): return self._forward_pv @property def forward_spread(self): return self._forward_spread * 1e4 @property def ref(self): if ref_is_price: return self.index.price else: return self.index.spread @ref.setter def ref(self, val): if self._ref_is_price: if self.index.price is None or \ math.fabs(self.index.price - val) > 1e-6: self.index.price = val self._update() else: if self.index.spread is None or val != self.index.spread: self.index.spread = val self._update() def _update(self): if self.index._sc is not None: step_in_date = self.forward_date + datetime.timedelta(days=1) a = self.index._fee_leg.pv(self.index.trade_date, step_in_date, self.index.trade_date, self.index._yc, self.index._sc, False) Delta = self.index._fee_leg.accrued(step_in_date) q = self.index._sc.survival_probability(self.forward_date) self._forward_annuity = a - Delta * self.df * q self._forward_pv = self._forward_annuity * (self.index.spread - self.index.fixed_rate) * 1e-4 fep = (1 - self.index.recovery) * (1 - q) self._forward_pv = self._forward_pv / self.df + fep self._forward_spread = self.index._spread + fep * self.df / self._forward_annuity else: self._forward_annuity, self._forward_pv, self._forward_spread = None, None, None