aboutsummaryrefslogtreecommitdiffstats
path: root/python/analytics/index.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/analytics/index.py')
-rw-r--r--python/analytics/index.py501
1 files changed, 75 insertions, 426 deletions
diff --git a/python/analytics/index.py b/python/analytics/index.py
index ebd0d82b..0541c727 100644
--- a/python/analytics/index.py
+++ b/python/analytics/index.py
@@ -1,24 +1,12 @@
-from __future__ import division
import array
import datetime
-import math
-import numpy as np
import pandas as pd
-import warnings
-from dateutil.relativedelta import relativedelta
-from pyisda.legs import ContingentLeg, FeeLeg
-from termcolor import colored
+from .credit_default_swap import CreditDefaultSwap
+from .db import _engine, dbengine, DataError
+from bbg_helpers import BBG_IP, retrieve_data, init_bbg_session
from pandas.tseries.offsets import BDay
-from sqlalchemy import exc
from pyisda.curve import SpreadCurve
-from pyisda.date import previous_twentieth
-from .utils import build_table
-from .db import _engine, dbengine
-from bbg_helpers import BBG_IP, retrieve_data, init_bbg_session
-
-from yieldcurve import get_curve, rate_helpers, YC, ql_to_jp
-from weakref import WeakSet
def g(index, spread, exercise_date, pv=None):
@@ -48,350 +36,19 @@ def g(index, spread, exercise_date, pv=None):
else:
return (spread - index.fixed_rate) * a * 1e-4
-class Index(object):
- """ minimal class to represent a credit index """
- __slots__ = ('fixed_rate', 'notional', '_start_date', '_end_date',
- 'recovery', '_version', '_fee_leg', '_default_leg',
- '_value_date', '_yc', '_sc', '_risky_annuity', '_spread',
- '_price', 'name', 'issue_date', '_quote_is_price',
- '_direction', 'currency', '_step_in_date', '_accrued',
- '_cash_settle_date', '_dl_pv', '_pv', '_clean_pv',
- '_original_clean_pv', '_trade_date',
- 'index_type', 'series', 'tenor', '_observed')
- def __init__(self, start_date, end_date, recovery, fixed_rate,
- notional=10e6, quote_is_price=False, issue_date=None):
- """
- 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 = abs(notional)
- self._start_date = start_date
- self._end_date = end_date
- self.recovery = recovery
- self._version = ()
-
- self._fee_leg = FeeLeg(self._start_date, end_date, True, 1., 1.)
- self._default_leg = ContingentLeg(self._start_date, end_date, True)
- self._value_date = None
- self._yc, self._sc = None, None
- self._risky_annuity = None
- self._spread, self._price = None, None
- self.name = None
- self.issue_date = issue_date
- self._quote_is_price = quote_is_price
- self._direction = -1. if notional > 0 else 1.
- for attr in ['currency', '_step_in_date', '_cash_settle_date', '_accrued',
- '_dl_pv', '_pv', '_clean_pv', '_original_clean_pv',
- '_trade_date', 'index_type', 'series', 'tenor']:
- setattr(self, attr, None)
- self._observed = WeakSet()
-
- def __hash__(self):
- return hash(tuple(getattr(self, k) for k in self.__slots__[:-1]))
-
- def __getstate__(self):
- return {k: getattr(self, k) for k in self.__slots__[:-1]}
-
- def __setstate__(self, state):
- for name, value in state.items():
- setattr(self, name, value)
- self._observed = WeakSet()
-
- @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, True)
- 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, True)
- self._end_date = d
-
- @property
- def spread(self):
- if self._spread is not None:
- return self._spread * 1e4
- else:
- return None
- @property
- def direction(self):
- if self._direction == -1.:
- return "Buyer"
- else:
- return "Seller"
-
- @direction.setter
- def direction(self, d):
- if d == "Buyer":
- self._direction = -1.
- elif d == "Seller":
- self._direction = 1.
- else:
- raise ValueError("Direction needs to be either 'Buyer' or 'Seller'")
-
- def _update(self):
- self._sc = SpreadCurve(self._yc.base_date, self._yc, self.start_date,
- self._step_in_date, self._cash_settle_date,
- [self.end_date], np.array([self._spread]), np.zeros(1),
- np.array([self.recovery]))
-
- self._risky_annuity = self._fee_leg.pv(self.value_date, self._step_in_date,
- self._cash_settle_date, self._yc,
- self._sc, False)
- self._dl_pv = self._default_leg.pv(
- self.value_date, self._step_in_date, self._cash_settle_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()
- self.notify()
-
- @property
- def flat_hazard(self):
- sc_data = self._sc.inspect()['data']
- # conversion to continuous compounding
- return sc_data[0][1]
-
- @property
- def pv(self):
- return - self._direction * self.notional * self.factor * self._pv
-
- @pv.setter
- def pv(self, val):
- self._pv = val / (self.notional * self.factor) * self._direction
- self._clean_pv = self._pv + self._accrued * self.fixed_rate * 1e-4
- self.price = 100 * (1 - self._clean_pv)
-
- @property
- def accrued(self):
- return self._direction * self.notional * self.factor * self._accrued * \
- self.fixed_rate * 1e-4
-
- @property
- def days_accrued(self):
- return int(self._accrued * 360)
-
- @property
- def clean_pv(self):
- return - self._direction * self.notional * self.factor * 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:
- self._clean_pv = (100 - val) / 100
- self._sc = SpreadCurve(
- self.value_date, self._yc, self.start_date,
- self._step_in_date, self._cash_settle_date,
- [self.end_date], array.array('d', [self.fixed_rate*1e-4]),
- array.array('d', [self._clean_pv]),
- array.array('d', [self.recovery]))
- self._risky_annuity = self._fee_leg.pv(
- self.value_date, self._step_in_date, self._cash_settle_date,
- self._yc, self._sc, False)
- self._dl_pv = self._default_leg.pv(
- self.value_date, self._step_in_date, self._cash_settle_date,
- self._yc, self._sc, self.recovery)
- self._pv = self._clean_pv - self._accrued * self.fixed_rate * 1e-4
- self._spread = self._clean_pv / (self._risky_annuity - self._accrued) \
- + self.fixed_rate * 1e-4
- self._price = val
- self.notify()
-
- @property
- def ref(self):
- if self._quote_is_price:
- return self.price
- else:
- return self.spread
-
- @ref.setter
- def ref(self, val):
- if self._quote_is_price:
- self.price = val
- else:
- self.spread = val
-
- @property
- def DV01(self):
- old_pv, old_spread = self.pv, self.spread
- self.spread += 1
- dv01 = self.pv - old_pv
- self.spread = old_spread
- return dv01
-
- @property
- def theta(self):
- old_pv, old_value_date = self.clean_pv, self.value_date
- with warnings.catch_warnings():
- warnings.simplefilter("ignore")
- self.value_date = self.value_date + relativedelta(days=1)
- carry = self.notional * self._direction * self.fixed_rate * 1e-4/360
- roll_down = self.clean_pv - old_pv
- self.value_date = old_value_date
- return carry + roll_down
-
- @property
- def IRDV01(self):
- old_pv, old_yc = self.pv, self._yc
- # for rh in self._helpers:
- # rh.quote += 1e-4
- # self._yc = ql_to_jp(self._ql_yc)
- helpers = rate_helpers(self.currency, evaluation_date=self.value_date)
- for rh in helpers:
- rh.quote.value += 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_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 * self.factor * self._direction * \
- (self.recovery + self._clean_pv - 1)
-
- @property
- def risky_annuity(self):
- return self._risky_annuity - self._accrued
+class CreditIndex(CreditDefaultSwap):
+ __slots__ = ('_indic', '_version', 'index_type', 'series', 'tenor')
- @property
- def value_date(self):
- if self._value_date is None:
- raise AttributeError('Please set value_date first')
- else:
- return self._value_date
-
- @value_date.setter
- def value_date(self, d):
- if isinstance(d, datetime.datetime):
- d = d.date()
- self.start_date = previous_twentieth(d)
- self._yc = get_curve(d, self.currency)
- self._value_date = d
- self._step_in_date = d + datetime.timedelta(days=1)
- self._accrued = self._fee_leg.accrued(self._step_in_date)
- self._cash_settle_date = pd.Timestamp(self._value_date) + 3 * BDay()
- if self._spread is not None:
- self._update()
- self.notify()
-
- def reset_pv(self):
- self._original_clean_pv = self._clean_pv
- self._trade_date = self._value_date
-
- @property
- def pnl(self):
- if self._original_clean_pv is None:
- raise ValueError("original pv not set")
- else:
- # TODO: handle factor change
- days_accrued = (self.value_date - self._trade_date).days / 360
- return - self._direction * self.notional * self.factor * \
- (self._clean_pv - self._original_clean_pv -
- days_accrued * self.fixed_rate * 1e-4)
-
- def notify(self):
- for obj in self._observed:
- obj._update()
-
- def observe(self, obj):
- self._observed.add(obj)
-
- def mark(self, **args):
- if self.value_date == datetime.date.today():
- with init_bbg_session(BBG_IP) as session:
- security = self.name + " Corp"
- field = "PX_LAST"
- ref_data = retrieve_data(session, [security], field)
- self.ref = ref_data[security][field]
- else:
- run = _engine.execute("""SELECT * FROM index_quotes
- WHERE index=%s AND series=%s AND tenor=%s AND date=%s""",
- (self.index_type, self.series, self.tenor, self.value_date))
- rec = run.fetchone()
- self.spread = rec.closespread
-
- @property
- def factor(self):
- for lastdate, factor, _ in self._version:
- if lastdate >= self.value_date:
- return factor
- else:
- return 1
-
- @property
- def version(self):
- for lastdate, _, version in self._version:
- if lastdate >= self.value_date:
- return version
- else:
- return None
-
- def shock(self, params, *, spread_shock, **kwargs):
- r = []
- actual_params = [p for p in params if hasattr(self, p)]
- orig_spread = self.spread
- for ss in spread_shock:
- self.spread = orig_spread * (1 + ss)
- r.append([getattr(self, p) for p in actual_params])
- self.spread = orig_spread
- ind = pd.Index(spread_shock, name='spread_shock', fastpath=True)
- return pd.DataFrame(r, index=ind, columns=actual_params)
-
- @classmethod
- def from_name(cls, index=None, series=None, tenor=None, value_date=datetime.date.today(),
- notional=10_000_000, redcode=None, maturity=None):
- if all([index, series, tenor]):
+ def __init__(self, index_type=None, series=None, tenor=None,
+ value_date=datetime.date.today(), notional=10_000_000,
+ redcode=None, maturity=None):
+ if all([index_type, series, tenor]):
sql_str = "SELECT indexfactor, lastdate, maturity, coupon, " \
"issue_date, version " \
"FROM index_desc WHERE index=%s AND series=%s AND tenor = %s " \
"ORDER BY lastdate ASC"
- params = (index.upper(), series, tenor)
+ params = (index_type.upper(), series, tenor)
elif all([redcode, maturity]):
sql_str = "SELECT index, series, indexfactor, lastdate, maturity, " \
"coupon, issue_date, tenor, version " \
@@ -408,34 +65,34 @@ class Index(object):
coupon = df.coupon[0]
if tenor is None:
tenor = df.tenor[0]
- index_type = index.upper() if index else df.loc[0, 'index']
+ index_type = index_type.upper() if index_type else df.loc[0, 'index']
series = series if series else df.series.iat[0]
df.loc[df.lastdate.isnull(), 'lastdate'] = maturity
- except exc.DataError as e:
+ except DataError as e:
print(e)
return None
else:
recovery = 0.4 if index_type in ['IG', 'EU'] else 0.3
- instance = cls(value_date, maturity, recovery, coupon, notional,
- index_type == "HY", df.issue_date[0])
- instance._version = tuple((ld.date(), factor / 100, version) for ld, factor, version in \
- df[['lastdate', 'indexfactor', 'version']].itertuples(index=False))
- instance.index_type = index_type
- instance.series = series
- instance.tenor = tenor
+ super().__init__(value_date, maturity, recovery, coupon, notional,
+ df.issue_date[0])
+ self._quote_is_price = index_type == "HY"
+ self._indic = tuple((ld.date(), factor / 100, version) for ld, factor, version in \
+ df[['lastdate', 'indexfactor', 'version']].itertuples(index=False))
+ self.index_type = index_type
+ self.series = series
+ self.tenor = tenor
tenor = tenor.upper()
if tenor.endswith("R"):
tenor = tenor[:-1]
- instance.name = "CDX {} CDSI S{} {}".format(index_type,
- series,
- tenor)
+ self.name = "CDX {} CDSI S{} {}".format(index_type,
+ series,
+ tenor)
if index_type in ["IG", "HY"]:
- instance.currency = "USD"
+ self.currency = "USD"
else:
- instance.currency = "EUR"
- instance.value_date = value_date
- return instance
+ self.currency = "EUR"
+ self.value_date = value_date
@classmethod
def from_tradeid(cls, trade_id):
@@ -446,73 +103,65 @@ class Index(object):
ON security_id = redindexcode AND cds.maturity = index_desc.maturity
WHERE id=%s""", (trade_id,))
rec = r.fetchone()
- index_type = rec.index
- recovery = 0.4 if index_type in ['IG', 'EU'] else 0.3
- strike_is_price = index_type == "HY"
- instance = cls(rec.trade_date, rec.maturity, recovery, rec.fixed_rate * 100,
- rec.notional, strike_is_price, rec.issue_date)
- r = _engine.execute("SELECT lastdate, indexfactor/100 AS factor, version FROM index_version " \
- "WHERE index=%s and series=%s", (rec.index, rec.series))
- instance._version = tuple(tuple(t) for t in r)
+ instance = cls(rec.index, rec.series, rec.tenor, rec.trade_date, rec.notional)
instance.name = rec.security_desc
- instance.currency = rec.currency
instance.direction = rec.protection
instance.value_date = rec.trade_date
instance.pv = rec.upfront
- instance._original_clean_pv = instance._clean_pv
- instance._trade_date = rec.trade_date
- instance.index_type = index_type
- instance.series = rec.series
- instance.tenor = rec.tenor
+ instance.reset_pv()
return instance
- def __repr__(self):
- if not self.spread:
- raise ValueError("Market spread is missing!")
- if self.days_accrued > 1:
- accrued_str = "Accrued ({} Days)".format(self.days_accrued)
+ @property
+ def ref(self):
+ if self._quote_is_price:
+ return self.price
else:
- accrued_str = "Accrued ({} Day)".format(self.days_accrued)
+ return self.spread
- s = ["{:<20}\tNotional {:>5}MM {}\tFactor {:>28}".format("Buy Protection"\
- if self._direction == -1
- else "Sell Protection",
- self.notional/1_000_000,
- self.currency,
- self.factor),
- "{:<20}\t{:>15}".format("CDS Index", colored(self.name, attrs=['bold'])),
- ""]
- rows = [["Trd Sprd (bp)", self.spread, "Coupon (bp)", self.fixed_rate],
- ["1st Accr Start", self.issue_date, "Payment Freq", "Quarterly"],
- ["Maturity Date", self.end_date, "Rec Rate", self.recovery],
- ["Bus Day Adj", "Following", "DayCount", "ACT/360"]]
- format_strings = [[None, '{:.2f}', None, '{:.0f}'],
- [None, '{:%m/%d/%y}', None, None],
- [None, '{:%m/%d/%y}', None, None],
- [None, None, None, None]]
- s += build_table(rows, format_strings, "{:<20}{:>19}\t\t{:<20}{:>15}")
- s += ["",
- colored("Calculator", attrs=['bold'])]
- rows = [["Valuation Date", self.value_date],
- ["Cash Settled On", self._cash_settle_date]]
- format_strings = [[None, '{:%m/%d/%y}'],
- [None, '{:%m/%d/%y}']]
- s += build_table(rows, format_strings, "{:<20}\t{:>15}")
- s += [""]
- rows = [["Price", self.price, "Spread DV01", self.DV01],
- ["Principal", self.clean_pv, "IR DV01", self.IRDV01],
- [accrued_str, self.accrued, "Rec Risk (1%)", self.rec_risk],
- ["Cash Amount", self.pv, "Def Exposure", self.jump_to_default]]
- format_strings = [[None, '{:.8f}', None, '{:,.2f}'],
- [None, '{:,.0f}', None, '{:,.2f}'],
- [None, '{:,.0f}', None, '{:,.2f}'],
- [None, '{:,.0f}', None, '{:,.0f}']]
- s += build_table(rows, format_strings, "{:<20}{:>19}\t\t{:<20}{:>15}")
- return "\n".join(s)
+ @ref.setter
+ def ref(self, val):
+ if self._quote_is_price:
+ self.price = val
+ else:
+ self.spread = val
+ def mark(self, **args):
+ if self.value_date == datetime.date.today():
+ with init_bbg_session(BBG_IP) as session:
+ security = self.name + " Corp"
+ field = "PX_LAST"
+ ref_data = retrieve_data(session, [security], field)
+ self.ref = ref_data[security][field]
+ else:
+ run = _engine.execute("""SELECT * FROM index_quotes
+ WHERE index=%s AND series=%s AND tenor=%s AND date=%s""",
+ (self.index_type, self.series, self.tenor, self.value_date))
+ rec = run.fetchone()
+ self.spread = rec.closespread
+
+ value_date = property(CreditDefaultSwap.value_date.__get__)
+
+ @value_date.setter
+ def value_date(self, d):
+ CreditDefaultSwap.value_date.__set__(self, d)
+ for lastdate, factor, version in self._indic:
+ if lastdate >= self.value_date:
+ self._factor = factor
+ self._version = version
+ else:
+ self._factor = 1.
+ self._version = None
+
+ @property
+ def factor(self):
+ return self._factor
+
+ @property
+ def version(self):
+ return self._version
-class ForwardIndex(object):
+class ForwardIndex():
__slots__ = ('index', 'forward_date', 'exercise_date_settle', 'df',
'_forward_annuity', '_forward_pv', '_forward_spread',
'__weakref__')
@@ -569,7 +218,7 @@ class ForwardIndex(object):
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_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