Skip to content

Commit

Permalink
REF: use fixed_rate for CreditPremiumPeriod instead of `credit_spre…
Browse files Browse the repository at this point in the history
…ad` (#446)

Co-authored-by: JHM Darbyshire (Win) <[email protected]>
  • Loading branch information
attack68 and attack68 authored Oct 15, 2024
1 parent 17d3d0e commit cc49947
Show file tree
Hide file tree
Showing 7 changed files with 59 additions and 82 deletions.
6 changes: 3 additions & 3 deletions docs/source/z_cdsw.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ The raw data necessary to build the curves and replicate the pricing risk metric
irs_tenor = ["1m", "2m", "3m", "6m", "12m", "2y", "3y", "4y", "5y", "6y", "7y", "8y", "9y", "10y", "12y"]
irs_rates = irs_rates = [4.8457, 4.7002, 4.5924, 4.3019, 3.8992, 3.5032, 3.3763, 3.3295, 3.3165, 3.3195, 3.3305, 3.3450, 3.3635, 3.3830, 3.4245]
cds_tenor = ["6m", "12m", "2y", "3y", "4y", "5y", "7y", "10y"]
cds_rates = [11.011, 14.189, 20.750, 26.859, 32.862, 37.861, 51.068, 66.891]
cds_rates = [0.11011, 0.14189, 0.20750, 0.26859, 0.32862, 0.37861, 0.51068, 0.66891]
.. image:: _static/cdsw_1.png
:alt: SOFR discount data
Expand Down Expand Up @@ -119,7 +119,7 @@ Lets look at the structure of the hazard rates generated. To do this we plot the
irs_tenor = ["1m", "2m", "3m", "6m", "12m", "2y", "3y", "4y", "5y", "6y", "7y", "8y", "9y", "10y", "12y"]
irs_rates = irs_rates = [4.8457, 4.7002, 4.5924, 4.3019, 3.8992, 3.5032, 3.3763, 3.3295, 3.3165, 3.3195, 3.3305, 3.3450, 3.3635, 3.3830, 3.4245]
cds_tenor = ["6m", "12m", "2y", "3y", "4y", "5y", "7y", "10y"]
cds_rates = [11.011, 14.189, 20.750, 26.859, 32.862, 37.861, 51.068, 66.891]
cds_rates = [0.11011, 0.14189, 0.20750, 0.26859, 0.32862, 0.37861, 0.51068, 0.66891]
today = dt(2024, 10, 4) # Friday 4th October 2024
spot = dt(2024, 10, 8) # Tuesday 8th October 2024
disc_curve = Curve(
Expand Down Expand Up @@ -189,7 +189,7 @@ numerical integrations of CDS protection and premium legs).
convention="act360",
calendar="nyc",
curves=["pfizer", "sofr"],
credit_spread=100.0,
fixed_rate=1.0,
recovery_rate=0.4,
premium_accrued=True,
notional=10e6,
Expand Down
28 changes: 10 additions & 18 deletions python/rateslib/instruments/rates_derivatives.py
Original file line number Diff line number Diff line change
Expand Up @@ -2431,10 +2431,10 @@ class CDS(BaseDerivative):
----------
args : dict
Required positional args to :class:`BaseDerivative`.
credit_spread : float or None, optional
fixed_rate : float or None, optional
The rate applied to determine the cashflow on the premium leg. If `None`, can be set later,
typically after a mid-market rate for all periods has been calculated.
Entered in basis points.
Entered in percentage points, e.g. 50bps is 0.50.
premium_accrued : bool, optional
Whether the premium is accrued within the period to default.
recovery_rate : float, Dual, Dual2, optional
Expand All @@ -2447,12 +2447,13 @@ class CDS(BaseDerivative):
Required keyword arguments to :class:`BaseDerivative`.
"""

_rate_scalar = 100.0
_rate_scalar = 1.0
_fixed_rate_mixin = True

def __init__(
self,
*args,
credit_spread: float | NoInput = NoInput(0),
fixed_rate: float | NoInput = NoInput(0),
premium_accrued: bool | NoInput = NoInput(0),
recovery_rate: DualTypes | NoInput = NoInput(0),
discretization: int | NoInput = NoInput(0),
Expand All @@ -2465,7 +2466,7 @@ def __init__(
leg2_initial_exchange=False,
leg2_final_exchange=False,
leg2_frequency="Z", # CDS protection is only ever one payoff
credit_spread=credit_spread,
fixed_rate=fixed_rate,
premium_accrued=premium_accrued,
leg2_recovery_rate=recovery_rate,
leg2_discretization=discretization,
Expand All @@ -2482,27 +2483,18 @@ def __init__(

self.leg1 = CreditPremiumLeg(**_get(self.kwargs, leg=1))
self.leg2 = CreditProtectionLeg(**_get(self.kwargs, leg=2))
self._credit_spread = self.kwargs["credit_spread"]

@property
def credit_spread(self):
return self._credit_spread

@credit_spread.setter
def credit_spread(self, value):
self._credit_spread = value
self.leg1.credit_spread = value
self._fixed_rate = self.kwargs["fixed_rate"]

def _set_pricing_mid(
self,
curves: Curve | str | list | NoInput = NoInput(0),
solver: Solver | NoInput = NoInput(0),
):
# the test for an unpriced IRS is that its fixed rate is not set.
if self.credit_spread is NoInput.blank:
if self.fixed_rate is NoInput.blank:
# set a rate for the purpose of generic methods NPV will be zero.
mid_market_rate = self.rate(curves, solver)
self.leg1.credit_spread = float(mid_market_rate)
self.leg1.fixed_rate = float(mid_market_rate)

def analytic_delta(self, *args, **kwargs):
"""
Expand Down Expand Up @@ -2573,7 +2565,7 @@ def rate(
self.leg1.currency,
)
leg2_npv = self.leg2.npv(curves[2], curves[3])
return self.leg1._spread(-leg2_npv, curves[0], curves[1])
return self.leg1._spread(-leg2_npv, curves[0], curves[1]) * 0.01

def cashflows(
self,
Expand Down
29 changes: 7 additions & 22 deletions python/rateslib/legs.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ def fixed_rate(self):
def fixed_rate(self, value):
self._fixed_rate = value
for period in getattr(self, "periods", []):
if isinstance(period, FixedPeriod):
if isinstance(period, (FixedPeriod, CreditPremiumPeriod)):
period.fixed_rate = value

def _regular_period(
Expand Down Expand Up @@ -1713,15 +1713,15 @@ def npv(self, *args, **kwargs):
return super().npv(*args, **kwargs)


class CreditPremiumLeg(BaseLeg):
class CreditPremiumLeg(BaseLeg, _FixedLegMixin):
"""
Create a credit premium leg composed of :class:`~rateslib.periods.CreditPremiumPeriod` s.
Parameters
----------
args : tuple
Required positional args to :class:`BaseLeg`.
credit_spread : float, optional
fixed_rate : float, optional
The credit spread applied to determine cashflows in bps (i.e 5.0 = 5bps). Can be left unset and
designated later, perhaps after a mid-market rate for all periods has been calculated.
premium_accrued : bool, optional
Expand Down Expand Up @@ -1759,7 +1759,7 @@ class CreditPremiumLeg(BaseLeg):
hazard_curve = Curve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.995})
premium_leg = CreditPremiumLeg(
dt(2022, 1, 1), "9M", "Q",
credit_spread=26.0,
fixed_rate=2.60,
notional=1000000,
)
premium_leg.cashflows(hazard_curve, disc_curve)
Expand All @@ -1769,30 +1769,15 @@ class CreditPremiumLeg(BaseLeg):
def __init__(
self,
*args,
credit_spread: float | NoInput = NoInput(0),
fixed_rate: float | NoInput = NoInput(0),
premium_accrued: bool | NoInput = NoInput(0),
**kwargs,
):
self._credit_spread = credit_spread
self._fixed_rate = fixed_rate
self.premium_accrued = _drb(defaults.cds_premium_accrued, premium_accrued)
super().__init__(*args, **kwargs)
self._set_periods()

@property
def credit_spread(self):
"""
float or NoInput : If set will also set the ``credit_spread`` of
contained :class:`CreditPremiumPeriod` s.
"""
return self._credit_spread

@credit_spread.setter
def credit_spread(self, value):
self._credit_spread = value
for period in getattr(self, "periods", []):
if isinstance(period, CreditPremiumPeriod):
period.credit_spread = value

def analytic_delta(self, *args, **kwargs):
"""
Return the analytic delta of the *CreditPremiumLeg* via summing all periods.
Expand Down Expand Up @@ -1833,7 +1818,7 @@ def _regular_period(
iterator: int,
):
return CreditPremiumPeriod(
credit_spread=self.credit_spread,
fixed_rate=self.fixed_rate,
premium_accrued=self.premium_accrued,
start=start,
end=end,
Expand Down
18 changes: 9 additions & 9 deletions python/rateslib/periods.py
Original file line number Diff line number Diff line change
Expand Up @@ -1745,10 +1745,10 @@ class CreditPremiumPeriod(BasePeriod):
----------
args : dict
Required positional args to :class:`BasePeriod`.
credit_spread : float or None, optional
fixed_rate : float or None, optional
The rate applied to determine the cashflow. If `None`, can be set later,
typically after a mid-market rate for all periods has been calculated.
Entered in basis points.
Entered in percentage points, e.g 50bps is 0.50.
premium_accrued : bool, optional
Whether the premium is accrued within the period to default.
kwargs : dict
Expand Down Expand Up @@ -1796,23 +1796,23 @@ class CreditPremiumPeriod(BasePeriod):
def __init__(
self,
*args,
credit_spread: float | NoInput = NoInput(0),
fixed_rate: float | NoInput = NoInput(0),
premium_accrued: bool | NoInput = NoInput(0),
**kwargs,
):
self.premium_accrued = _drb(defaults.cds_premium_accrued, premium_accrued)
self.credit_spread = credit_spread
self.fixed_rate = fixed_rate
super().__init__(*args, **kwargs)

@property
def cashflow(self) -> float | None:
"""
float, Dual or Dual2 : The calculated value from rate, dcf and notional.
"""
if self.credit_spread is NoInput.blank:
if self.fixed_rate is NoInput.blank:
return None
else:
return -self.notional * self.dcf * self.credit_spread * 0.0001
return -self.notional * self.dcf * self.fixed_rate * 0.01

def npv(
self,
Expand All @@ -1830,8 +1830,8 @@ def npv(
raise TypeError("`curves` have not been supplied correctly.")
if not isinstance(curve, Curve) and curve is NoInput.blank:
raise TypeError("`curves` have not been supplied correctly.")
if self.credit_spread is NoInput.blank:
raise ValueError("`credit_spread` must be set as a value to return a valid NPV.")
if self.fixed_rate is NoInput.blank:
raise ValueError("`fixed_rate` must be set as a value to return a valid NPV.")
v_payment = disc_curve[self.payment]
q_end = curve[self.end]
_ = 0.0
Expand Down Expand Up @@ -1926,7 +1926,7 @@ def cashflows(

return {
**super().cashflows(curve, disc_curve, fx, base),
defaults.headers["spread"]: float(self.credit_spread),
defaults.headers["rate"]: float(self.fixed_rate),
defaults.headers["survival"]: survival,
defaults.headers["cashflow"]: float(self.cashflow),
defaults.headers["npv"]: npv,
Expand Down
20 changes: 10 additions & 10 deletions python/tests/test_instruments.py
Original file line number Diff line number Diff line change
Expand Up @@ -2329,14 +2329,14 @@ def okane_curve(self):
convention="act360",
payment_lag=0,
curves=["credit", "ibor"],
credit_spread=400,
fixed_rate=4.00,
recovery_rate=0.4,
premium_accrued=True,
calendar="nyc",
)
for _ in cds_tenor
],
s=[400, 400, 400, 400, 400, 400, 400, 400],
s=[4.00, 4.00, 4.00, 4.00, 4.00, 4.00, 4.00, 4.00],
)
return credit_curve, ibor, cc_sv

Expand All @@ -2347,14 +2347,14 @@ def test_okane_values(self):
dt(2029, 6, 20),
front_stub=dt(2019, 9, 20),
frequency="q",
credit_spread=150,
fixed_rate=1.50,
curves=["credit", "ibor"],
discretization=5,
calendar="nyc",
)
c1, c2, solver = self.okane_curve()
result1 = cds.rate(solver=solver)
assert abs(result1 - 399.99960) < 5e-3
assert abs(result1 - 3.9999960) < 5e-5

result2 = cds.npv(solver=solver)
assert abs(result2 - 170739.5956) < 175
Expand Down Expand Up @@ -2390,7 +2390,7 @@ def test_rate(self, curve, curve2) -> None:
)

rate = cds.rate([hazard_curve, disc_curve])
expected = 241.64004881061285
expected = 2.4164004881061285
assert abs(rate - expected) < 1e-7

def test_npv(self, curve, curve2) -> None:
Expand All @@ -2403,7 +2403,7 @@ def test_npv(self, curve, curve2) -> None:
"M",
payment_lag=0,
currency="eur",
credit_spread=100.0,
fixed_rate=1.00,
)

npv = cds.npv([hazard_curve, disc_curve])
Expand Down Expand Up @@ -2455,7 +2455,7 @@ def test_solver(self, curve2):
CDS(dt(2022, 1, 1), "6m", frequency="Q", curves=["haz", c1]),
CDS(dt(2022, 1, 1), "12m", frequency="Q", curves=["haz", c1]),
],
s=[30, 40],
s=[.30, .40],
instrument_labels=["6m", "12m"],
)
inst = CDS(dt(2022, 7, 1), "3M", "Q", curves=["haz", c1], notional=1e6)
Expand Down Expand Up @@ -2517,11 +2517,11 @@ def test_okane_paper(self):
CDS(dt(2003, 6, 20), "4y", **args),
CDS(dt(2003, 6, 20), "5y", **args),
],
s=[110, 120, 130, 140, 150],
s=[1.10, 1.20, 1.30, 1.40, 1.50],
)
cds = CDS(dt(2003, 6, 20), dt(2007, 9, 20), credit_spread=200, notional=10e6, **args)
cds = CDS(dt(2003, 6, 20), dt(2007, 9, 20), fixed_rate=2.00, notional=10e6, **args)
result = cds.rate(solver=solver)
assert abs(result - 142.7) < 0.30
assert abs(result - 1.427) < 0.0030

_table = cds.cashflows(solver=solver)
leg1_npv = cds.leg1.npv(haz_curve, usd_libor)
Expand Down
18 changes: 9 additions & 9 deletions python/tests/test_legs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1122,10 +1122,10 @@ def test_premium_leg_npv(self, hazard_curve, curve, premium_accrued) -> None:
convention="Act360",
frequency="Q",
premium_accrued=premium_accrued,
credit_spread=40.0,
fixed_rate=4.00,
)
result = leg.npv(hazard_curve, curve)
assert abs(result + 40.0 * leg.analytic_delta(hazard_curve, curve)) < 1e-7
assert abs(result + 400 * leg.analytic_delta(hazard_curve, curve)) < 1e-7

def test_premium_leg_cashflows(self, hazard_curve, curve) -> None:
leg = CreditPremiumLeg(
Expand All @@ -1135,15 +1135,15 @@ def test_premium_leg_cashflows(self, hazard_curve, curve) -> None:
notional=-1e9,
convention="Act360",
frequency="Q",
credit_spread=400.0,
fixed_rate=4.00,
)
result = leg.cashflows(hazard_curve, curve)
# test a couple of return elements
assert abs(result.loc[0, defaults.headers["cashflow"]] - 6555555.55555) < 1e-4
assert abs(result.loc[1, defaults.headers["df"]] - 0.98307) < 1e-4
assert abs(result.loc[1, defaults.headers["notional"]] + 1e9) < 1e-7

def test_premium_leg_set_credit_spread(self, curve) -> None:
def test_premium_leg_set_fixed_rate(self, curve) -> None:
leg = CreditPremiumLeg(
effective=dt(2022, 1, 1),
termination=dt(2022, 6, 1),
Expand All @@ -1152,12 +1152,12 @@ def test_premium_leg_set_credit_spread(self, curve) -> None:
convention="Act360",
frequency="Q",
)
assert leg.credit_spread is NoInput(0)
assert leg.periods[0].credit_spread is NoInput(0)
assert leg.fixed_rate is NoInput(0)
assert leg.periods[0].fixed_rate is NoInput(0)

leg.credit_spread = 2.0
assert leg.credit_spread == 2.0
assert leg.periods[0].credit_spread == 2.0
leg.fixed_rate = 2.0
assert leg.fixed_rate == 2.0
assert leg.periods[0].fixed_rate == 2.0


class TestCreditProtectionLeg:
Expand Down
Loading

0 comments on commit cc49947

Please sign in to comment.