Skip to content

Commit bc81200

Browse files
mikelyncattack68
andauthored
REF: review all the *Leg* spreadfunctions adding tests (#143) (#1064)
Co-authored-by: Mike Lync <[email protected]> Co-authored-by: JHM Darbyshire <[email protected]>
1 parent 04ee57d commit bc81200

File tree

6 files changed

+387
-150
lines changed

6 files changed

+387
-150
lines changed

python/rateslib/legs/components/credit.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,17 @@
1212
if TYPE_CHECKING:
1313
from rateslib.typing import ( # pragma: no cover
1414
FX_,
15+
CurveOption_,
1516
DualTypes,
1617
DualTypes_,
18+
FXForwards_,
19+
FXVolOption_,
1720
Schedule,
1821
_BaseCurve_,
1922
_SettlementParams,
2023
bool_,
2124
datetime,
25+
datetime_,
2226
str_,
2327
)
2428

@@ -171,6 +175,28 @@ def __init__(
171175
# self._exchange_periods = (None, None)
172176
# self._mtm_exchange_periods = None
173177

178+
def spread(
179+
self,
180+
*,
181+
target_npv: DualTypes,
182+
rate_curve: CurveOption_ = NoInput(0),
183+
index_curve: _BaseCurve_ = NoInput(0),
184+
disc_curve: _BaseCurve_ = NoInput(0),
185+
fx: FXForwards_ = NoInput(0),
186+
fx_vol: FXVolOption_ = NoInput(0),
187+
settlement: datetime_ = NoInput(0),
188+
forward: datetime_ = NoInput(0),
189+
) -> DualTypes:
190+
a_delta = self.local_analytic_delta(
191+
rate_curve=rate_curve,
192+
disc_curve=disc_curve,
193+
index_curve=index_curve,
194+
fx=fx,
195+
forward=forward,
196+
settlement=settlement,
197+
)
198+
return -target_npv / a_delta
199+
174200

175201
class CreditProtectionLeg(_BaseLeg):
176202
"""

python/rateslib/legs/components/fixed.py

Lines changed: 36 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
import rateslib.errors as err
77
from rateslib import defaults
8+
from rateslib.curves._parsers import (
9+
_disc_required_maybe_from_curve,
10+
)
811
from rateslib.data.fixings import _leg_fixings_to_list
912
from rateslib.enums.generics import NoInput, _drb
1013
from rateslib.legs.components.amortization import Amortization, _AmortizationType, _get_amortization
@@ -21,16 +24,15 @@
2124

2225
if TYPE_CHECKING:
2326
from rateslib.typing import ( # pragma: no cover
24-
FX_,
2527
CurveOption_,
2628
DualTypes,
2729
DualTypes_,
2830
FXForwards_,
31+
FXVolOption_,
2932
IndexMethod,
3033
LegFixings,
3134
Schedule,
3235
Series,
33-
_BaseCurve,
3436
_BaseCurve_,
3537
_SettlementParams,
3638
datetime,
@@ -654,53 +656,18 @@ def fx_delivery(i: int) -> datetime:
654656
else:
655657
self._mtm_exchange_periods = None
656658

657-
def _spread(
659+
def spread(
658660
self,
661+
*,
659662
target_npv: DualTypes,
660-
rate_curve: CurveOption_,
661-
disc_curve: _BaseCurve_,
662-
index_curve: _BaseCurve_,
663+
rate_curve: CurveOption_ = NoInput(0),
664+
index_curve: _BaseCurve_ = NoInput(0),
665+
disc_curve: _BaseCurve_ = NoInput(0),
663666
fx: FXForwards_ = NoInput(0),
664-
forward: datetime_ = NoInput(0),
667+
fx_vol: FXVolOption_ = NoInput(0),
665668
settlement: datetime_ = NoInput(0),
669+
forward: datetime_ = NoInput(0),
666670
) -> DualTypes:
667-
"""
668-
Calculates the ``fixed_rate`` to match a specific target NPV on the leg.
669-
670-
Parameters
671-
----------
672-
target_npv : float, Dual or Dual2
673-
The target NPV that an adjustment to the parameter will achieve. **Must
674-
be in local currency of the leg.**
675-
rate_curve : Curve or LineCurve
676-
The forecast curve passed to analytic delta calculation.
677-
disc_curve : Curve
678-
The discounting curve passed to analytic delta calculation.
679-
index_curve : _BaseCurve_
680-
The curve used for forecasting index values.
681-
fx : FXForwards, optional
682-
Required for multi-currency legs which are MTM exchanged.
683-
684-
Returns
685-
-------
686-
float, Dual, Dual2
687-
688-
Notes
689-
-----
690-
``FixedLeg`` and ``FloatLeg`` with a *"none_simple"* spread compound method have
691-
linear sensitivity to the spread. This can be calculated directly and
692-
exactly using an analytic delta calculation.
693-
694-
*"isda_compounding"* and *"isda_flat_compounding"* spread compound methods
695-
have non-linear sensitivity to the spread. This requires a root finding,
696-
iterative algorithm, which, coupled with very poor performance of calculating
697-
period rates under this method is exceptionally slow. We approximate this
698-
using first and second order AD and extrapolate a solution as a Taylor
699-
expansion. This results in approximation error.
700-
701-
Examples
702-
--------
703-
"""
704671
a_delta = self.local_analytic_delta(
705672
rate_curve=rate_curve,
706673
disc_curve=disc_curve,
@@ -866,19 +833,36 @@ def fixed_rate(self, value: DualTypes_) -> None:
866833
for period in self._regular_periods:
867834
period.rate_params.fixed_rate = value
868835

869-
def _spread(
836+
def spread(
870837
self,
838+
*,
871839
target_npv: DualTypes,
872-
rate_curve: CurveOption_,
873-
disc_curve: _BaseCurve,
840+
rate_curve: CurveOption_ = NoInput(0),
874841
index_curve: _BaseCurve_ = NoInput(0),
842+
disc_curve: _BaseCurve_ = NoInput(0),
875843
fx: FXForwards_ = NoInput(0),
844+
fx_vol: FXVolOption_ = NoInput(0),
845+
settlement: datetime_ = NoInput(0),
846+
forward: datetime_ = NoInput(0),
876847
) -> DualTypes:
877-
"""
878-
Overload the _spread calc to use analytic delta based on period rate
879-
"""
848+
disc_curve_ = _disc_required_maybe_from_curve(rate_curve, disc_curve)
880849

881-
unindexed_target_npv = target_npv / self._regular_periods[0].index_up(
850+
if not isinstance(settlement, NoInput):
851+
if settlement > self.settlement_params.ex_dividend:
852+
raise ZeroDivisionError(
853+
"A `spread` cannot be determined when the *Leg* always has zero value.\n"
854+
"The given `settlement` is after the `ex_dividend` date."
855+
)
856+
else:
857+
w_fwd = disc_curve_[_drb(settlement, forward)]
858+
else:
859+
if isinstance(forward, NoInput):
860+
w_fwd = 1.0
861+
else:
862+
w_fwd = disc_curve_[forward]
863+
864+
immediate_target_npv = target_npv * w_fwd
865+
unindexed_target_npv = immediate_target_npv / self._regular_periods[0].index_up(
882866
1.0, index_curve=index_curve
883867
)
884868
unindexed_reference_target_npv = unindexed_target_npv / self._regular_periods[
@@ -888,7 +872,7 @@ def _spread(
888872
f = self.schedule.periods_per_annum
889873
d = self._regular_periods[0].dcf
890874
N = self.settlement_params.notional
891-
w = disc_curve[self.settlement_params.payment]
875+
w = disc_curve_[self.settlement_params.payment]
892876
R = ((-unindexed_reference_target_npv / (N * w) + 1) ** (1 / (d * f)) - 1) * f * 10000.0
893877
return R
894878

@@ -998,15 +982,3 @@ def __init__(
998982
)
999983
self._exchange_periods = (_ini_cf,)
1000984
self._regular_periods = (_final_cf,)
1001-
1002-
def _spread(
1003-
self,
1004-
target_npv: DualTypes,
1005-
fore_curve: CurveOption_,
1006-
disc_curve: CurveOption_,
1007-
fx: FX_ = NoInput(0),
1008-
) -> DualTypes:
1009-
"""
1010-
Overload the _spread calc to use analytic delta based on period rate
1011-
"""
1012-
raise NotImplementedError()

python/rateslib/legs/components/float.py

Lines changed: 66 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,20 @@
2020

2121
if TYPE_CHECKING:
2222
from rateslib.typing import ( # pragma: no cover
23-
FX_,
2423
CurveOption_,
2524
DualTypes,
2625
DualTypes_,
2726
FloatRateSeries,
2827
Frequency,
2928
FXForwards_,
29+
FXVolOption_,
3030
IndexMethod,
3131
LegFixings,
3232
Schedule,
3333
_BaseCurve_,
3434
_BasePeriod,
3535
datetime,
36+
datetime_,
3637
int_,
3738
str_,
3839
)
@@ -367,82 +368,51 @@ def _is_linear(self) -> bool:
367368
return False
368369
return True
369370

370-
def _spread(
371+
def spread(
371372
self,
373+
*,
372374
target_npv: DualTypes,
373-
rate_curve: CurveOption_,
374-
disc_curve: _BaseCurve_,
375+
rate_curve: CurveOption_ = NoInput(0),
375376
index_curve: _BaseCurve_ = NoInput(0),
377+
disc_curve: _BaseCurve_ = NoInput(0),
376378
fx: FXForwards_ = NoInput(0),
379+
fx_vol: FXVolOption_ = NoInput(0),
380+
settlement: datetime_ = NoInput(0),
381+
forward: datetime_ = NoInput(0),
377382
) -> DualTypes:
378-
"""
379-
Calculates an adjustment to the ``fixed_rate`` or ``float_spread`` to match
380-
a specific target NPV.
381-
382-
Parameters
383-
----------
384-
target_npv : float, Dual or Dual2
385-
The target NPV that an adjustment to the parameter will achieve. **Must
386-
be in local currency of the leg.**
387-
rate_curve : Curve or LineCurve
388-
The forecast curve passed to analytic delta calculation.
389-
disc_curve : Curve
390-
The discounting curve passed to analytic delta calculation.
391-
fx : FXForwards, optional
392-
Required for multi-currency legs which are MTM exchanged.
393-
index_curve : _BaseCurve, optional
394-
The index curve used for forecasting index values.
395-
396-
Returns
397-
-------
398-
float, Dual, Dual2
399-
400-
Notes
401-
-----
402-
``FixedLeg`` and ``FloatLeg`` with a *"none_simple"* spread compound method have
403-
linear sensitivity to the spread. This can be calculated directly and
404-
exactly using an analytic delta calculation.
405-
406-
*"isda_compounding"* and *"isda_flat_compounding"* spread compound methods
407-
have non-linear sensitivity to the spread. This requires a root finding,
408-
iterative algorithm, which, coupled with very poor performance of calculating
409-
period rates under this method is exceptionally slow. We approximate this
410-
using first and second order AD and extrapolate a solution as a Taylor
411-
expansion. This results in approximation error.
412-
413-
Examples
414-
--------
415-
"""
416383
if self._is_linear:
417-
a_delta: DualTypes = self.local_analytic_delta(
384+
local_npv = self.local_npv(
418385
rate_curve=rate_curve,
419386
disc_curve=disc_curve,
420387
index_curve=index_curve,
421388
fx=fx,
389+
forward=forward,
390+
settlement=settlement,
422391
)
423-
return -target_npv / a_delta
424-
else:
425-
original_z = self.float_spread
426-
original_npv = self.npv(
392+
a_delta = self.local_analytic_delta(
427393
rate_curve=rate_curve,
428394
disc_curve=disc_curve,
429395
index_curve=index_curve,
430396
fx=fx,
397+
forward=forward,
398+
settlement=settlement,
431399
)
400+
return -(target_npv - local_npv) / a_delta
401+
else:
402+
original_z = self.float_spread
432403

433404
def s(g: DualTypes) -> DualTypes:
434405
"""
435406
This determines the NPV change subject to a given float spread change denoted, g.
436407
"""
437-
self.float_spread = g + original_z
438-
return (
439-
self.npv( # type: ignore[operator]
440-
rate_curve=rate_curve,
441-
disc_curve=disc_curve,
442-
index_curve=index_curve,
443-
fx=fx,
444-
)
445-
- original_npv
408+
self.float_spread = g
409+
return self.local_npv(
410+
rate_curve=rate_curve,
411+
disc_curve=disc_curve,
412+
index_curve=index_curve,
413+
fx=fx,
414+
forward=forward,
415+
settlement=settlement,
446416
)
447417

448418
result = ift_1dim(
@@ -620,14 +590,46 @@ def __init__(
620590
),
621591
)
622592

623-
def _spread(
593+
def spread(
624594
self,
595+
*,
625596
target_npv: DualTypes,
626-
fore_curve: CurveOption_,
627-
disc_curve: CurveOption_,
628-
fx: FX_ = NoInput(0),
597+
rate_curve: CurveOption_ = NoInput(0),
598+
index_curve: _BaseCurve_ = NoInput(0),
599+
disc_curve: _BaseCurve_ = NoInput(0),
600+
fx: FXForwards_ = NoInput(0),
601+
fx_vol: FXVolOption_ = NoInput(0),
602+
settlement: datetime_ = NoInput(0),
603+
forward: datetime_ = NoInput(0),
629604
) -> DualTypes:
630-
"""
631-
Overload the _spread calc to use analytic delta based on period rate
632-
"""
633-
raise NotImplementedError()
605+
original_z = self.float_spread
606+
607+
def s(g: DualTypes) -> DualTypes:
608+
"""
609+
This determines the NPV of the *Leg* subject to a given float spread change denoted, g.
610+
"""
611+
self.float_spread = g
612+
iteration_local_npv = self.local_npv(
613+
rate_curve=rate_curve,
614+
disc_curve=disc_curve,
615+
index_curve=index_curve,
616+
fx=fx,
617+
forward=forward,
618+
settlement=settlement,
619+
)
620+
return iteration_local_npv
621+
622+
result = ift_1dim(
623+
s=s,
624+
s_tgt=target_npv,
625+
h="ytm_quadratic",
626+
ini_h_args=(-300, 300, 1200),
627+
# h="modified_brent",
628+
# ini_h_args=(-10000, 10000),
629+
func_tol=1e-6,
630+
conv_tol=1e-6,
631+
)
632+
633+
self.float_spread = original_z
634+
_: DualTypes = result["g"]
635+
return _

0 commit comments

Comments
 (0)