diff --git a/python/rateslib/legs/components/credit.py b/python/rateslib/legs/components/credit.py index 66222b18a..edaeee385 100644 --- a/python/rateslib/legs/components/credit.py +++ b/python/rateslib/legs/components/credit.py @@ -12,13 +12,17 @@ if TYPE_CHECKING: from rateslib.typing import ( # pragma: no cover FX_, + CurveOption_, DualTypes, DualTypes_, + FXForwards_, + FXVolOption_, Schedule, _BaseCurve_, _SettlementParams, bool_, datetime, + datetime_, str_, ) @@ -171,6 +175,28 @@ def __init__( # self._exchange_periods = (None, None) # self._mtm_exchange_periods = None + def spread( + self, + *, + target_npv: DualTypes, + rate_curve: CurveOption_ = NoInput(0), + index_curve: _BaseCurve_ = NoInput(0), + disc_curve: _BaseCurve_ = NoInput(0), + fx: FXForwards_ = NoInput(0), + fx_vol: FXVolOption_ = NoInput(0), + settlement: datetime_ = NoInput(0), + forward: datetime_ = NoInput(0), + ) -> DualTypes: + a_delta = self.local_analytic_delta( + rate_curve=rate_curve, + disc_curve=disc_curve, + index_curve=index_curve, + fx=fx, + forward=forward, + settlement=settlement, + ) + return -target_npv / a_delta + class CreditProtectionLeg(_BaseLeg): """ diff --git a/python/rateslib/legs/components/fixed.py b/python/rateslib/legs/components/fixed.py index 461208281..08066f06e 100644 --- a/python/rateslib/legs/components/fixed.py +++ b/python/rateslib/legs/components/fixed.py @@ -5,6 +5,9 @@ import rateslib.errors as err from rateslib import defaults +from rateslib.curves._parsers import ( + _disc_required_maybe_from_curve, +) from rateslib.data.fixings import _leg_fixings_to_list from rateslib.enums.generics import NoInput, _drb from rateslib.legs.components.amortization import Amortization, _AmortizationType, _get_amortization @@ -21,16 +24,15 @@ if TYPE_CHECKING: from rateslib.typing import ( # pragma: no cover - FX_, CurveOption_, DualTypes, DualTypes_, FXForwards_, + FXVolOption_, IndexMethod, LegFixings, Schedule, Series, - _BaseCurve, _BaseCurve_, _SettlementParams, datetime, @@ -654,53 +656,18 @@ def fx_delivery(i: int) -> datetime: else: self._mtm_exchange_periods = None - def _spread( + def spread( self, + *, target_npv: DualTypes, - rate_curve: CurveOption_, - disc_curve: _BaseCurve_, - index_curve: _BaseCurve_, + rate_curve: CurveOption_ = NoInput(0), + index_curve: _BaseCurve_ = NoInput(0), + disc_curve: _BaseCurve_ = NoInput(0), fx: FXForwards_ = NoInput(0), - forward: datetime_ = NoInput(0), + fx_vol: FXVolOption_ = NoInput(0), settlement: datetime_ = NoInput(0), + forward: datetime_ = NoInput(0), ) -> DualTypes: - """ - Calculates the ``fixed_rate`` to match a specific target NPV on the leg. - - Parameters - ---------- - target_npv : float, Dual or Dual2 - The target NPV that an adjustment to the parameter will achieve. **Must - be in local currency of the leg.** - rate_curve : Curve or LineCurve - The forecast curve passed to analytic delta calculation. - disc_curve : Curve - The discounting curve passed to analytic delta calculation. - index_curve : _BaseCurve_ - The curve used for forecasting index values. - fx : FXForwards, optional - Required for multi-currency legs which are MTM exchanged. - - Returns - ------- - float, Dual, Dual2 - - Notes - ----- - ``FixedLeg`` and ``FloatLeg`` with a *"none_simple"* spread compound method have - linear sensitivity to the spread. This can be calculated directly and - exactly using an analytic delta calculation. - - *"isda_compounding"* and *"isda_flat_compounding"* spread compound methods - have non-linear sensitivity to the spread. This requires a root finding, - iterative algorithm, which, coupled with very poor performance of calculating - period rates under this method is exceptionally slow. We approximate this - using first and second order AD and extrapolate a solution as a Taylor - expansion. This results in approximation error. - - Examples - -------- - """ a_delta = self.local_analytic_delta( rate_curve=rate_curve, disc_curve=disc_curve, @@ -866,19 +833,36 @@ def fixed_rate(self, value: DualTypes_) -> None: for period in self._regular_periods: period.rate_params.fixed_rate = value - def _spread( + def spread( self, + *, target_npv: DualTypes, - rate_curve: CurveOption_, - disc_curve: _BaseCurve, + rate_curve: CurveOption_ = NoInput(0), index_curve: _BaseCurve_ = NoInput(0), + disc_curve: _BaseCurve_ = NoInput(0), fx: FXForwards_ = NoInput(0), + fx_vol: FXVolOption_ = NoInput(0), + settlement: datetime_ = NoInput(0), + forward: datetime_ = NoInput(0), ) -> DualTypes: - """ - Overload the _spread calc to use analytic delta based on period rate - """ + disc_curve_ = _disc_required_maybe_from_curve(rate_curve, disc_curve) - unindexed_target_npv = target_npv / self._regular_periods[0].index_up( + if not isinstance(settlement, NoInput): + if settlement > self.settlement_params.ex_dividend: + raise ZeroDivisionError( + "A `spread` cannot be determined when the *Leg* always has zero value.\n" + "The given `settlement` is after the `ex_dividend` date." + ) + else: + w_fwd = disc_curve_[_drb(settlement, forward)] + else: + if isinstance(forward, NoInput): + w_fwd = 1.0 + else: + w_fwd = disc_curve_[forward] + + immediate_target_npv = target_npv * w_fwd + unindexed_target_npv = immediate_target_npv / self._regular_periods[0].index_up( 1.0, index_curve=index_curve ) unindexed_reference_target_npv = unindexed_target_npv / self._regular_periods[ @@ -888,7 +872,7 @@ def _spread( f = self.schedule.periods_per_annum d = self._regular_periods[0].dcf N = self.settlement_params.notional - w = disc_curve[self.settlement_params.payment] + w = disc_curve_[self.settlement_params.payment] R = ((-unindexed_reference_target_npv / (N * w) + 1) ** (1 / (d * f)) - 1) * f * 10000.0 return R @@ -998,15 +982,3 @@ def __init__( ) self._exchange_periods = (_ini_cf,) self._regular_periods = (_final_cf,) - - def _spread( - self, - target_npv: DualTypes, - fore_curve: CurveOption_, - disc_curve: CurveOption_, - fx: FX_ = NoInput(0), - ) -> DualTypes: - """ - Overload the _spread calc to use analytic delta based on period rate - """ - raise NotImplementedError() diff --git a/python/rateslib/legs/components/float.py b/python/rateslib/legs/components/float.py index 2f554625a..e7b7e879a 100644 --- a/python/rateslib/legs/components/float.py +++ b/python/rateslib/legs/components/float.py @@ -20,19 +20,20 @@ if TYPE_CHECKING: from rateslib.typing import ( # pragma: no cover - FX_, CurveOption_, DualTypes, DualTypes_, FloatRateSeries, Frequency, FXForwards_, + FXVolOption_, IndexMethod, LegFixings, Schedule, _BaseCurve_, _BasePeriod, datetime, + datetime_, int_, str_, ) @@ -367,82 +368,51 @@ def _is_linear(self) -> bool: return False return True - def _spread( + def spread( self, + *, target_npv: DualTypes, - rate_curve: CurveOption_, - disc_curve: _BaseCurve_, + rate_curve: CurveOption_ = NoInput(0), index_curve: _BaseCurve_ = NoInput(0), + disc_curve: _BaseCurve_ = NoInput(0), fx: FXForwards_ = NoInput(0), + fx_vol: FXVolOption_ = NoInput(0), + settlement: datetime_ = NoInput(0), + forward: datetime_ = NoInput(0), ) -> DualTypes: - """ - Calculates an adjustment to the ``fixed_rate`` or ``float_spread`` to match - a specific target NPV. - - Parameters - ---------- - target_npv : float, Dual or Dual2 - The target NPV that an adjustment to the parameter will achieve. **Must - be in local currency of the leg.** - rate_curve : Curve or LineCurve - The forecast curve passed to analytic delta calculation. - disc_curve : Curve - The discounting curve passed to analytic delta calculation. - fx : FXForwards, optional - Required for multi-currency legs which are MTM exchanged. - index_curve : _BaseCurve, optional - The index curve used for forecasting index values. - - Returns - ------- - float, Dual, Dual2 - - Notes - ----- - ``FixedLeg`` and ``FloatLeg`` with a *"none_simple"* spread compound method have - linear sensitivity to the spread. This can be calculated directly and - exactly using an analytic delta calculation. - - *"isda_compounding"* and *"isda_flat_compounding"* spread compound methods - have non-linear sensitivity to the spread. This requires a root finding, - iterative algorithm, which, coupled with very poor performance of calculating - period rates under this method is exceptionally slow. We approximate this - using first and second order AD and extrapolate a solution as a Taylor - expansion. This results in approximation error. - - Examples - -------- - """ if self._is_linear: - a_delta: DualTypes = self.local_analytic_delta( + local_npv = self.local_npv( rate_curve=rate_curve, disc_curve=disc_curve, index_curve=index_curve, fx=fx, + forward=forward, + settlement=settlement, ) - return -target_npv / a_delta - else: - original_z = self.float_spread - original_npv = self.npv( + a_delta = self.local_analytic_delta( rate_curve=rate_curve, disc_curve=disc_curve, index_curve=index_curve, fx=fx, + forward=forward, + settlement=settlement, ) + return -(target_npv - local_npv) / a_delta + else: + original_z = self.float_spread def s(g: DualTypes) -> DualTypes: """ This determines the NPV change subject to a given float spread change denoted, g. """ - self.float_spread = g + original_z - return ( - self.npv( # type: ignore[operator] - rate_curve=rate_curve, - disc_curve=disc_curve, - index_curve=index_curve, - fx=fx, - ) - - original_npv + self.float_spread = g + return self.local_npv( + rate_curve=rate_curve, + disc_curve=disc_curve, + index_curve=index_curve, + fx=fx, + forward=forward, + settlement=settlement, ) result = ift_1dim( @@ -620,14 +590,46 @@ def __init__( ), ) - def _spread( + def spread( self, + *, target_npv: DualTypes, - fore_curve: CurveOption_, - disc_curve: CurveOption_, - fx: FX_ = NoInput(0), + rate_curve: CurveOption_ = NoInput(0), + index_curve: _BaseCurve_ = NoInput(0), + disc_curve: _BaseCurve_ = NoInput(0), + fx: FXForwards_ = NoInput(0), + fx_vol: FXVolOption_ = NoInput(0), + settlement: datetime_ = NoInput(0), + forward: datetime_ = NoInput(0), ) -> DualTypes: - """ - Overload the _spread calc to use analytic delta based on period rate - """ - raise NotImplementedError() + original_z = self.float_spread + + def s(g: DualTypes) -> DualTypes: + """ + This determines the NPV of the *Leg* subject to a given float spread change denoted, g. + """ + self.float_spread = g + iteration_local_npv = self.local_npv( + rate_curve=rate_curve, + disc_curve=disc_curve, + index_curve=index_curve, + fx=fx, + forward=forward, + settlement=settlement, + ) + return iteration_local_npv + + result = ift_1dim( + s=s, + s_tgt=target_npv, + h="ytm_quadratic", + ini_h_args=(-300, 300, 1200), + # h="modified_brent", + # ini_h_args=(-10000, 10000), + func_tol=1e-6, + conv_tol=1e-6, + ) + + self.float_spread = original_z + _: DualTypes = result["g"] + return _ diff --git a/python/rateslib/legs/components/protocols/npv.py b/python/rateslib/legs/components/protocols/npv.py index 11a63b4b8..10594d037 100644 --- a/python/rateslib/legs/components/protocols/npv.py +++ b/python/rateslib/legs/components/protocols/npv.py @@ -35,6 +35,60 @@ def periods(self) -> list[_BasePeriod]: def __repr__(self) -> str: return f"" + def local_npv( + self, + *, + rate_curve: CurveOption_ = NoInput(0), + index_curve: _BaseCurve_ = NoInput(0), + disc_curve: _BaseCurve_ = NoInput(0), + fx: FXForwards_ = NoInput(0), + fx_vol: FXVolOption_ = NoInput(0), + settlement: datetime_ = NoInput(0), + forward: datetime_ = NoInput(0), + ) -> DualTypes: + """ + Calculate the NPV of the *Period* expressed in local settlement currency. + + Parameters + ---------- + rate_curve: _BaseCurve or dict of such indexed by string tenor, optional + Used to forecast floating period rates, if necessary. + index_curve: _BaseCurve, optional + Used to forecast index values for indexation, if necessary. + disc_curve: _BaseCurve, optional + Used to discount cashflows. + fx: FXForwards, optional + The :class:`~rateslib.fx.FXForwards` object used for forecasting the + ``fx_fixing`` for deliverable cashflows, if necessary. Or, an + :class:`~rateslib.fx.FXRates` object purely for immediate currency conversion. + fx_vol: FXDeltaVolSmile, FXSabrSmile, FXDeltaVolSurface, FXSabrSurface, optional + The FX volatility *Smile* or *Surface* object used for determining Black calendar + day implied volatility values. + settlement: datetime, optional + The assumed settlement date of the *PV* determination. Used only to evaluate + *ex-dividend* status. + forward: datetime, optional + The future date to project the *PV* to using the ``disc_curve``. + + Returns + ------- + float, Dual, Dual2, Variable + """ + # a Leg only has cashflows in one single currency + local_npv: DualTypes = sum( + _.local_npv( + rate_curve=rate_curve, + index_curve=index_curve, + disc_curve=disc_curve, + fx=fx, + fx_vol=fx_vol, + settlement=settlement, + forward=forward, + ) + for _ in self.periods + ) + return local_npv + def npv( self, *, @@ -117,3 +171,49 @@ def npv( fx=fx, base=base, ) + + def spread( + self, + *, + target_npv: DualTypes, + rate_curve: CurveOption_ = NoInput(0), + index_curve: _BaseCurve_ = NoInput(0), + disc_curve: _BaseCurve_ = NoInput(0), + fx: FXForwards_ = NoInput(0), + fx_vol: FXVolOption_ = NoInput(0), + settlement: datetime_ = NoInput(0), + forward: datetime_ = NoInput(0), + ) -> DualTypes: + """ + Calculate a spread metric which when applied to the *Leg* allows it to attain the target + value. + + Parameters + ---------- + target_npv: DualTypes, required + The target value of the *Leg* measured using all of the other given arguments. + Must be expressed in local settlement currency units. + rate_curve: _BaseCurve or dict of such indexed by string tenor, optional + Used to forecast floating period rates, if necessary. + index_curve: _BaseCurve, optional + Used to forecast index values for indexation, if necessary. + disc_curve: _BaseCurve, optional + Used to discount cashflows. + fx: FXForwards, optional + The :class:`~rateslib.fx.FXForwards` object used for forecasting the + ``fx_fixing`` for deliverable cashflows, if necessary. Or, an + :class:`~rateslib.fx.FXRates` object purely for immediate currency conversion. + fx_vol: FXDeltaVolSmile, FXSabrSmile, FXDeltaVolSurface, FXSabrSurface, optional + The FX volatility *Smile* or *Surface* object used for determining Black calendar + day implied volatility values. + settlement: datetime, optional + The assumed settlement date of the *PV* determination. Used only to evaluate + *ex-dividend* status. + forward: datetime, optional + The future date to project the *PV* to using the ``disc_curve``. + + Returns + ------- + float, Dual, Dual2, Variable + """ + raise NotImplementedError(f"Method: `spread` is not available for {type(self).__name__}.") diff --git a/python/rateslib/periods/components/protocols/npv.py b/python/rateslib/periods/components/protocols/npv.py index 40d45abd6..accf71e52 100644 --- a/python/rateslib/periods/components/protocols/npv.py +++ b/python/rateslib/periods/components/protocols/npv.py @@ -246,10 +246,10 @@ def local_npv( fx_vol: FXDeltaVolSmile, FXSabrSmile, FXDeltaVolSurface, FXSabrSurface, optional The FX volatility *Smile* or *Surface* object used for determining Black calendar day implied volatility values. - settlement: datetime, optional + settlement: datetime, optional (set as immediate date) The assumed settlement date of the *PV* determination. Used only to evaluate *ex-dividend* status. - forward: datetime, optional + forward: datetime, optional (set as ``settlement``) The future date to project the *PV* to using the ``disc_curve``. Returns @@ -354,10 +354,10 @@ def npv( The currency to convert the *local settlement* NPV to. local: bool, optional An override flag to return a dict of NPV values indexed by string currency. - settlement: datetime, optional + settlement: datetime, optional, (set as immediate date) The assumed settlement date of the *PV* determination. Used only to evaluate *ex-dividend* status. - forward: datetime, optional + forward: datetime, optional, (set as ``settlement``) The future date to project the *PV* to using the ``disc_curve``. Returns diff --git a/python/tests/legs/test_legs_legacy.py b/python/tests/legs/test_legs_legacy.py index 904b7d704..e9ab8ad63 100644 --- a/python/tests/legs/test_legs_legacy.py +++ b/python/tests/legs/test_legs_legacy.py @@ -436,14 +436,20 @@ def test_is_linear(self, method, spread_method, expected) -> None: assert float_leg._is_linear is expected @pytest.mark.parametrize( - ("method", "expected"), + ("method", "settlement", "forward", "expected"), [ - ("ISDA_compounding", 357.7019143401966), - ("ISDA_flat_compounding", 360.65913016465225), - ("NONE_Simple", 362.2342162), + ("ISDA_compounding", NoInput(0), NoInput(0), 357.7019143401966), + ("ISDA_compounding", dt(2022, 4, 6), NoInput(0), 580.3895480501503), + ("ISDA_flat_compounding", NoInput(0), NoInput(0), 360.65913016465225), + ("ISDA_flat_compounding", dt(2022, 4, 6), NoInput(0), 587.64160672647), + ("NONE_Simple", NoInput(0), NoInput(0), 362.2342162), + ("NONE_Simple", NoInput(0), dt(2022, 2, 1), 360.98240826375957), + ("NONE_Simple", dt(2022, 4, 6), NoInput(0), 590.6350781908598), ], ) - def test_float_leg_spread_calculation(self, method, expected, curve) -> None: + def test_float_leg_spread_calculation( + self, method, settlement, forward, expected, curve + ) -> None: leg = FloatLeg( schedule=Schedule( effective=dt(2022, 1, 1), @@ -458,11 +464,26 @@ def test_float_leg_spread_calculation(self, method, expected, curve) -> None: currency="nok", float_spread=0, ) - base_npv = leg.npv(rate_curve=curve, disc_curve=curve) - result = leg._spread(-15000000, curve, curve) + base_npv = leg.npv( + rate_curve=curve, disc_curve=curve, forward=forward, settlement=settlement + ) + result = leg.spread( + target_npv=-15000000 + base_npv, + rate_curve=curve, + disc_curve=curve, + settlement=settlement, + forward=forward, + ) assert abs(result - expected) < 1e-3 leg.float_spread = result - assert abs(leg.npv(rate_curve=curve, disc_curve=curve) - base_npv + 15000000) < 2e2 + assert ( + abs( + leg.npv(rate_curve=curve, disc_curve=curve, forward=forward, settlement=settlement) + - base_npv + + 15000000 + ) + < 2e2 + ) def test_fixing_method_raises(self) -> None: with pytest.raises(ValueError, match="`fixing_method`"): @@ -926,6 +947,46 @@ def test_zero_float_leg_analytic_delta(self, curve) -> None: assert abs(result - expected) < 1e-3 + @pytest.mark.parametrize( + ("settlement", "forward", "exp"), + [ + (NoInput(0), NoInput(0), 0.71008), + (NoInput(0), dt(2023, 1, 1), -0.11739), + (dt(2026, 1, 1), NoInput(0), -2.40765), + ], + ) + def test_zero_float_spread_calc(self, settlement, forward, exp, curve) -> None: + rate_curve = curve.shift(25) + zfl = ZeroFloatLeg( + schedule=Schedule( + effective=dt(2022, 1, 1), + termination="5y", + payment_lag=0, + frequency="A", + ), + notional=-1e8, + convention="Act360", + fixing_method="ibor", + ) + tgt_npv = 25000000 * curve[dt(2027, 1, 1)] + result = zfl.spread( + target_npv=tgt_npv, + rate_curve=rate_curve, + disc_curve=curve, + settlement=settlement, + forward=forward, + ) + + zfl.float_spread = result + tested = zfl.local_npv( + rate_curve=rate_curve, + disc_curve=curve, + settlement=settlement, + forward=forward, + ) + assert abs(result / 100 - exp) < 1e-3 + assert abs(tgt_npv - tested) < 1e-3 + class TestZeroFixedLeg: @pytest.mark.parametrize( @@ -1030,7 +1091,15 @@ def test_zero_fixed_leg_analytic_delta(self, curve) -> None: result2 = zfl.analytic_delta(disc_curve=curve) assert abs(result2 + 45024.1974) < 1e-3 - def test_zero_fixed_spread(self, curve) -> None: + @pytest.mark.parametrize( + ("settlement", "forward", "exp"), + [ + (NoInput(0), NoInput(0), 2.50), + (NoInput(0), dt(2023, 1, 1), 2.404826), + (dt(2026, 1, 1), NoInput(0), 2.139550), + ], + ) + def test_zero_fixed_spread(self, settlement, forward, exp, curve) -> None: zfl = ZeroFixedLeg( schedule=Schedule( effective=dt(2022, 1, 1), @@ -1042,12 +1111,35 @@ def test_zero_fixed_spread(self, curve) -> None: convention="ActAct", fixed_rate=NoInput(0), ) - result = zfl._spread( + result = zfl.spread( target_npv=13140821.29 * curve[dt(2027, 1, 1)], rate_curve=NoInput(0), disc_curve=curve, + settlement=settlement, + forward=forward, + ) + assert abs(result / 100 - exp) < 1e-3 + + def test_zero_fixed_spread_raises_settlement(self, curve) -> None: + zfl = ZeroFixedLeg( + schedule=Schedule( + effective=dt(2022, 1, 1), + termination="5y", + payment_lag=0, + frequency="A", + ), + notional=-1e8, + convention="ActAct", + fixed_rate=NoInput(0), ) - assert abs(result / 100 - 2.50) < 1e-3 + with pytest.raises(ZeroDivisionError): + zfl.spread( + target_npv=13140821.29 * curve[dt(2027, 1, 1)], + rate_curve=NoInput(0), + disc_curve=curve, + settlement=dt(2029, 1, 1), + forward=NoInput(0), + ) def test_zero_fixed_spread_indexed(self, curve) -> None: zfl = ZeroFixedLeg( @@ -1063,7 +1155,7 @@ def test_zero_fixed_spread_indexed(self, curve) -> None: index_base=100.0, index_fixings=110.0, ) - result = zfl._spread( + result = zfl.spread( target_npv=13140821.29 * curve[dt(2027, 1, 1)], rate_curve=NoInput(0), disc_curve=curve, @@ -1085,7 +1177,7 @@ def test_zero_fixed_spread_non_deliverable(self, curve) -> None: pair="eurusd", fx_fixings=2.0, ) - result = zfl._spread( + result = zfl.spread( target_npv=13140821.29 * curve[dt(2027, 1, 1)], rate_curve=NoInput(0), disc_curve=curve, @@ -1551,7 +1643,15 @@ def test_non_deliverable(self, curve): # v2.5 - def test_fixed_leg_spread(self, curve) -> None: + @pytest.mark.parametrize( + ("settlement", "forward", "exp"), + [ + (NoInput(0), NoInput(0), 403.9491881327746), + (dt(2022, 3, 30), NoInput(0), 399.9990223763462), + (dt(2022, 4, 6), NoInput(0), 799.0147512470912), + ], + ) + def test_fixed_leg_spread(self, settlement, forward, exp, curve) -> None: fixed_leg = FixedLeg( schedule=Schedule( effective=dt(2022, 1, 1), @@ -1565,10 +1665,15 @@ def test_fixed_leg_spread(self, curve) -> None: fixed_rate=4.00, currency="usd", ) - result = fixed_leg._spread( - target_npv=20000000, disc_curve=curve, rate_curve=curve, index_curve=curve + result = fixed_leg.spread( + target_npv=20000000, + disc_curve=curve, + rate_curve=curve, + index_curve=curve, + settlement=settlement, + forward=forward, ) - assert abs(result - 403.9491881327746) < 1e-6 + assert abs(result - exp) < 1e-6 @pytest.mark.parametrize("initial", [True, False]) @pytest.mark.parametrize("final", [True, False]) @@ -1958,6 +2063,38 @@ def test_exchanges_raises(self, final): final_exchange=not final, ) + @pytest.mark.parametrize( + ("settlement", "forward", "exp"), + [ + (NoInput(0), NoInput(0), 408.02994815795125), + (dt(2022, 3, 30), NoInput(0), 404.03987718823055), + (dt(2022, 4, 6), NoInput(0), 811.1815703665554), + ], + ) + def test_fixed_leg_spread(self, settlement, forward, exp, curve) -> None: + fixed_leg = CreditPremiumLeg( + schedule=Schedule( + effective=dt(2022, 1, 1), + termination=dt(2022, 7, 1), + payment_lag=2, + payment_lag_exchange=1, + frequency="Q", + ), + notional=-1e9, + convention="Act360", + fixed_rate=4.00, + currency="usd", + ) + result = fixed_leg.spread( + target_npv=20000000, + disc_curve=curve, + rate_curve=curve, + index_curve=curve, + settlement=settlement, + forward=forward, + ) + assert abs(result - exp) < 1e-6 + class TestCreditProtectionLeg: def test_leg_analytic_delta(self, hazard_curve, curve) -> None: @@ -2653,7 +2790,7 @@ def test_mtm_leg_exchange_spread(self) -> None: rate_curve=fxf.curve("usd", "usd"), disc_curve=fxf.curve("usd", "usd"), fx=fxf ) # a_delta = leg.analytic_delta(fxf.curve("usd", "usd"), fxf.curve("usd", "usd"), fxf) - result = leg._spread( + result = leg.spread( target_npv=100, rate_curve=fxf.curve("usd", "usd"), disc_curve=fxf.curve("usd", "usd"),