diff --git a/pyproject.toml b/pyproject.toml index 4028e968..5f7bd8c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -154,21 +154,21 @@ exclude = [ # "/instruments/components/protocols", "/instruments/components/cds.py", "/instruments/components/fly.py", - "/instruments/components/fra.py", + # "/instruments/components/fra.py", "/instruments/components/fx_forward.py", "/instruments/components/fx_swap.py", "/instruments/components/fx_vol_value.py", - "/instruments/components/iirs.py", + # "/instruments/components/iirs.py", # "/instruments/components/irs.py", "/instruments/components/ndf.py", "/instruments/components/portfolio.py", - "/instruments/components/sbs.py", + # "/instruments/components/sbs.py", "/instruments/components/spread.py", "/instruments/components/stir_future.py", "/instruments/components/value.py", - "/instruments/components/xcs.py", - "/instruments/components/zcis.py", - "/instruments/components/zcs.py", + # "/instruments/components/xcs.py", + # "/instruments/components/zcis.py", + # "/instruments/components/zcs.py", ] strict = true #packages = [ diff --git a/python/rateslib/default.py b/python/rateslib/default.py index c2b1d747..1fab8181 100644 --- a/python/rateslib/default.py +++ b/python/rateslib/default.py @@ -60,7 +60,7 @@ def __init__(self) -> None: # Instrument parameterisation self.metric = { - "SBS": "float_spread", + "SBS": "leg1", } self.convention = "ACT360" self.notional = 1.0e6 diff --git a/python/rateslib/instruments/components/fra.py b/python/rateslib/instruments/components/fra.py index 0b4dd087..89140dc1 100644 --- a/python/rateslib/instruments/components/fra.py +++ b/python/rateslib/instruments/components/fra.py @@ -4,12 +4,13 @@ from rateslib import defaults from rateslib.dual.utils import _dual_float -from rateslib.enums.generics import NoInput, _drb +from rateslib.enums.generics import NoInput, Ok, Result, _drb from rateslib.enums.parameters import FloatFixingMethod, SpreadCompoundMethod from rateslib.instruments.components.protocols import _BaseInstrument from rateslib.instruments.components.protocols.kwargs import _convert_to_schedule_kwargs, _KWArgs from rateslib.instruments.components.protocols.pricing import ( _Curves, + _maybe_get_curve_maybe_from_solver, _maybe_get_curve_or_dict_maybe_from_solver, ) from rateslib.legs.components import FixedLeg, FloatLeg @@ -18,8 +19,7 @@ if TYPE_CHECKING: from rateslib.typing import ( # pragma: no cover CalInput, - CurveOption_, - Curves_, + CurvesT_, DataFrame, DualTypes, DualTypes_, @@ -30,6 +30,7 @@ FXVolOption_, RollDay, Solver_, + _BaseCurveOrDict_, _BaseLeg, bool_, datetime, @@ -44,71 +45,135 @@ class FRA(_BaseInstrument): A *forward rate agreement (FRA)* compositing a :class:`~rateslib.legs.components.FixedLeg` and :class:`~rateslib.legs.components.FloatLeg`. - Parameters - ---------- - fixed_rate : float or None - The fixed rate applied to the :class:`~rateslib.legs.FixedLeg`. If `None` - will be set to mid-market when curves are provided. - leg2_float_spread : float, optional - The spread applied to the :class:`~rateslib.legs.FloatLeg`. Can be set to - `None` and designated - later, perhaps after a mid-market spread for all periods has been calculated. - leg2_spread_compound_method : str, optional - The method to use for adding a floating spread to compounded rates. Available - options are `{"none_simple", "isda_compounding", "isda_flat_compounding"}`. - leg2_fixings : float, list, or Series optional - If a float scalar, will be applied as the determined fixing for the first - period. If a list of *n* fixings will be used as the fixings for the first *n* - periods. If any sublist of length *m* is given, is used as the first *m* RFR - fixings for that :class:`~rateslib.periods.FloatPeriod`. If a datetime - indexed ``Series`` will use the fixings that are available in that object, - and derive the rest from the ``curve``. - leg2_fixing_method : str, optional - The method by which floating rates are determined, set by default. See notes. - leg2_method_param : int, optional - A parameter that is used for the various ``fixing_method`` s. See notes. - - Notes - ----- - A *STIRFuture* is modelled as a single period *IRS* whose payment date is overloaded to always - result in immediate settlement, with the immediate date derived from the discount curve - used during pricing. - - Examples - -------- - Construct a curve to price the example. + These *Legs* have *Instrument* level overloads in order to satisfy the cashflow determination + conventions of a *FRA* instruments. + + .. rubric:: Examples + + .. ipython:: python + :suppress: + + from rateslib.instruments.components import FRA + from datetime import datetime as dt .. ipython:: python - usd = Curve( - nodes={ - dt(2022, 1, 1): 1.0, - dt(2023, 1, 1): 0.965, - dt(2024, 1, 1): 0.94 - }, - id="usd_stir" + fra = FRA( + effective=dt(2000, 1, 1), + termination="6m", + spec="eur_fra6", + fixed_rate=2.0, ) + fra.cashflows() - Create the *STIRFuture*, and demonstrate the :meth:`~rateslib.instruments.STIRFuture.rate`, - :meth:`~rateslib.instruments.STIRFuture.npv`, + .. rubric:: Pricing - .. ipython:: python - :suppress: + An *FRA* requires a *disc curve* on both legs (which should be the same *Curve*) and a + *leg2 rate curve* to forecast the IBOR type rate on the *FloatLeg*. The following input + formats are allowed: - from rateslib import STIRFuture + .. code-block:: python - .. ipython:: python + curves = curve | [curve] # a single curve is repeated for all required curves + curves = [rate_curve, disc_curve] # two curves are applied in the given order + curves = [None, disc_curve, rate_curve, disc_curve] # four curves applied to each leg + curves = {"leg2_rate_curve": rate_curve, "disc_curve": disc_curve} # dict form is explicit - stir = STIRFuture( - effective=dt(2022, 3, 16), - termination=dt(2022, 6, 15), - spec="usd_stir", - curves=usd, - price=99.50, - contracts=10, - ) - stir.rate(metric="price") - stir.npv() + .. role:: red + + .. role:: green + + Parameters + ---------- + . + + .. note:: + + The following define generalised **scheduling** parameters. + + effective : datetime, :red:`required` + The unadjusted effective date. If given as adjusted, unadjusted alternatives may be + inferred. + termination : datetime, str, :red:`required` + The unadjusted termination date. If given as adjusted, unadjusted alternatives may be + inferred. If given as string tenor will be calculated from ``effective``. + frequency : Frequency, str, :red:`required` + The frequency of the schedule. + If given as string will derive a :class:`~rateslib.scheduling.Frequency` aligning with: + monthly ("M"), quarterly ("Q"), semi-annually ("S"), annually("A") or zero-coupon ("Z"), or + a set number of calendar or business days ("_D", "_B"), weeks ("_W"), months ("_M") or + years ("_Y"). + Where required, the :class:`~rateslib.scheduling.RollDay` is derived as per ``roll`` + and business day calendar as per ``calendar``. + stub : StubInference, str in {"ShortFront", "LongFront", "ShortBack", "LongBack"}, :green:`optional` + The stub type used if stub inference is required. If given as string will derive a + :class:`~rateslib.scheduling.StubInference`. + front_stub : datetime, :green:`optional` + The unadjusted date for the start stub period. If given as adjusted, unadjusted + alternatives may be inferred. + back_stub : datetime, :green:`optional` + The unadjusted date for the back stub period. If given as adjusted, unadjusted + alternatives may be inferred. + See notes for combining ``stub``, ``front_stub`` and ``back_stub`` + and any automatic stub inference. + roll : RollDay, int in [1, 31], str in {"eom", "imm", "som"}, :green:`optional` + The roll day of the schedule. If not given or not available in ``frequency`` will be + inferred for monthly frequency variants. + eom : bool, :green:`optional` + Use an end of month preference rather than regular rolls for ``roll`` inference. Set by + default. Not required if ``roll`` is defined. + modifier : Adjuster, str in {"NONE", "F", "MF", "P", "MP"}, :green:`optional` + The :class:`~rateslib.scheduling.Adjuster` used for adjusting unadjusted schedule dates + into adjusted dates. If given as string must define simple date rolling rules. + calendar : calendar, str, :green:`optional` + The business day calendar object to use. If string will call + :meth:`~rateslib.scheduling.get_calendar`. + payment_lag: int, :green:`optional (set as 0)` + A number of business days by which to lag a traditional *FRA* payment date. + + .. warning:: + + *FRAs* are defined by a payment structure that has a cashflow at the accrual start + date and an amount adjusted by the rate fixing. An input to this parameter, say 5, + will apply an adjuster: `Adjuster.BusDaysLagSettleInAdvance(5)`. + + ex_div: int, :green:`optional (set as 0)` + Applied in the same manner as the ``payment_lag``, except negated. An input of 1 will + apply an adjuster: `Adjuster.BusDaysLagSettleInAdvance(-1)`. + convention: str, :green:`optional (set by 'defaults')` + The day count convention applied to calculations of period accrual dates. + See :meth:`~rateslib.scheduling.dcf`. + + .. note:: + + The following define generalised **settlement** parameters. + + currency : str, :green:`optional (set by 'defaults')` + The local settlement currency of the *Instrument* (3-digit code). + notional : float, Dual, Dual2, Variable, :green:`optional (set by 'defaults')` + The initial leg notional, defined in units of *reference currency*. + + .. note:: + + The following are **rate parameters**. + + fixed_rate : float or None + The fixed rate applied to the :class:`~rateslib.legs.FixedLeg`. If `None` + will be set to mid-market when curves are provided. + leg2_method_param: int, :green:`optional (set by 'defaults')` + A specific parameter that is used by the specific ``fixing_method``. + leg2_fixing_frequency: Frequency, str, :green:`optional (set by 'frequency' or '1B')` + The :class:`~rateslib.scheduling.Frequency` as a component of the + :class:`~rateslib.data.fixings.FloatRateIndex`. If not given is assumed to match the + frequency of the schedule. + leg2_fixing_series: FloatRateSeries, str, :green:`optional (implied by other parameters)` + The :class:`~rateslib.data.fixings.FloatRateSeries` as a component of the + :class:`~rateslib.data.fixings.FloatRateIndex`. If not given inherits attributes given + such as the ``calendar``, ``convention``, ``method_param`` etc. + leg2_rate_fixings: float, Dual, Dual2, Variable, Series, str, :green:`optional` + See XXX (working with fixings). + The value of the rate fixing. If a scalar, is used directly. If a string identifier, links + to the central ``fixings`` object and data loader. """ @@ -140,7 +205,7 @@ def legs(self) -> list[_BaseLeg]: """A list of the *Legs* of the *Instrument*.""" return self._legs - def _parse_curves(self, curves: CurveOption_) -> _Curves: + def _parse_curves(self, curves: CurvesT_) -> _Curves: """ An STIRFuture has two curve requirements: a leg2_rate_curve and a disc_curve used by both legs. @@ -188,11 +253,13 @@ def _parse_curves(self, curves: CurveOption_) -> _Curves: raise ValueError( f"{type(self).__name__} requires only 2 curve types. Got {len(curves)}." ) + elif isinstance(curves, _Curves): + return curves else: # `curves` is just a single input which is copied across all curves return _Curves( - leg2_rate_curve=curves, - disc_curve=curves, - leg2_disc_curve=curves, + leg2_rate_curve=curves, # type: ignore[arg-type] + disc_curve=curves, # type: ignore[arg-type] + leg2_disc_curve=curves, # type: ignore[arg-type] ) def __init__( @@ -207,7 +274,7 @@ def __init__( modifier: str_ = NoInput(0), calendar: CalInput = NoInput(0), payment_lag: int = 0, - ex_div: int_ = NoInput(0), + ex_div: int = 0, convention: str_ = NoInput(0), # settlement parameters currency: str_ = NoInput(0), @@ -219,7 +286,7 @@ def __init__( leg2_fixing_frequency: Frequency | str_ = NoInput(0), leg2_fixing_series: FloatRateSeries | str_ = NoInput(0), # meta parameters - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), spec: str_ = NoInput(0), metric: str_ = NoInput(0), ) -> None: @@ -232,8 +299,6 @@ def __init__( eom=eom, modifier=modifier, calendar=calendar, - payment_lag=payment_lag, - ex_div=ex_div, convention=convention, # settlement currency=currency, @@ -265,6 +330,7 @@ def __init__( leg2_notional=NoInput.negate, leg2_currency=NoInput.inherit, payment_lag=Adjuster.BusDaysLagSettleInAdvance(payment_lag), + ex_div=Adjuster.BusDaysLagSettleInAdvance(-ex_div), initial_exchange=False, final_exchange=False, leg2_initial_exchange=False, @@ -291,14 +357,23 @@ def __init__( f"one regular period. Got '{self.leg1.schedule.n_periods}'." ) - def _fra_rate_scalar(self, leg2_rate_curve) -> DualTypes_: + def _fra_rate_scalar(self, leg2_rate_curve: _BaseCurveOrDict_) -> DualTypes: r = self.leg2._regular_periods[0].rate(rate_curve=leg2_rate_curve) return 1 / (1 + self.leg2._regular_periods[0].period_params.dcf * r / 100.0) + def _try_fra_rate_scalar(self, leg2_rate_curve: _BaseCurveOrDict_) -> Result[DualTypes]: + r = self.leg2._regular_periods[0].try_rate(rate_curve=leg2_rate_curve) + if r.is_err: + return r + else: + return Ok( + 1 / (1 + self.leg2._regular_periods[0].period_params.dcf * r.unwrap() / 100.0) + ) + def npv( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -330,14 +405,14 @@ def npv( settlement=settlement, forward=forward, ) - if local: + if isinstance(npv, dict): return {k: v * fra_scalar for k, v in npv.items()} else: return npv * fra_scalar def _set_pricing_mid( self, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), settlement: datetime_ = NoInput(0), forward: datetime_ = NoInput(0), @@ -356,7 +431,7 @@ def _set_pricing_mid( def rate( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -364,7 +439,7 @@ def rate( settlement: datetime_ = NoInput(0), forward: datetime_ = NoInput(0), metric: str_ = NoInput(0), - ) -> DualTypes_: + ) -> DualTypes: _curves = self._parse_curves(curves) metric_ = _drb(self.kwargs.meta["metric"], metric).lower() @@ -372,7 +447,7 @@ def rate( rate_curve=_maybe_get_curve_or_dict_maybe_from_solver( self.kwargs.meta["curves"], _curves, "leg2_rate_curve", solver ), - disc_curve=_maybe_get_curve_or_dict_maybe_from_solver( + disc_curve=_maybe_get_curve_maybe_from_solver( self.kwargs.meta["curves"], _curves, "leg2_disc_curve", solver ), settlement=settlement, @@ -382,7 +457,7 @@ def rate( self.leg1.spread( target_npv=-leg2_npv, rate_curve=NoInput(0), - disc_curve=_maybe_get_curve_or_dict_maybe_from_solver( + disc_curve=_maybe_get_curve_maybe_from_solver( self.kwargs.meta["curves"], _curves, "disc_curve", solver ), index_curve=NoInput(0), @@ -399,7 +474,7 @@ def rate( def analytic_delta( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -415,7 +490,7 @@ def analytic_delta( solver=solver, curves_meta=_curves_meta, curves=_curves, name="leg2_rate_curve" ) fra_scalar = self._fra_rate_scalar(leg2_rate_curve=leg2_rate_curve) - return fra_scalar * super().analytic_delta( + a_delta = super().analytic_delta( curves=curves, solver=solver, fx=fx, @@ -426,11 +501,15 @@ def analytic_delta( forward=forward, leg=leg, ) + if isinstance(a_delta, dict): + return {k: v * fra_scalar for k, v in a_delta.items()} + else: + return a_delta * fra_scalar def local_analytic_rate_fixings( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -456,7 +535,7 @@ def local_analytic_rate_fixings( def cashflows( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -479,7 +558,7 @@ def cashflows( leg2_rate_curve = _maybe_get_curve_or_dict_maybe_from_solver( solver=solver, curves_meta=_curves_meta, curves=_curves, name="leg2_rate_curve" ) - scalar = _dual_float(self._fra_rate_scalar(leg2_rate_curve=leg2_rate_curve)) + scalar = self._try_fra_rate_scalar(leg2_rate_curve=leg2_rate_curve) headers = [ defaults.headers["cashflow"], @@ -487,6 +566,8 @@ def cashflows( defaults.headers["npv_fx"], ] for header in headers: - df[header] = df[header] * scalar - + if scalar.is_err: + df[header] = None + else: + df[header] = df[header] * _dual_float(scalar.unwrap()) return df diff --git a/python/rateslib/instruments/components/fx_options/fx_option.py b/python/rateslib/instruments/components/fx_options/fx_option.py index f5631fc1..de4c5465 100644 --- a/python/rateslib/instruments/components/fx_options/fx_option.py +++ b/python/rateslib/instruments/components/fx_options/fx_option.py @@ -400,7 +400,7 @@ def _set_strike_and_vol( # will determine the strike from % delta or ATM-delta string method = self.kwargs["strike"].lower() if method == "atm_delta": - self._pricing.delta_index, self._pricing.vol, self._pricing.k = self.periods[ + (self._pricing.delta_index, self._pricing.vol, self._pricing.k) = self.periods[ 0 ]._index_vol_and_strike_from_atm( delta_type=self.periods[0].fx_option_params.delta_type, diff --git a/python/rateslib/instruments/components/iirs.py b/python/rateslib/instruments/components/iirs.py index 577e1409..28fa856a 100644 --- a/python/rateslib/instruments/components/iirs.py +++ b/python/rateslib/instruments/components/iirs.py @@ -9,6 +9,7 @@ from rateslib.instruments.components.protocols.kwargs import _convert_to_schedule_kwargs, _KWArgs from rateslib.instruments.components.protocols.pricing import ( _Curves, + _maybe_get_curve_maybe_from_solver, _maybe_get_curve_or_dict_maybe_from_solver, ) from rateslib.legs.components import FixedLeg, FloatLeg @@ -16,8 +17,7 @@ if TYPE_CHECKING: from rateslib.typing import ( # pragma: no cover CalInput, - CurveOption_, - Curves_, + CurvesT_, DataFrame, DualTypes, DualTypes_, @@ -40,6 +40,196 @@ class IIRS(_BaseInstrument): + """ + An *indexed interest rate swap (IIRS)* composing a :class:`~rateslib.legs.components.FixedLeg` + and a :class:`~rateslib.legs.components.FloatLeg`. + + .. rubric:: Examples + + .. ipython:: python + :suppress: + + from rateslib.instruments.components import IIRS + from rateslib import fixings + from datetime import datetime as dt + + .. ipython:: python + + fixings.add("CPI_UK", Series(index=[dt(1999, 10, 1), dt(1999, 11, 1)], data=[110.0, 112.0])) + iirs = IIRS( + effective=dt(2000, 1, 1), + termination="2y", + frequency="A", + leg2_frequency="S", + index_fixings="CPI_UK", + index_lag=3, + fixed_rate=2.0, + ) + iirs.cashflows() + + .. ipython:: python + :suppress: + + fixings.pop("CPI_UK") + + .. rubric:: Pricing + + An *IIRS* requires a *disc curve* on both legs (which should be the same *Curve*), an + *index curve* for index forecasting on the *FixedLeg*, and a + *leg2 rate curve* to forecast rates on the *FloatLeg*. The following input formats are + allowed: + + .. code-block:: python + + curves = [index_curve, disc_curve, leg2_rate_curve] # three curves are applied in order + curves = [index_curve, disc_curve, leg2_rate_curve, disc_curve] # four curves applied to each leg + curves = { # dict form is explicit + "leg2_rate_curve": leg2_rate_curve, + "disc_curve": disc_curve, + "index_curve": index_curve, + } + + .. role:: red + + .. role:: green + + Parameters + ---------- + . + + .. note:: + + The following define generalised **scheduling** parameters. + + effective : datetime, :red:`required` + The unadjusted effective date. If given as adjusted, unadjusted alternatives may be + inferred. + termination : datetime, str, :red:`required` + The unadjusted termination date. If given as adjusted, unadjusted alternatives may be + inferred. If given as string tenor will be calculated from ``effective``. + frequency : Frequency, str, :red:`required` + The frequency of the schedule. + If given as string will derive a :class:`~rateslib.scheduling.Frequency` aligning with: + monthly ("M"), quarterly ("Q"), semi-annually ("S"), annually("A") or zero-coupon ("Z"), or + a set number of calendar or business days ("_D", "_B"), weeks ("_W"), months ("_M") or + years ("_Y"). + Where required, the :class:`~rateslib.scheduling.RollDay` is derived as per ``roll`` + and business day calendar as per ``calendar``. + stub : StubInference, str in {"ShortFront", "LongFront", "ShortBack", "LongBack"}, :green:`optional` + The stub type used if stub inference is required. If given as string will derive a + :class:`~rateslib.scheduling.StubInference`. + front_stub : datetime, :green:`optional` + The unadjusted date for the start stub period. If given as adjusted, unadjusted + alternatives may be inferred. + back_stub : datetime, :green:`optional` + The unadjusted date for the back stub period. If given as adjusted, unadjusted + alternatives may be inferred. + See notes for combining ``stub``, ``front_stub`` and ``back_stub`` + and any automatic stub inference. + roll : RollDay, int in [1, 31], str in {"eom", "imm", "som"}, :green:`optional` + The roll day of the schedule. If not given or not available in ``frequency`` will be + inferred for monthly frequency variants. + eom : bool, :green:`optional` + Use an end of month preference rather than regular rolls for ``roll`` inference. Set by + default. Not required if ``roll`` is defined. + modifier : Adjuster, str in {"NONE", "F", "MF", "P", "MP"}, :green:`optional` + The :class:`~rateslib.scheduling.Adjuster` used for adjusting unadjusted schedule dates + into adjusted dates. If given as string must define simple date rolling rules. + calendar : calendar, str, :green:`optional` + The business day calendar object to use. If string will call + :meth:`~rateslib.scheduling.get_calendar`. + payment_lag: Adjuster, int, :green:`optional` + The :class:`~rateslib.scheduling.Adjuster` to use to map adjusted schedule dates into + a payment date. If given as integer will define the number of business days to + lag payments by. + payment_lag_exchange: Adjuster, int, :green:`optional` + The :class:`~rateslib.scheduling.Adjuster` to use to map adjusted schedule dates into + additional payment date. If given as integer will define the number of business days to + lag payments by. + ex_div: Adjuster, int, :green:`optional` + The :class:`~rateslib.scheduling.Adjuster` to use to map adjusted schedule dates into + additional dates, which may be used, for example by fixings schedules. If given as integer + will define the number of business days to lag dates by. + convention: str, :green:`optional (set by 'defaults')` + The day count convention applied to calculations of period accrual dates. + See :meth:`~rateslib.scheduling.dcf`. + leg2_effective : datetime, :green:`optional (inherited from leg1)` + leg2_termination : datetime, str, :green:`optional (inherited from leg1)` + leg2_frequency : Frequency, str, :green:`optional (inherited from leg1)` + leg2_stub : StubInference, str, :green:`optional (inherited from leg1)` + leg2_front_stub : datetime, :green:`optional (inherited from leg1)` + leg2_back_stub : datetime, :green:`optional (inherited from leg1)` + leg2_roll : RollDay, int, str, :green:`optional (inherited from leg1)` + leg2_eom : bool, :green:`optional (inherited from leg1)` + leg2_modifier : Adjuster, str, :green:`optional (inherited from leg1)` + leg2_calendar : calendar, str, :green:`optional (inherited from leg1)` + leg2_payment_lag: Adjuster, int, :green:`optional (inherited from leg1)` + leg2_payment_lag_exchange: Adjuster, int, :green:`optional (inherited from leg1)` + leg2_ex_div: Adjuster, int, :green:`optional (inherited from leg1)` + leg2_convention: str, :green:`optional (inherited from leg1)` + + .. note:: + + The following define generalised **settlement** parameters. + + currency : str, :green:`optional (set by 'defaults')` + The local settlement currency of the *Instrument* (3-digit code). + notional_exchange: bool, :green:`optional (set as False)` + Whether to include a final notional exchange on both legs, which affects the PV since + the *FixedLeg* has an *indexed* cashflow. + notional : float, Dual, Dual2, Variable, :green:`optional (set by 'defaults')` + The initial leg notional, defined in units of *reference currency*. + amortization: float, Dual, Dual2, Variable, str, Amortization, :green:`optional (set as zero)` + Set a non-constant notional per *Period*. If a scalar value, adjusts the ``notional`` of + each successive period by that same value. Should have + sign equal to that of notional if the notional is to reduce towards zero. + leg2_notional : float, Dual, Dual2, Variable, :green:`optional (negatively inherited from leg1)` + leg2_amortization : float, Dual, Dual2, Variable, str, Amortization, :green:`optional (negatively inherited from leg1)` + + .. note:: + + The following are **rate parameters**. + + fixed_rate : float or None + The fixed rate applied to the :class:`~rateslib.legs.FixedLeg`. If `None` + will be set to mid-market when curves are provided. + leg2_fixing_method: FloatFixingMethod, str, :green:`optional (set by 'defaults')` + The :class:`~rateslib.enums.parameters.FloatFixingMethod` describing the determination + of the floating rate for each period. + leg2_method_param: int, :green:`optional (set by 'defaults')` + A specific parameter that is used by the specific ``fixing_method``. + leg2_fixing_frequency: Frequency, str, :green:`optional (set by 'frequency' or '1B')` + The :class:`~rateslib.scheduling.Frequency` as a component of the + :class:`~rateslib.data.fixings.FloatRateIndex`. If not given is assumed to match the + frequency of the schedule for an IBOR type ``fixing_method`` or '1B' if RFR type. + leg2_fixing_series: FloatRateSeries, str, :green:`optional (implied by other parameters)` + The :class:`~rateslib.data.fixings.FloatRateSeries` as a component of the + :class:`~rateslib.data.fixings.FloatRateIndex`. If not given inherits attributes given + such as the ``calendar``, ``convention``, ``method_param`` etc. + leg2_float_spread: float, Dual, Dual2, Variable, :green:`optional (set as 0.0)` + The amount (in bps) added to the rate in each period rate determination. + leg2_spread_compound_method: SpreadCompoundMethod, str, :green:`optional (set by 'defaults')` + The :class:`~rateslib.enums.parameters.SpreadCompoundMethod` used in the calculation + of the period rate when combining a ``float_spread``. Used **only** with RFR type + ``fixing_method``. + leg2_rate_fixings: float, Dual, Dual2, Variable, Series, str, :green:`optional` + See XXX (working with fixings). + The value of the rate fixing. If a scalar, is used directly. If a string identifier, links + to the central ``fixings`` object and data loader. + + .. note:: + + The following are **meta parameters**. + + curves : _BaseCurve, str, dict, _Curves, Sequence, :green:`optional` + Pricing objects passed directly to the *Instrument's* methods' ``curves`` argument. See + **Pricing**. + spec: str, :green:`optional` + A collective group of parameters. See + :ref:`default argument specifications `. + + """ # noqa: E501 + _rate_scalar = 1.0 @property @@ -51,14 +241,6 @@ def fixed_rate(self, value: DualTypes_) -> None: self.kwargs.leg1["fixed_rate"] = value self.leg1.fixed_rate = value - # @property - # def float_spread(self) -> NoReturn: - # raise AttributeError(f"Attribute not available on {type(self).__name__}") - # - # @property - # def leg2_fixed_rate(self) -> NoReturn: - # raise AttributeError(f"Attribute not available on {type(self).__name__}") - @property def leg2_float_spread(self) -> DualTypes_: return self.leg2.float_spread @@ -89,12 +271,6 @@ def __init__( termination: datetime | str_ = NoInput(0), frequency: Frequency | str_ = NoInput(0), *, - fixed_rate: DualTypes_ = NoInput(0), - notional_exchange: bool = False, - index_base: DualTypes_ = NoInput(0), - index_lag: int_ = NoInput(0), - index_method: IndexMethod | str_ = NoInput(0), - index_fixings: Series[DualTypes] | str_ = NoInput(0), stub: str_ = NoInput(0), front_stub: datetime_ = NoInput(0), back_stub: datetime_ = NoInput(0), @@ -105,15 +281,7 @@ def __init__( payment_lag: int_ = NoInput(0), payment_lag_exchange: int_ = NoInput(0), ex_div: int_ = NoInput(0), - notional: float_ = NoInput(0), - currency: str_ = NoInput(0), - amortization: float_ = NoInput(0), convention: str_ = NoInput(0), - leg2_float_spread: DualTypes_ = NoInput(0), - leg2_spread_compound_method: str_ = NoInput(0), - leg2_rate_fixings: FixingsRates_ = NoInput(0), # type: ignore[type-var] - leg2_fixing_method: str_ = NoInput(0), - leg2_method_param: int_ = NoInput(0), leg2_effective: datetime_ = NoInput(1), leg2_termination: datetime | str_ = NoInput(1), leg2_frequency: Frequency | str_ = NoInput(1), @@ -126,11 +294,29 @@ def __init__( leg2_calendar: CalInput = NoInput(1), leg2_payment_lag: int_ = NoInput(1), leg2_payment_lag_exchange: int_ = NoInput(1), - leg2_notional: float_ = NoInput(-1), - leg2_amortization: float_ = NoInput(-1), leg2_convention: str_ = NoInput(1), leg2_ex_div: int_ = NoInput(1), - curves: Curves_ = NoInput(0), + # settlement params + currency: str_ = NoInput(0), + notional_exchange: bool = False, + notional: float_ = NoInput(0), + amortization: float_ = NoInput(0), + leg2_notional: float_ = NoInput(-1), + leg2_amortization: float_ = NoInput(-1), + # index params + index_base: DualTypes_ = NoInput(0), + index_lag: int_ = NoInput(0), + index_method: IndexMethod | str_ = NoInput(0), + index_fixings: Series[DualTypes] | str_ = NoInput(0), + # rate params + fixed_rate: DualTypes_ = NoInput(0), + leg2_float_spread: DualTypes_ = NoInput(0), + leg2_spread_compound_method: str_ = NoInput(0), + leg2_rate_fixings: FixingsRates_ = NoInput(0), # type: ignore[type-var] + leg2_fixing_method: str_ = NoInput(0), + leg2_method_param: int_ = NoInput(0), + # meta params + curves: CurvesT_ = NoInput(0), spec: str_ = NoInput(0), ) -> None: user_args = dict( @@ -208,7 +394,7 @@ def __init__( def rate( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -216,14 +402,14 @@ def rate( settlement: datetime_ = NoInput(0), forward: datetime_ = NoInput(0), metric: str_ = NoInput(0), - ) -> DualTypes_: + ) -> DualTypes: _curves = self._parse_curves(curves) leg2_npv: DualTypes = self.leg2.local_npv( rate_curve=_maybe_get_curve_or_dict_maybe_from_solver( self.kwargs.meta["curves"], _curves, "leg2_rate_curve", solver ), - disc_curve=_maybe_get_curve_or_dict_maybe_from_solver( + disc_curve=_maybe_get_curve_maybe_from_solver( self.kwargs.meta["curves"], _curves, "leg2_disc_curve", solver ), index_curve=NoInput(0), @@ -249,10 +435,10 @@ def rate( self.leg1.spread( target_npv=-leg2_npv, # - leg1_npv, rate_curve=NoInput(0), - disc_curve=_maybe_get_curve_or_dict_maybe_from_solver( + disc_curve=_maybe_get_curve_maybe_from_solver( self.kwargs.meta["curves"], _curves, "disc_curve", solver ), - index_curve=_maybe_get_curve_or_dict_maybe_from_solver( + index_curve=_maybe_get_curve_maybe_from_solver( self.kwargs.meta["curves"], _curves, "index_curve", solver ), settlement=settlement, @@ -264,7 +450,7 @@ def rate( def spread( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -276,10 +462,10 @@ def spread( leg1_npv: DualTypes = self.leg1.local_npv( rate_curve=NoInput(0), - disc_curve=_maybe_get_curve_or_dict_maybe_from_solver( + disc_curve=_maybe_get_curve_maybe_from_solver( self.kwargs.meta["curves"], _curves, "disc_curve", solver ), - index_curve=_maybe_get_curve_or_dict_maybe_from_solver( + index_curve=_maybe_get_curve_maybe_from_solver( self.kwargs.meta["curves"], _curves, "index_curve", solver ), settlement=settlement, @@ -290,7 +476,7 @@ def spread( rate_curve=_maybe_get_curve_or_dict_maybe_from_solver( self.kwargs.meta["curves"], _curves, "leg2_rate_curve", solver ), - disc_curve=_maybe_get_curve_or_dict_maybe_from_solver( + disc_curve=_maybe_get_curve_maybe_from_solver( self.kwargs.meta["curves"], _curves, "leg2_disc_curve", solver ), index_curve=NoInput(0), @@ -301,7 +487,7 @@ def spread( def npv( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -329,7 +515,7 @@ def npv( def _set_pricing_mid( self, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), settlement: datetime_ = NoInput(0), forward: datetime_ = NoInput(0), @@ -345,12 +531,10 @@ def _set_pricing_mid( ) self.leg1.fixed_rate = _dual_float(mid_market_rate) - def _parse_curves(self, curves: CurveOption_) -> _Curves: + def _parse_curves(self, curves: CurvesT_) -> _Curves: """ An IIRS has three curve requirements: an index_curve, a leg2_rate_curve and a disc_curve used by both legs. - - When given as 2 elements the first is treated as the rate curve and the 2nd as disc curve. """ if isinstance(curves, NoInput): return _Curves() @@ -375,13 +559,6 @@ def _parse_curves(self, curves: CurveOption_) -> _Curves: leg2_rate_curve=curves[2], leg2_disc_curve=curves[1], ) - elif len(curves) == 2: - return _Curves( - disc_curve=curves[1], - index_curve=curves[0], - leg2_rate_curve=curves[1], - leg2_disc_curve=curves[1], - ) elif len(curves) == 4: return _Curves( disc_curve=curves[1], @@ -393,14 +570,15 @@ def _parse_curves(self, curves: CurveOption_) -> _Curves: raise ValueError( f"{type(self).__name__} requires 3 curve types. Got {len(curves)}." ) - + elif isinstance(curves, _Curves): + return curves else: # `curves` is just a single input which is copied across all curves raise ValueError(f"{type(self).__name__} requires 3 curve types. Got 1.") def cashflows( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -421,7 +599,7 @@ def cashflows( def local_analytic_rate_fixings( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), diff --git a/python/rateslib/instruments/components/irs.py b/python/rateslib/instruments/components/irs.py index 104c5981..76f0057b 100644 --- a/python/rateslib/instruments/components/irs.py +++ b/python/rateslib/instruments/components/irs.py @@ -72,7 +72,7 @@ class IRS(_BaseInstrument): curves = curve | [curve] # a single curve is repeated for all required curves curves = [rate_curve, disc_curve] # two curves are applied in the given order curves = [None, disc_curve, rate_curve, disc_curve] # four curves applied to each leg - curves = {"leg2_rate_curve": rate_curve, "disc_curve"} # dict form is explicit + curves = {"leg2_rate_curve": rate_curve, "disc_curve": disc_curve} # dict form is explicit .. role:: red @@ -204,7 +204,8 @@ class IRS(_BaseInstrument): The following are **meta parameters**. curves : _BaseCurve, str, dict, _Curves, Sequence, :green:`optional` - Pricing objects passed directly to the *Instrument's* methods' ``curves`` argument. + Pricing objects passed directly to the *Instrument's* methods' ``curves`` argument. See + **Pricing**. spec: str, :green:`optional` A collective group of parameters. See :ref:`default argument specifications `. @@ -224,16 +225,6 @@ def fixed_rate(self, value: DualTypes_) -> None: self.kwargs.leg1["fixed_rate"] = value self.leg1.fixed_rate = value - # @property - # def float_spread(self) -> NoReturn: - # """The float spread parameter of the composited - # :class:`~rateslib.legs.components.FloatLeg`.""" - # raise AttributeError(f"Attribute not available on {type(self).__name__}") - - # @property - # def leg2_fixed_rate(self) -> NoReturn: - # raise AttributeError(f"Attribute not available on {type(self).__name__}") - @property def leg2_float_spread(self) -> DualTypes_: """The float spread parameter of the composited diff --git a/python/rateslib/instruments/components/protocols/analytic_delta.py b/python/rateslib/instruments/components/protocols/analytic_delta.py index 5cb87543..c475b13b 100644 --- a/python/rateslib/instruments/components/protocols/analytic_delta.py +++ b/python/rateslib/instruments/components/protocols/analytic_delta.py @@ -16,6 +16,7 @@ DualTypes, FXForwards_, FXVolOption_, + Sequence, Solver_, _BaseLeg, _Curves, @@ -34,7 +35,7 @@ class _WithAnalyticDelta(_WithPricingObjs, Protocol): def kwargs(self) -> _KWArgs: ... @property - def legs(self) -> list[_BaseLeg]: ... + def legs(self) -> Sequence[_BaseLeg]: ... def analytic_delta( self, diff --git a/python/rateslib/instruments/components/sbs.py b/python/rateslib/instruments/components/sbs.py index 141ac46a..c9f1cbf0 100644 --- a/python/rateslib/instruments/components/sbs.py +++ b/python/rateslib/instruments/components/sbs.py @@ -9,6 +9,7 @@ from rateslib.instruments.components.protocols.kwargs import _convert_to_schedule_kwargs, _KWArgs from rateslib.instruments.components.protocols.pricing import ( _Curves, + _maybe_get_curve_maybe_from_solver, _maybe_get_curve_or_dict_maybe_from_solver, ) from rateslib.legs.components import FloatLeg @@ -16,8 +17,7 @@ if TYPE_CHECKING: from rateslib.typing import ( # pragma: no cover CalInput, - CurveOption_, - Curves_, + CurvesT_, DataFrame, DualTypes, DualTypes_, @@ -26,6 +26,7 @@ FXForwards_, FXVolOption_, RollDay, + Sequence, Solver_, _BaseLeg, bool_, @@ -38,14 +39,197 @@ class SBS(_BaseInstrument): - _rate_scalar = 100.0 + """ + An *single currency basis swap (SBS)* composing a :class:`~rateslib.legs.components.FloatLeg` + and a :class:`~rateslib.legs.components.FloatLeg`. + + .. rubric:: Examples + + .. ipython:: python + :suppress: + + from rateslib.instruments.components import SBS + from datetime import datetime as dt + + .. ipython:: python + + sbs = SBS( + effective=dt(2000, 1, 1), + termination="1y", + spec="eur_sbs36", + float_spread=9.5, + ) + sbs.cashflows() + + .. rubric:: Pricing + + An *SBS* requires a *disc curve* on both legs (which should be the same *Curve*) and a + *rate curve* and *leg2 rate curve* to forecast rates on each *FloatLeg*. The following input + formats are allowed: + + .. code-block:: python + + curves = [rate_curve, disc_curve, leg2_rate_curve] # three curves + curves = [rate_curve, disc_curve, leg2_rate_curve, disc_curve] # four curves + curves = { # dict form is explicit + "rate_curve": rate_curve, + "disc_curve": disc_curve, + "leg2_rate_curve": leg2_rate_curve, + } + + The available pricing ``metric`` are in *{'leg1', 'leg2'}* which will return a *float spread* + on the specified leg. + + .. role:: red + + .. role:: green + + Parameters + ---------- + . + + .. note:: + + The following define generalised **scheduling** parameters. + + effective : datetime, :red:`required` + The unadjusted effective date. If given as adjusted, unadjusted alternatives may be + inferred. + termination : datetime, str, :red:`required` + The unadjusted termination date. If given as adjusted, unadjusted alternatives may be + inferred. If given as string tenor will be calculated from ``effective``. + frequency : Frequency, str, :red:`required` + The frequency of the schedule. + If given as string will derive a :class:`~rateslib.scheduling.Frequency` aligning with: + monthly ("M"), quarterly ("Q"), semi-annually ("S"), annually("A") or zero-coupon ("Z"), or + a set number of calendar or business days ("_D", "_B"), weeks ("_W"), months ("_M") or + years ("_Y"). + Where required, the :class:`~rateslib.scheduling.RollDay` is derived as per ``roll`` + and business day calendar as per ``calendar``. + stub : StubInference, str in {"ShortFront", "LongFront", "ShortBack", "LongBack"}, :green:`optional` + The stub type used if stub inference is required. If given as string will derive a + :class:`~rateslib.scheduling.StubInference`. + front_stub : datetime, :green:`optional` + The unadjusted date for the start stub period. If given as adjusted, unadjusted + alternatives may be inferred. + back_stub : datetime, :green:`optional` + The unadjusted date for the back stub period. If given as adjusted, unadjusted + alternatives may be inferred. + See notes for combining ``stub``, ``front_stub`` and ``back_stub`` + and any automatic stub inference. + roll : RollDay, int in [1, 31], str in {"eom", "imm", "som"}, :green:`optional` + The roll day of the schedule. If not given or not available in ``frequency`` will be + inferred for monthly frequency variants. + eom : bool, :green:`optional` + Use an end of month preference rather than regular rolls for ``roll`` inference. Set by + default. Not required if ``roll`` is defined. + modifier : Adjuster, str in {"NONE", "F", "MF", "P", "MP"}, :green:`optional` + The :class:`~rateslib.scheduling.Adjuster` used for adjusting unadjusted schedule dates + into adjusted dates. If given as string must define simple date rolling rules. + calendar : calendar, str, :green:`optional` + The business day calendar object to use. If string will call + :meth:`~rateslib.scheduling.get_calendar`. + payment_lag: Adjuster, int, :green:`optional` + The :class:`~rateslib.scheduling.Adjuster` to use to map adjusted schedule dates into + a payment date. If given as integer will define the number of business days to + lag payments by. + payment_lag_exchange: Adjuster, int, :green:`optional` + The :class:`~rateslib.scheduling.Adjuster` to use to map adjusted schedule dates into + additional payment date. If given as integer will define the number of business days to + lag payments by. + ex_div: Adjuster, int, :green:`optional` + The :class:`~rateslib.scheduling.Adjuster` to use to map adjusted schedule dates into + additional dates, which may be used, for example by fixings schedules. If given as integer + will define the number of business days to lag dates by. + convention: str, :green:`optional (set by 'defaults')` + The day count convention applied to calculations of period accrual dates. + See :meth:`~rateslib.scheduling.dcf`. + leg2_effective : datetime, :green:`optional (inherited from leg1)` + leg2_termination : datetime, str, :green:`optional (inherited from leg1)` + leg2_frequency : Frequency, str, :green:`optional (inherited from leg1)` + leg2_stub : StubInference, str, :green:`optional (inherited from leg1)` + leg2_front_stub : datetime, :green:`optional (inherited from leg1)` + leg2_back_stub : datetime, :green:`optional (inherited from leg1)` + leg2_roll : RollDay, int, str, :green:`optional (inherited from leg1)` + leg2_eom : bool, :green:`optional (inherited from leg1)` + leg2_modifier : Adjuster, str, :green:`optional (inherited from leg1)` + leg2_calendar : calendar, str, :green:`optional (inherited from leg1)` + leg2_payment_lag: Adjuster, int, :green:`optional (inherited from leg1)` + leg2_payment_lag_exchange: Adjuster, int, :green:`optional (inherited from leg1)` + leg2_ex_div: Adjuster, int, :green:`optional (inherited from leg1)` + leg2_convention: str, :green:`optional (inherited from leg1)` - # @property - # def fixed_rate(self) -> DualTypes_: - # raise AttributeError(f"Attribute not available on {type(self).__name__}") + .. note:: + + The following define generalised **settlement** parameters. + + currency : str, :green:`optional (set by 'defaults')` + The local settlement currency of the *Instrument* (3-digit code). + notional : float, Dual, Dual2, Variable, :green:`optional (set by 'defaults')` + The initial leg notional, defined in units of *reference currency*. + amortization: float, Dual, Dual2, Variable, str, Amortization, :green:`optional (set as zero)` + Set a non-constant notional per *Period*. If a scalar value, adjusts the ``notional`` of + each successive period by that same value. Should have + sign equal to that of notional if the notional is to reduce towards zero. + leg2_notional : float, Dual, Dual2, Variable, :green:`optional (negatively inherited from leg1)` + leg2_amortization : float, Dual, Dual2, Variable, str, Amortization, :green:`optional (negatively inherited from leg1)` + + .. note:: + + The following are **rate parameters**. + + fixing_method: FloatFixingMethod, str, :green:`optional (set by 'defaults')` + The :class:`~rateslib.enums.parameters.FloatFixingMethod` describing the determination + of the floating rate for each period. + method_param: int, :green:`optional (set by 'defaults')` + A specific parameter that is used by the specific ``fixing_method``. + fixing_frequency: Frequency, str, :green:`optional (set by 'frequency' or '1B')` + The :class:`~rateslib.scheduling.Frequency` as a component of the + :class:`~rateslib.data.fixings.FloatRateIndex`. If not given is assumed to match the + frequency of the schedule for an IBOR type ``fixing_method`` or '1B' if RFR type. + fixing_series: FloatRateSeries, str, :green:`optional (implied by other parameters)` + The :class:`~rateslib.data.fixings.FloatRateSeries` as a component of the + :class:`~rateslib.data.fixings.FloatRateIndex`. If not given inherits attributes given + such as the ``calendar``, ``convention``, ``method_param`` etc. + float_spread: float, Dual, Dual2, Variable, :green:`optional (set as 0.0)` + The amount (in bps) added to the rate in each period rate determination. + spread_compound_method: SpreadCompoundMethod, str, :green:`optional (set by 'defaults')` + The :class:`~rateslib.enums.parameters.SpreadCompoundMethod` used in the calculation + of the period rate when combining a ``float_spread``. Used **only** with RFR type + ``fixing_method``. + rate_fixings: float, Dual, Dual2, Variable, Series, str, :green:`optional` + See XXX (working with fixings). + The value of the rate fixing. If a scalar, is used directly. If a string identifier, links + to the central ``fixings`` object and data loader. + leg2_fixing_method: FloatFixingMethod, str, :green:`optional (set by 'defaults')` + leg2_method_param: int, :green:`optional (set by 'defaults')` + leg2_fixing_frequency: Frequency, str, :green:`optional (set by 'frequency' or '1B')` + leg2_fixing_series: FloatRateSeries, str, :green:`optional (implied by other parameters)` + leg2_float_spread: float, Dual, Dual2, Variable, :green:`optional (set as 0.0)` + leg2_spread_compound_method: SpreadCompoundMethod, str, :green:`optional (set by 'defaults')` + leg2_rate_fixings: float, Dual, Dual2, Variable, Series, str, :green:`optional` + + .. note:: + + The following are **meta parameters**. + + curves : _BaseCurve, str, dict, _Curves, Sequence, :green:`optional` + Pricing objects passed directly to the *Instrument's* methods' ``curves`` argument. See + **Pricing**. + metric : str, :green:`optional` (set by 'defaults') + The pricing metric returned by :meth:`~rateslib.instruments.components.SBS.rate`. + spec: str, :green:`optional` + A collective group of parameters. See + :ref:`default argument specifications `. + + """ # noqa: E501 + + _rate_scalar = 100.0 @property def float_spread(self) -> DualTypes_: + """The float spread parameter of the composited + :class:`~rateslib.legs.components.FloatLeg`.""" return self.leg1.float_spread @float_spread.setter @@ -53,12 +237,10 @@ def float_spread(self, value: DualTypes) -> None: self.kwargs.leg1["float_spread"] = value self.leg1.float_spread = value - # @property - # def leg2_fixed_rate(self) -> NoReturn: - # raise AttributeError(f"Attribute not available on {type(self).__name__}") - @property def leg2_float_spread(self) -> DualTypes_: + """The float spread parameter of the composited + :class:`~rateslib.legs.components.FloatLeg`.""" return self.leg2.float_spread @leg2_float_spread.setter @@ -77,21 +259,17 @@ def leg2(self) -> FloatLeg: return self._leg2 @property - def legs(self) -> list[_BaseLeg]: + def legs(self) -> Sequence[_BaseLeg]: """A list of the *Legs* of the *Instrument*.""" return self._legs def __init__( self, + # scheduling effective: datetime_ = NoInput(0), termination: datetime | str_ = NoInput(0), frequency: Frequency | str_ = NoInput(0), *, - float_spread: DualTypes_ = NoInput(0), - spread_compound_method: str_ = NoInput(0), - rate_fixings: FixingsRates_ = NoInput(0), # type: ignore[type-var] - fixing_method: str_ = NoInput(0), - method_param: int_ = NoInput(0), stub: str_ = NoInput(0), front_stub: datetime_ = NoInput(0), back_stub: datetime_ = NoInput(0), @@ -102,19 +280,7 @@ def __init__( payment_lag: int_ = NoInput(0), payment_lag_exchange: int_ = NoInput(0), ex_div: int_ = NoInput(0), - notional: float_ = NoInput(0), - currency: str_ = NoInput(0), - amortization: float_ = NoInput(0), convention: str_ = NoInput(0), - fixing_frequency: Frequency | str_ = NoInput(0), - fixing_series: FloatRateSeries | str_ = NoInput(0), - leg2_fixing_frequency: Frequency | str_ = NoInput(0), - leg2_fixing_series: FloatRateSeries | str_ = NoInput(0), - leg2_float_spread: DualTypes_ = NoInput(0), - leg2_spread_compound_method: str_ = NoInput(0), - leg2_rate_fixings: FixingsRates_ = NoInput(0), # type: ignore[type-var] - leg2_fixing_method: str_ = NoInput(0), - leg2_method_param: int_ = NoInput(0), leg2_effective: datetime_ = NoInput(1), leg2_termination: datetime | str_ = NoInput(1), leg2_frequency: Frequency | str_ = NoInput(1), @@ -127,13 +293,33 @@ def __init__( leg2_calendar: CalInput = NoInput(1), leg2_payment_lag: int_ = NoInput(1), leg2_payment_lag_exchange: int_ = NoInput(1), + leg2_ex_div: int_ = NoInput(1), + leg2_convention: str_ = NoInput(1), + # settlement parameters + currency: str_ = NoInput(0), + notional: float_ = NoInput(0), + amortization: float_ = NoInput(0), leg2_notional: float_ = NoInput(-1), leg2_amortization: float_ = NoInput(-1), - leg2_convention: str_ = NoInput(1), - leg2_ex_div: int_ = NoInput(1), - curves: Curves_ = NoInput(0), - metric: str_ = NoInput(0), + # rate parameters + float_spread: DualTypes_ = NoInput(0), + spread_compound_method: str_ = NoInput(0), + rate_fixings: FixingsRates_ = NoInput(0), + fixing_method: str_ = NoInput(0), + method_param: int_ = NoInput(0), + fixing_frequency: Frequency | str_ = NoInput(0), + fixing_series: FloatRateSeries | str_ = NoInput(0), + leg2_float_spread: DualTypes_ = NoInput(0), + leg2_spread_compound_method: str_ = NoInput(0), + leg2_rate_fixings: FixingsRates_ = NoInput(0), + leg2_fixing_method: str_ = NoInput(0), + leg2_method_param: int_ = NoInput(0), + leg2_fixing_frequency: Frequency | str_ = NoInput(0), + leg2_fixing_series: FloatRateSeries | str_ = NoInput(0), + # meta parameters + curves: CurvesT_ = NoInput(0), spec: str_ = NoInput(0), + metric: str_ = NoInput(0), ) -> None: user_args = dict( effective=effective, @@ -214,7 +400,7 @@ def __init__( def rate( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -222,16 +408,16 @@ def rate( settlement: datetime_ = NoInput(0), forward: datetime_ = NoInput(0), metric: str_ = NoInput(0), - ) -> DualTypes_: + ) -> DualTypes: metric_: str = _drb(self.kwargs.meta["metric"], metric) _curves = self._parse_curves(curves) - if metric_.lower() == "float_spread": + if metric_.lower() == "leg1": leg2_npv: DualTypes = self.leg2.local_npv( rate_curve=_maybe_get_curve_or_dict_maybe_from_solver( self.kwargs.meta["curves"], _curves, "leg2_rate_curve", solver ), - disc_curve=_maybe_get_curve_or_dict_maybe_from_solver( + disc_curve=_maybe_get_curve_maybe_from_solver( self.kwargs.meta["curves"], _curves, "leg2_disc_curve", solver ), index_curve=NoInput(0), @@ -243,19 +429,19 @@ def rate( rate_curve=_maybe_get_curve_or_dict_maybe_from_solver( self.kwargs.meta["curves"], _curves, "rate_curve", solver ), - disc_curve=_maybe_get_curve_or_dict_maybe_from_solver( + disc_curve=_maybe_get_curve_maybe_from_solver( self.kwargs.meta["curves"], _curves, "disc_curve", solver ), index_curve=NoInput(0), settlement=settlement, forward=forward, ) - else: # metric == "leg2_float_spread" + else: # metric == "leg2" leg1_npv: DualTypes = self.leg1.local_npv( rate_curve=_maybe_get_curve_or_dict_maybe_from_solver( self.kwargs.meta["curves"], _curves, "rate_curve", solver ), - disc_curve=_maybe_get_curve_or_dict_maybe_from_solver( + disc_curve=_maybe_get_curve_maybe_from_solver( self.kwargs.meta["curves"], _curves, "disc_curve", solver ), index_curve=NoInput(0), @@ -267,7 +453,7 @@ def rate( rate_curve=_maybe_get_curve_or_dict_maybe_from_solver( self.kwargs.meta["curves"], _curves, "leg2_rate_curve", solver ), - disc_curve=_maybe_get_curve_or_dict_maybe_from_solver( + disc_curve=_maybe_get_curve_maybe_from_solver( self.kwargs.meta["curves"], _curves, "leg2_disc_curve", solver ), index_curve=NoInput(0), @@ -278,7 +464,7 @@ def rate( def spread( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -301,7 +487,7 @@ def spread( def npv( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -329,13 +515,13 @@ def npv( def _set_pricing_mid( self, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), settlement: datetime_ = NoInput(0), forward: datetime_ = NoInput(0), ) -> None: # the test for an unpriced IRS is that its fixed rate is not set. - if self.kwargs.meta["metric"].lower() == "float_spread": + if self.kwargs.meta["metric"].lower() == "leg1": if isinstance(self.kwargs.leg1["float_spread"], NoInput): # set a fixed rate for the purpose of generic methods NPV will be zero. mid_market_rate = self.rate( @@ -345,7 +531,7 @@ def _set_pricing_mid( forward=forward, ) self.leg1.float_spread = _dual_float(mid_market_rate) - else: # metric == "leg2_float_spread" + else: # metric == "leg2" if isinstance(self.kwargs.leg2["float_spread"], NoInput): # set a fixed rate for the purpose of generic methods NPV will be zero. mid_market_rate = self.rate( @@ -356,7 +542,7 @@ def _set_pricing_mid( ) self.leg2.float_spread = _dual_float(mid_market_rate) - def _parse_curves(self, curves: CurveOption_) -> _Curves: + def _parse_curves(self, curves: CurvesT_) -> _Curves: """ An SBS has three curve requirements: @@ -400,13 +586,15 @@ def _parse_curves(self, curves: CurveOption_) -> _Curves: leg2_rate_curve=curves[2], leg2_disc_curve=curves[3], ) + elif isinstance(curves, _Curves): + return curves else: # `curves` is just a single input raise TypeError("Number of `curves` for an SBS must be 3 or 4. Got 1.") def cashflows( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -427,7 +615,7 @@ def cashflows( def local_analytic_rate_fixings( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), diff --git a/python/rateslib/instruments/components/xcs.py b/python/rateslib/instruments/components/xcs.py index 89af8d63..da70bc9c 100644 --- a/python/rateslib/instruments/components/xcs.py +++ b/python/rateslib/instruments/components/xcs.py @@ -9,7 +9,8 @@ from rateslib.instruments.components.protocols.kwargs import _convert_to_schedule_kwargs, _KWArgs from rateslib.instruments.components.protocols.pricing import ( _Curves, - _get_fx_maybe_from_solver, + _get_fx_forwards_maybe_from_solver, + _maybe_get_curve_maybe_from_solver, _maybe_get_curve_or_dict_maybe_from_solver, ) from rateslib.legs.components import FixedLeg, FloatLeg @@ -17,8 +18,7 @@ if TYPE_CHECKING: from rateslib.typing import ( # pragma: no cover CalInput, - CurveOption_, - Curves_, + CurvesT_, DataFrame, DualTypes, DualTypes_, @@ -29,6 +29,7 @@ FXVolOption_, LegFixings, RollDay, + Sequence, Solver_, _BaseLeg, bool_, @@ -76,16 +77,25 @@ class XCS(_BaseInstrument): .. rubric:: Pricing - The methods of a *XCS* require an :class:`~rateslib.fx.FXForwards` object for ``fx``, and - **four** :class:`~rateslib.curves._BaseCurve` for ``curves``; + The methods of a *XCS* require an :class:`~rateslib.fx.FXForwards` object for ``fx`` . - - a **rate_curve**, - - a **disc_curve**, - - a **leg2_rate_curve**, - - a **leg2_disc_curve** (for pricing consistency the collateral of each discount curve should - be the same). + They also require a *disc curve* and a *leg2 disc curve* which are appropriate curves for the + relevant currency, typically under the same collateral. For *FloatLegs*, an additional + *rate curve* and *leg2 rate curve* are required. The following input + formats are allowed: - If given as a list these should be specified in this order. + .. code-block:: python + + curves = [rate_curve, disc_curve, leg2_rate_curve, leg2_disc_curve] # four curves + curves = { # dict form is explicit + "rate_curve": rate_curve, + "disc_curve": disc_curve, + "leg2_rate_curve": leg2_rate_curve, + "leg2_disc_curve": leg2_disc_curve, + } + + The available pricing ``metric`` are in *{'leg1', 'leg2'}* which will return a *float spread* + or a *fixed rate* on the specified leg, for the appropriate *Leg* type. .. role:: red @@ -246,8 +256,9 @@ class XCS(_BaseInstrument): The following are **meta parameters**. - curves : XXX - Pricing objects passed directly to the *Instrument's* methods' ``curves`` argument. + curves : _BaseCurve, str, dict, _Curves, Sequence, :green:`optional` + Pricing objects passed directly to the *Instrument's* methods' ``curves`` argument. See + **Pricing**. spec: str, :green:`optional` A collective group of parameters. See :ref:`default argument specifications `. @@ -312,14 +323,14 @@ class XCS(_BaseInstrument): def fixed_rate(self) -> DualTypes_: """The fixed rate parameter of the composited :class:`~rateslib.legs.components.FixedLeg`.""" - if self.kwargs.meta["fixed"]: + if isinstance(self.leg1, FixedLeg): return self.leg1.fixed_rate else: raise AttributeError(f"Leg1 is of type: {type(self.leg1).__name__}") @fixed_rate.setter def fixed_rate(self, value: DualTypes_) -> None: - if self.kwargs.meta["fixed"]: + if isinstance(self.leg1, FixedLeg): self.kwargs.leg1["fixed_rate"] = value self.leg1.fixed_rate = value else: @@ -329,14 +340,14 @@ def fixed_rate(self, value: DualTypes_) -> None: def float_spread(self) -> DualTypes: """The float spread parameter of the composited :class:`~rateslib.legs.components.FloatLeg`.""" - if not self.kwargs.meta["fixed"]: + if isinstance(self.leg1, FloatLeg): return self.leg1.float_spread else: raise AttributeError(f"Leg1 is of type: {type(self.leg1).__name__}") @float_spread.setter - def float_spread(self, value: DualTypes_) -> None: - if not self.kwargs.meta["fixed"]: + def float_spread(self, value: DualTypes) -> None: + if isinstance(self.leg1, FloatLeg): self.kwargs.leg1["float_spread"] = value self.leg1.float_spread = value else: @@ -346,14 +357,14 @@ def float_spread(self, value: DualTypes_) -> None: def leg2_fixed_rate(self) -> DualTypes_: """The float spread parameter of the composited :class:`~rateslib.legs.components.FloatLeg`.""" - if self.kwargs.meta["leg2_fixed"]: + if isinstance(self.leg2, FixedLeg): return self.leg2.fixed_rate else: raise AttributeError(f"Leg2 is of type: {type(self.leg2).__name__}") @leg2_fixed_rate.setter def leg2_fixed_rate(self, value: DualTypes_) -> None: - if self.kwargs.meta["leg2_fixed"]: + if isinstance(self.leg2, FixedLeg): self.kwargs.leg2["fixed_rate"] = value self.leg2.fixed_rate = value else: @@ -363,14 +374,14 @@ def leg2_fixed_rate(self, value: DualTypes_) -> None: def leg2_float_spread(self) -> DualTypes_: """The float spread parameter of the composited :class:`~rateslib.legs.components.FloatLeg`.""" - if not self.kwargs.meta["leg2_fixed"]: + if isinstance(self.leg2, FloatLeg): return self.leg2.float_spread else: raise AttributeError(f"Leg2 is of type: {type(self.leg2).__name__}") @leg2_float_spread.setter def leg2_float_spread(self, value: DualTypes) -> None: - if not self.kwargs.meta["leg2_fixed"]: + if isinstance(self.leg2, FloatLeg): self.kwargs.leg2["float_spread"] = value self.leg2.float_spread = value else: @@ -389,7 +400,7 @@ def leg2(self) -> FixedLeg | FloatLeg: return self._leg2 @property - def legs(self) -> list[_BaseLeg]: + def legs(self) -> Sequence[_BaseLeg]: """A list of the *Legs* of the *Instrument*.""" return self._legs @@ -449,14 +460,14 @@ def __init__( leg2_fixed_rate: DualTypes_ = NoInput(0), leg2_float_spread: DualTypes_ = NoInput(0), leg2_spread_compound_method: str_ = NoInput(0), - leg2_rate_fixings: FixingsRates_ = NoInput(0), # type: ignore[type-var] + leg2_rate_fixings: FixingsRates_ = NoInput(0), leg2_fixing_method: str_ = NoInput(0), leg2_method_param: int_ = NoInput(0), leg2_fixing_frequency: Frequency | str_ = NoInput(0), leg2_fixing_series: FloatRateSeries | str_ = NoInput(0), leg2_fx_fixings: LegFixings = NoInput(0), # meta parameters - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), spec: str_ = NoInput(0), metric: str = "leg1", ) -> None: @@ -612,11 +623,15 @@ def __init__( ) if fixed: - self._leg1 = FixedLeg(**_convert_to_schedule_kwargs(self.kwargs.leg1, 1)) + self._leg1: FixedLeg | FloatLeg = FixedLeg( + **_convert_to_schedule_kwargs(self.kwargs.leg1, 1) + ) else: self._leg1 = FloatLeg(**_convert_to_schedule_kwargs(self.kwargs.leg1, 1)) if leg2_fixed: - self._leg2 = FixedLeg(**_convert_to_schedule_kwargs(self.kwargs.leg2, 1)) + self._leg2: FixedLeg | FloatLeg = FixedLeg( + **_convert_to_schedule_kwargs(self.kwargs.leg2, 1) + ) else: self._leg2 = FloatLeg(**_convert_to_schedule_kwargs(self.kwargs.leg2, 1)) self._legs = [self.leg1, self.leg2] @@ -624,7 +639,7 @@ def __init__( def rate( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -632,26 +647,26 @@ def rate( settlement: datetime_ = NoInput(0), forward: datetime_ = NoInput(0), metric: str_ = NoInput(0), - ) -> DualTypes_: + ) -> DualTypes: _curves = self._parse_curves(curves) metric_ = _drb(self.kwargs.meta["metric"], metric) - fx_ = _get_fx_maybe_from_solver(fx=fx, solver=solver) + fx_ = _get_fx_forwards_maybe_from_solver(fx=fx, solver=solver) leg2_rate_curve = _maybe_get_curve_or_dict_maybe_from_solver( self.kwargs.meta["curves"], _curves, "leg2_rate_curve", solver ) - leg2_disc_curve = _maybe_get_curve_or_dict_maybe_from_solver( + leg2_disc_curve = _maybe_get_curve_maybe_from_solver( self.kwargs.meta["curves"], _curves, "leg2_disc_curve", solver ) rate_curve = _maybe_get_curve_or_dict_maybe_from_solver( self.kwargs.meta["curves"], _curves, "rate_curve", solver ) - disc_curve = _maybe_get_curve_or_dict_maybe_from_solver( + disc_curve = _maybe_get_curve_maybe_from_solver( self.kwargs.meta["curves"], _curves, "disc_curve", solver ) if metric_ == "leg1": - leg2_npv: DualTypes = self.leg2.npv( + leg2_npv: DualTypes = self.leg2.npv( # type: ignore[assignment] rate_curve=leg2_rate_curve, disc_curve=leg2_disc_curve, base=self.leg1.settlement_params.currency, @@ -672,7 +687,7 @@ def rate( else: return spread elif metric_ == "leg2": - leg1_npv: DualTypes = self.leg1.npv( + leg1_npv: DualTypes = self.leg1.npv( # type: ignore[assignment] rate_curve=rate_curve, disc_curve=disc_curve, base=self.leg2.settlement_params.currency, @@ -715,7 +730,7 @@ def rate( def spread( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -738,7 +753,7 @@ def spread( def npv( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -767,7 +782,7 @@ def npv( def _set_pricing_mid( self, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), settlement: datetime_ = NoInput(0), @@ -776,8 +791,8 @@ def _set_pricing_mid( # all float_spread are assumed to be equal to zero if not given. # missing fixed rates will be priced and set if possible. - if self.kwargs.meta["fixed"] and isinstance(self.kwargs.leg1["fixed_rate"], NoInput): - if self.kwargs.meta["leg2_fixed"] and isinstance( + if isinstance(self.leg1, FixedLeg) and isinstance(self.kwargs.leg1["fixed_rate"], NoInput): + if isinstance(self.leg2, FixedLeg) and isinstance( self.kwargs.leg2["fixed_rate"], NoInput ): raise ValueError("At least one leg must have a defined `fixed_rate`.") @@ -792,8 +807,10 @@ def _set_pricing_mid( ) self.leg1.fixed_rate = _dual_float(mid_price) - elif self.kwargs.meta["leg2_fixed"] and isinstance(self.kwargs.leg2["fixed_rate"], NoInput): - # leg1 cannot be fixed with NoInput + elif isinstance(self.leg2, FixedLeg) and isinstance( + self.kwargs.leg2["fixed_rate"], NoInput + ): + # leg1 cannot be fixed with NoInput - this branch is covered above mid_price = self.rate( curves=curves, solver=solver, @@ -804,17 +821,13 @@ def _set_pricing_mid( ) self.leg2.fixed_rate = _dual_float(mid_price) - def _parse_curves(self, curves: CurveOption_) -> _Curves: + def _parse_curves(self, curves: CurvesT_) -> _Curves: """ - An IRS has two curve requirements: a leg2_rate_curve and a disc_curve used by both legs. - - When given as only 1 element this curve is applied to all of the those components - - When given as 2 elements the first is treated as the rate curve and the 2nd as disc curve. + A XCS requires 4 curves (mostly if float-float, otherwise it needs 2) """ if isinstance(curves, NoInput): return _Curves() - if isinstance(curves, dict): + elif isinstance(curves, dict): return _Curves( rate_curve=curves.get("rate_curve", NoInput(0)), disc_curve=curves.get("disc_curve", NoInput(0)), @@ -831,15 +844,17 @@ def _parse_curves(self, curves: CurveOption_) -> _Curves: ) else: raise ValueError( - f"{type(self).__name__} requires 4 curve types. Got {len(curves)}." + f"{type(self).__name__} requires a 4 curve type input. Got {len(curves)}." ) - else: # `curves` is just a single input which is copied across all curves - raise ValueError(f"{type(self).__name__} requires 4 curve types. Got 1.") + elif isinstance(curves, _Curves): + return curves + else: + raise ValueError(f"{type(self).__name__} requires a 4 curve type input. Got 1.") def cashflows( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -860,7 +875,7 @@ def cashflows( def local_analytic_rate_fixings( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), diff --git a/python/rateslib/instruments/components/zcis.py b/python/rateslib/instruments/components/zcis.py index 755ed0f9..14457aec 100644 --- a/python/rateslib/instruments/components/zcis.py +++ b/python/rateslib/instruments/components/zcis.py @@ -9,15 +9,14 @@ from rateslib.instruments.components.protocols.kwargs import _convert_to_schedule_kwargs, _KWArgs from rateslib.instruments.components.protocols.pricing import ( _Curves, - _maybe_get_curve_or_dict_maybe_from_solver, + _maybe_get_curve_maybe_from_solver, ) from rateslib.legs.components import ZeroFixedLeg, ZeroIndexLeg if TYPE_CHECKING: from rateslib.typing import ( # pragma: no cover CalInput, - CurveOption_, - Curves_, + CurvesT_, DataFrame, DualTypes, DualTypes_, @@ -40,7 +39,7 @@ class ZCIS(_BaseInstrument): """ - Create a *zero coupon swap (ZCS)* composing a :class:`~rateslib.legs.components.ZeroFixedLeg` + An *indexed zero coupon swap (ZCIS)* composing a :class:`~rateslib.legs.components.ZeroFixedLeg` and a :class:`~rateslib.legs.components.ZeroIndexLeg`. .. role:: red @@ -181,27 +180,6 @@ def fixed_rate(self, value: DualTypes_) -> None: self.kwargs.leg1["fixed_rate"] = value self.leg1.fixed_rate = value - # @property - # def float_spread(self) -> NoReturn: - # """The float spread parameter of the composited - # :class:`~rateslib.legs.components.FloatLeg`.""" - # raise AttributeError(f"Attribute not available on {type(self).__name__}") - - # @property - # def leg2_fixed_rate(self) -> NoReturn: - # raise AttributeError(f"Attribute not available on {type(self).__name__}") - - # @property - # def leg2_float_spread(self) -> DualTypes_: - # """The float spread parameter of the composited - # :class:`~rateslib.legs.components.FloatLeg`.""" - # return self.leg2.float_spread - # - # @leg2_float_spread.setter - # def leg2_float_spread(self, value: DualTypes) -> None: - # self.kwargs.leg2["float_spread"] = value - # self.leg2.float_spread = value - @property def leg1(self) -> ZeroFixedLeg: """The :class:`~rateslib.legs.components.ZeroFixedLeg` of the *Instrument*.""" @@ -252,9 +230,7 @@ def __init__( # settlement parameters currency: str_ = NoInput(0), notional: float_ = NoInput(0), - amortization: float_ = NoInput(0), leg2_notional: float_ = NoInput(-1), - leg2_amortization: float_ = NoInput(-1), # rate parameters fixed_rate: DualTypes_ = NoInput(0), # indexing @@ -263,7 +239,7 @@ def __init__( leg2_index_method: IndexMethod | str_ = NoInput(0), leg2_index_fixings: Series[DualTypes] | str_ = NoInput(0), # meta parameters - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), spec: str_ = NoInput(0), ) -> None: user_args = dict( @@ -339,7 +315,7 @@ def __init__( def rate( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -347,15 +323,15 @@ def rate( settlement: datetime_ = NoInput(0), forward: datetime_ = NoInput(0), metric: str_ = NoInput(0), - ) -> DualTypes_: + ) -> DualTypes: _curves = self._parse_curves(curves) leg2_npv: DualTypes = self.leg2.local_npv( rate_curve=NoInput(0), - disc_curve=_maybe_get_curve_or_dict_maybe_from_solver( + disc_curve=_maybe_get_curve_maybe_from_solver( self.kwargs.meta["curves"], _curves, "leg2_disc_curve", solver ), - index_curve=_maybe_get_curve_or_dict_maybe_from_solver( + index_curve=_maybe_get_curve_maybe_from_solver( self.kwargs.meta["curves"], _curves, "leg2_index_curve", solver ), settlement=settlement, @@ -365,7 +341,7 @@ def rate( self.leg1.spread( target_npv=-leg2_npv, rate_curve=NoInput(0), - disc_curve=_maybe_get_curve_or_dict_maybe_from_solver( + disc_curve=_maybe_get_curve_maybe_from_solver( self.kwargs.meta["curves"], _curves, "disc_curve", solver ), index_curve=NoInput(0), @@ -378,7 +354,7 @@ def rate( def spread( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -391,7 +367,7 @@ def spread( def npv( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -419,7 +395,7 @@ def npv( def _set_pricing_mid( self, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), settlement: datetime_ = NoInput(0), forward: datetime_ = NoInput(0), @@ -435,7 +411,7 @@ def _set_pricing_mid( ) self.leg1.fixed_rate = _dual_float(mid_market_rate) - def _parse_curves(self, curves: CurveOption_) -> _Curves: + def _parse_curves(self, curves: CurvesT_) -> _Curves: """ An ZCIS has two curve requirements: a leg2_index_curve and a disc_curve used by both legs. @@ -463,12 +439,6 @@ def _parse_curves(self, curves: CurveOption_) -> _Curves: disc_curve=curves[1], leg2_disc_curve=curves[1], ) - elif len(curves) == 1: - return _Curves( - leg2_index_curve=curves[0], - disc_curve=curves[0], - leg2_disc_curve=curves[0], - ) elif len(curves) == 4: return _Curves( leg2_index_curve=curves[2], @@ -479,17 +449,15 @@ def _parse_curves(self, curves: CurveOption_) -> _Curves: raise ValueError( f"{type(self).__name__} requires only 2 curve types. Got {len(curves)}." ) - else: # `curves` is just a single input which is copied across all curves - return _Curves( - leg2_rate_curve=curves, - disc_curve=curves, - leg2_disc_curve=curves, - ) + elif isinstance(curves, _Curves): + return curves + else: + raise ValueError(f"{type(self).__name__} requires only 2 curve types. Got 1.") def cashflows( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -510,7 +478,7 @@ def cashflows( def local_analytic_rate_fixings( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), diff --git a/python/rateslib/instruments/components/zcs.py b/python/rateslib/instruments/components/zcs.py index 03f2a6af..22eafd37 100644 --- a/python/rateslib/instruments/components/zcs.py +++ b/python/rateslib/instruments/components/zcs.py @@ -9,6 +9,7 @@ from rateslib.instruments.components.protocols.kwargs import _convert_to_schedule_kwargs, _KWArgs from rateslib.instruments.components.protocols.pricing import ( _Curves, + _maybe_get_curve_maybe_from_solver, _maybe_get_curve_or_dict_maybe_from_solver, ) from rateslib.legs.components import ZeroFixedLeg, ZeroFloatLeg @@ -16,8 +17,7 @@ if TYPE_CHECKING: from rateslib.typing import ( # pragma: no cover CalInput, - CurveOption_, - Curves_, + CurvesT_, DataFrame, DualTypes, DualTypes_, @@ -40,9 +40,40 @@ class ZCS(_BaseInstrument): """ - Create a *zero coupon swap (ZCS)* composing a :class:`~rateslib.legs.components.ZeroFixedLeg` + A *zero coupon swap (ZCS)* composing a :class:`~rateslib.legs.components.ZeroFixedLeg` and a :class:`~rateslib.legs.components.ZeroFloatLeg`. + .. rubric:: Examples + + .. ipython:: python + :suppress: + + from rateslib.instruments.components import ZCS + from datetime import datetime as dt + + .. ipython:: python + + zcs = ZCS( + effective=dt(2000, 1, 1), + termination="2y", + frequency="S", + fixed_rate=2.0, + ) + zcs.cashflows() + + .. rubric:: Pricing + + A *ZCS* requires a *disc curve* on both legs (which should be the same *Curve*) and a + *leg2 rate curve* to forecast rates on the *ZeroFloatLeg*. The following input formats are + allowed: + + .. code-block:: python + + curves = curve | [curve] # a single curve is repeated for all required curves + curves = [rate_curve, disc_curve] # two curves are applied in the given order + curves = [None, disc_curve, rate_curve, disc_curve] # four curves applied to each leg + curves = {"leg2_rate_curve": rate_curve, "disc_curve": disc_curve} # dict form is explicit + .. role:: red .. role:: green @@ -137,7 +168,7 @@ class ZCS(_BaseInstrument): The following are **rate parameters**. fixed_rate : float or None - The fixed rate applied to the :class:`~rateslib.legs.FixedLeg`. If `None` + The fixed rate applied to the :class:`~rateslib.legs.ZeroFixedLeg`. If `None` will be set to mid-market when curves are provided. leg2_fixing_method: FloatFixingMethod, str, :green:`optional (set by 'defaults')` The :class:`~rateslib.enums.parameters.FloatFixingMethod` describing the determination @@ -167,8 +198,9 @@ class ZCS(_BaseInstrument): The following are **meta parameters**. - curves : XXX - Pricing objects passed directly to the *Instrument's* methods' ``curves`` argument. + curves : _BaseCurve, str, dict, _Curves, Sequence, :green:`optional` + Pricing objects passed directly to the *Instrument's* methods' ``curves`` argument. See + **Pricing**. spec: str, :green:`optional` A collective group of parameters. See :ref:`default argument specifications `. @@ -185,10 +217,6 @@ class ZCS(_BaseInstrument): ``leg2_fixings``, but a cookbook article is also produced for :ref:`working with fixings `. - Examples - -------- - Construct a curve to price the example. - """ # noqa: E501 _rate_scalar = 1.0 @@ -275,9 +303,9 @@ def __init__( # settlement parameters currency: str_ = NoInput(0), notional: float_ = NoInput(0), - amortization: float_ = NoInput(0), + # amortization: float_ = NoInput(0), leg2_notional: float_ = NoInput(-1), - leg2_amortization: float_ = NoInput(-1), + # leg2_amortization: float_ = NoInput(-1), # rate parameters fixed_rate: DualTypes_ = NoInput(0), leg2_float_spread: DualTypes_ = NoInput(0), @@ -288,7 +316,7 @@ def __init__( leg2_fixing_frequency: Frequency | str_ = NoInput(0), leg2_fixing_series: FloatRateSeries | str_ = NoInput(0), # meta parameters - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), spec: str_ = NoInput(0), ) -> None: user_args = dict( @@ -343,6 +371,8 @@ def __init__( final_exchange=False, leg2_initial_exchange=False, leg2_final_exchange=False, + # amortization=NoInput(0), + # leg2_amortization=NoInput(0), ) default_args = dict( @@ -364,7 +394,7 @@ def __init__( def rate( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -372,14 +402,14 @@ def rate( settlement: datetime_ = NoInput(0), forward: datetime_ = NoInput(0), metric: str_ = NoInput(0), - ) -> DualTypes_: + ) -> DualTypes: _curves = self._parse_curves(curves) leg2_npv: DualTypes = self.leg2.local_npv( rate_curve=_maybe_get_curve_or_dict_maybe_from_solver( self.kwargs.meta["curves"], _curves, "leg2_rate_curve", solver ), - disc_curve=_maybe_get_curve_or_dict_maybe_from_solver( + disc_curve=_maybe_get_curve_maybe_from_solver( self.kwargs.meta["curves"], _curves, "leg2_disc_curve", solver ), index_curve=NoInput(0), @@ -390,7 +420,7 @@ def rate( self.leg1.spread( target_npv=-leg2_npv, rate_curve=NoInput(0), - disc_curve=_maybe_get_curve_or_dict_maybe_from_solver( + disc_curve=_maybe_get_curve_maybe_from_solver( self.kwargs.meta["curves"], _curves, "disc_curve", solver ), index_curve=NoInput(0), @@ -403,7 +433,7 @@ def rate( def spread( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -415,7 +445,7 @@ def spread( leg2_rate_curve = _maybe_get_curve_or_dict_maybe_from_solver( self.kwargs.meta["curves"], _curves, "leg2_rate_curve", solver ) - disc_curve = _maybe_get_curve_or_dict_maybe_from_solver( + disc_curve = _maybe_get_curve_maybe_from_solver( self.kwargs.meta["curves"], _curves, "disc_curve", solver ) leg1_npv: DualTypes = self.leg1.local_npv( @@ -437,7 +467,7 @@ def spread( def npv( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -465,7 +495,7 @@ def npv( def _set_pricing_mid( self, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), settlement: datetime_ = NoInput(0), forward: datetime_ = NoInput(0), @@ -481,7 +511,7 @@ def _set_pricing_mid( ) self.leg1.fixed_rate = _dual_float(mid_market_rate) - def _parse_curves(self, curves: CurveOption_) -> _Curves: + def _parse_curves(self, curves: CurvesT_) -> _Curves: """ An ZCS has two curve requirements: a leg2_rate_curve and a disc_curve used by both legs. @@ -491,7 +521,7 @@ def _parse_curves(self, curves: CurveOption_) -> _Curves: """ if isinstance(curves, NoInput): return _Curves() - if isinstance(curves, dict): + elif isinstance(curves, dict): return _Curves( rate_curve=curves.get("rate_curve", NoInput(0)), disc_curve=curves.get("disc_curve", NoInput(0)), @@ -521,17 +551,19 @@ def _parse_curves(self, curves: CurveOption_) -> _Curves: raise ValueError( f"{type(self).__name__} requires only 2 curve types. Got {len(curves)}." ) + elif isinstance(curves, _Curves): + return curves else: # `curves` is just a single input which is copied across all curves return _Curves( - leg2_rate_curve=curves, - disc_curve=curves, - leg2_disc_curve=curves, + leg2_rate_curve=curves, # type: ignore[arg-type] + disc_curve=curves, # type: ignore[arg-type] + leg2_disc_curve=curves, # type: ignore[arg-type] ) def cashflows( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), @@ -552,7 +584,7 @@ def cashflows( def local_analytic_rate_fixings( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), fx_vol: FXVolOption_ = NoInput(0), diff --git a/python/rateslib/legs/components/amortization.py b/python/rateslib/legs/components/amortization.py index fb277493..592cd0f6 100644 --- a/python/rateslib/legs/components/amortization.py +++ b/python/rateslib/legs/components/amortization.py @@ -23,6 +23,19 @@ class Amortization: """ An amortization schedule for any :class:`~rateslib.legs.base.BaseLeg`. + .. rubric:: Examples + + .. ipython:: python + :suppress: + + from rateslib.legs.components import Amortization + + .. ipython:: python + + obj = Amortization(n=5, initial=1e6, amortization="to_zero") + obj.outstanding + obj.amortization + Parameters ---------- n: int @@ -41,14 +54,22 @@ class Amortization: - a list or tuple of *n-1* scalars, then this is defines a custome amortization schedule. - a string flag then an amortization schedule will be calculated directly: - - *"to_zero": each period will be a constant value ending with zero implied ending balance. - - *"{float}%": each period will amortize by a constant percentage of the outstanding balance. + - *"to_zero"*: each period will be a constant value ending with zero implied ending balance. + - *"{float}%"*: each period will amortize by a constant percentage of the outstanding balance. """ _type: _AmortizationType - amortization: tuple[DualTypes, ...] - outstanding: tuple[DualTypes, ...] + + @property + def amortization(self) -> tuple[DualTypes, ...]: + """A tuple of (n-1) amortization amounts for each *Period*.""" + return self._amortization + + @property + def outstanding(self) -> tuple[DualTypes, ...]: + """A tuple of n outstanding notional amounts for each *Period*.""" + return self._outstanding def __init__( self, @@ -58,8 +79,8 @@ def __init__( ) -> None: if isinstance(amortization, NoInput): self._type = _AmortizationType.NoAmortization - self.amortization = (0.0,) * (n - 1) - self.outstanding = (initial,) * n + self._amortization: tuple[DualTypes, ...] = (0.0,) * (n - 1) + self._outstanding: tuple[DualTypes, ...] = (initial,) * n elif isinstance(amortization, list | tuple): self._type = _AmortizationType.CustomSchedule if len(amortization) != (n - 1): @@ -67,16 +88,16 @@ def __init__( "Custom amortisation schedules must have `n-1` amortization amounts for `n` " f"periods.\nGot '{len(amortization)}' amounts for '{n}' periods." ) - self.amortization = tuple(amortization) + self._amortization = tuple(amortization) outstanding = [initial] for value in amortization: outstanding.append(outstanding[-1] - value) - self.outstanding = tuple(outstanding) + self._outstanding = tuple(outstanding) elif isinstance(amortization, str): if amortization.lower() == "to_zero": self._type = _AmortizationType.ConstantPeriod - self.amortization = (initial / n,) * (n - 1) - self.outstanding = (initial,) + tuple([initial * (1 - i / n) for i in range(1, n)]) + self._amortization = (initial / n,) * (n - 1) + self._outstanding = (initial,) + tuple([initial * (1 - i / n) for i in range(1, n)]) elif amortization[-1] == "%": self._type = _AmortizationType.CustomSchedule amortization_ = [initial * float(amortization[:-1]) / 100] @@ -85,14 +106,16 @@ def __init__( outstanding_.append(outstanding_[-1] - amortization_[-1]) if i != n - 1: amortization_.append(outstanding_[-1] * float(amortization[:-1]) / 100) - self.outstanding = tuple(outstanding_) - self.amortization = tuple(amortization_) + self._outstanding = tuple(outstanding_) + self._amortization = tuple(amortization_) else: raise ValueError("`amortization` as string must be one of 'to_zero', '{float}%'.") else: # isinstance(amortization, DualTypes) self._type = _AmortizationType.ConstantPeriod - self.amortization = (amortization,) * (n - 1) - self.outstanding = (initial,) + tuple([initial - amortization * i for i in range(1, n)]) + self._amortization = (amortization,) * (n - 1) + self._outstanding = (initial,) + tuple( + [initial - amortization * i for i in range(1, n)] + ) def __mul__(self, other: DualTypes) -> Amortization: return Amortization( diff --git a/python/rateslib/legs/components/credit.py b/python/rateslib/legs/components/credit.py index f47a1700..4a8eb920 100644 --- a/python/rateslib/legs/components/credit.py +++ b/python/rateslib/legs/components/credit.py @@ -80,7 +80,7 @@ def settlement_params(self) -> _SettlementParams: return self._regular_periods[0].settlement_params @property - def periods(self) -> list[CreditPremiumPeriod]: # type: ignore[override] + def periods(self) -> list[CreditPremiumPeriod]: """Combine all period collection types into an ordered list.""" return list(self._regular_periods) @@ -253,7 +253,7 @@ def settlement_params(self) -> _SettlementParams: return self._regular_periods[0].settlement_params @property - def periods(self) -> list[CreditProtectionPeriod]: # type: ignore[override] + def periods(self) -> list[CreditProtectionPeriod]: """Combine all period collection types into an ordered list.""" return list(self._regular_periods) diff --git a/python/rateslib/legs/components/custom.py b/python/rateslib/legs/components/custom.py index c7432aee..2a64f77b 100644 --- a/python/rateslib/legs/components/custom.py +++ b/python/rateslib/legs/components/custom.py @@ -3,15 +3,19 @@ from typing import TYPE_CHECKING from rateslib.legs.components.protocols import _BaseLeg -from rateslib.periods.components import _BasePeriod +from rateslib.periods.components.protocols import _BasePeriod if TYPE_CHECKING: - pass + from rateslib.typing import ( # pragma: no cover + Sequence, + Any, + DualTypes, + ) class CustomLeg(_BaseLeg): """ - Create a leg containing user specified :class:`~rateslib.periods.components._BasePeriod`. + A *Leg* containing user specified :class:`~rateslib.periods.components._BasePeriod`. .. rubric:: Examples @@ -52,13 +56,16 @@ class CustomLeg(_BaseLeg): """ # noqa: E501 @property - def periods(self) -> list[_BasePeriod]: + def periods(self) -> Sequence[_BasePeriod]: """Combine all period collection types into an ordered list.""" return self._periods - def __init__(self, periods: list[_BasePeriod]) -> None: + def __init__(self, periods: Sequence[_BasePeriod]) -> None: if not all(isinstance(p, _BasePeriod) for p in periods): raise ValueError( "Each object in `periods` must be an instance of `_BasePeriod`.", ) self._periods = periods + + def spread(self, *args: Any, **kwargs: Any) -> DualTypes: + return super().spread(*args, **kwargs) # type: ignore[safe-super] diff --git a/python/rateslib/legs/components/fixed.py b/python/rateslib/legs/components/fixed.py index 3668a592..1f6baac3 100644 --- a/python/rateslib/legs/components/fixed.py +++ b/python/rateslib/legs/components/fixed.py @@ -15,16 +15,19 @@ _BaseLeg, _WithExDiv, ) +from rateslib.periods.components.protocols import ( + _BasePeriod, +) from rateslib.periods.components import ( Cashflow, FixedPeriod, MtmCashflow, ZeroFixedPeriod, - _BasePeriod, ) if TYPE_CHECKING: from rateslib.typing import ( # pragma: no cover + Any, CurveOption_, DualTypes, DualTypes_, @@ -45,7 +48,31 @@ class FixedLeg(_BaseLeg, _WithExDiv): """ - Define a *Leg* containing :class:`~rateslib.periods.components.FixedPeriod`. + A *Leg* containing :class:`~rateslib.periods.components.FixedPeriod`. + + .. rubric:: Examples + + .. ipython:: python + :suppress: + + from rateslib import fixings, Schedule + from pandas import Series + from rateslib.legs.components import FixedLeg + from datetime import datetime as dt + + .. ipython:: python + + fl = FixedLeg( + schedule=Schedule( + effective=dt(2000, 2, 1), + termination=dt(2002, 2, 1), + frequency="S", + ), + convention="ActActICMA", + fixed_rate=2.5, + notional=10e6, + ) + fl.cashflows() .. role:: red @@ -58,6 +85,14 @@ class FixedLeg(_BaseLeg, _WithExDiv): The schedule object also contains data for payment dates, payment dates for notional exchanges and ex-dividend dates for each period. + .. note:: + + The following are **period parameters** combined with the ``schedule``. + + convention: str, :green:`optional (set by 'defaults')` + The day count convention applied to calculations of period accrual dates. + See :meth:`~rateslib.scheduling.dcf`. + .. note:: The following define generalised **settlement** parameters. @@ -77,6 +112,13 @@ class FixedLeg(_BaseLeg, _WithExDiv): Whether to also include a final notional exchange and interim amortization notional exchanges. + .. note:: + + The following define **rate parameters**. + + fixed_rate: float, Dual, Dual2, Variable, :green:`optional` + The fixed rate of each composited :class:`~rateslib.periods.components.FixedPeriod`. + .. note:: The following define **non-deliverable** parameters. If the *Leg* is directly @@ -93,21 +135,6 @@ class FixedLeg(_BaseLeg, _WithExDiv): :class:`~rateslib.data.fixings.FXFixing` defined at the start of the *Leg*, or multiple throughout its settlement. Review the **notes** section non-deliverability. - .. note:: - - The following are **period parameters** combined with the ``schedule``. - - convention: str, :green:`optional (set by 'defaults')` - The day count convention applied to calculations of period accrual dates. - See :meth:`~rateslib.scheduling.dcf`. - - .. note:: - - The following define **rate parameters**. - - fixed_rate: float, Dual, Dual2, Variable, :green:`optional` - The fixed rate of each composited :class:`~rateslib.periods.components.FixedPeriod`. - .. note:: The following parameters define **indexation**. The *Period* will be considered @@ -138,15 +165,6 @@ class FixedLeg(_BaseLeg, _WithExDiv): no notional exchanges. This represents one component of, for example, an :class:`~rateslib.instruments.IRS`. - .. ipython:: python - :suppress: - - from rateslib import fixings - from pandas import Series - from rateslib.legs.components import FixedLeg - from rateslib import Schedule - from datetime import datetime as dt - .. ipython:: python leg = FixedLeg( @@ -299,7 +317,7 @@ class FixedLeg(_BaseLeg, _WithExDiv): - **Multiple** :class:`~rateslib.data.fixings.FXFixing`, at future deliveries, with notional exchanges. This is the type used by MTM :class:`~rateslib.instruments.XCS`. In this case the foreign notional is determined at the start of each period by a known - fixing and there an additional MTM cashflow exchange at the start of a period to adjust + fixing and there are additional MTM cashflow exchanges at the start of a period to adjust for that fixing, i.e. ``schedule.pschedule2[i]``. .. ipython:: python @@ -419,21 +437,17 @@ class FixedLeg(_BaseLeg, _WithExDiv): fixings.pop("EURUSD_1600") fixings.pop("MY_RPI") - Examples - -------- - See :ref:`Leg Examples` - """ @property def settlement_params(self) -> _SettlementParams: """The :class:`~rateslib.periods.components.parameters._SettlementParams` associated with - the first :class:`~rateslib.periods.components.FloatPeriod`.""" + the first :class:`~rateslib.periods.components.FixedPeriod`.""" return self._regular_periods[0].settlement_params @cached_property def periods(self) -> list[_BasePeriod]: - """Combine all period collection types into an ordered list.""" + """A list of all contained *Periods*.""" periods_: list[_BasePeriod] = [] if self._exchange_periods[0] is not None: @@ -469,10 +483,14 @@ def fixed_rate(self, value: DualTypes_) -> None: @property def schedule(self) -> Schedule: + """The :class:`~rateslib.scheduling.Schedule` object of *Leg*.""" return self._schedule @property def amortization(self) -> Amortization: + """ + The :class:`~rateslib.legs.components.Amortization` object associated with the schedule. + """ return self._amortization def __init__( @@ -557,18 +575,6 @@ def __init__( ) self._exchange_periods = (_ini_cf, _final_cf) - def fx_delivery(i: int) -> datetime: - if not mtm: - # then ND type is a one-fixing only - return self.schedule.pschedule2[0] - else: - if final_exchange_: - # then ND type is a XCS - return self.schedule.pschedule2[i] - else: - # then ND type is IRS - return self.schedule.pschedule[i + 1] - self._regular_periods: tuple[FixedPeriod, ...] = tuple( [ FixedPeriod( # type: ignore[abstract] @@ -591,7 +597,7 @@ def fx_delivery(i: int) -> datetime: # non-deliverable : Not allowed with notional exchange pair=pair, fx_fixings=fx_fixings_[0] if not mtm else fx_fixings_[i], - delivery=fx_delivery(i), + delivery=_fx_delivery(i, mtm, final_exchange_, schedule, False), # index params index_base=index_base, index_lag=index_lag, @@ -620,9 +626,9 @@ def fx_delivery(i: int) -> datetime: # non-deliverable params pair=pair, fx_fixings=fx_fixings_[0] if not mtm else fx_fixings_[i + 1], - delivery=self.schedule.pschedule2[0] - if not mtm - else self.schedule.pschedule2[i + 1], # schedule for exchanges + delivery=_fx_delivery( + i, mtm, True, schedule, True + ), # schedule for exchanges # index params index_base=index_base, index_lag=index_lag, @@ -705,20 +711,118 @@ def spread( class ZeroFixedLeg(_BaseLeg): """ - Create a zero coupon fixed leg composed of a single - :class:`~rateslib.periods.FixedPeriod` . + A zero coupon *Leg* composed of a single + :class:`~rateslib.periods.components.ZeroFixedPeriod` . + + .. rubric:: Examples + + .. ipython:: python + :suppress: + + from rateslib.legs.components import ZeroFixedLeg + from rateslib.scheduling import Schedule + from datetime import datetime as dt + from pandas import Series + + .. ipython:: python + + zfl = ZeroFixedLeg( + schedule=Schedule( + effective=dt(2000, 2, 1), + termination=dt(2002, 2, 1), + frequency="S", + ), + fixed_rate=2.5, + notional=10e6, + ) + zfl.cashflows() + + .. role:: red + + .. role:: green + + Parameters + ---------- + schedule: Schedule, :red:`required` + The :class:`~rateslib.scheduling.Schedule` object which structures contiguous *Periods*. + The schedule object also contains data for payment dates, payment dates for notional + exchanges and ex-dividend dates for each period. + + .. note:: + + The following are **period parameters** combined with the ``schedule``. + + convention: str, :green:`optional (set by 'defaults')` + The day count convention applied to calculations of period accrual dates. + See :meth:`~rateslib.scheduling.dcf`. + + .. note:: + + The following define generalised **settlement** parameters. + + currency : str, :green:`optional (set by 'defaults')` + The local settlement currency of the leg (3-digit code). + notional : float, Dual, Dual2, Variable, :green:`optional (set by 'defaults')` + The initial leg notional, defined in units of *reference currency*. + initial_exchange : bool, :green:`optional (set as False)` + Whether to also include an initial notional exchange. If *True* then ``final_exchange`` + **will** also be set to *True*. + final_exchange : bool, :green:`optional (set as initial_exchange)` + Whether to also include a final notional exchange and interim amortization + notional exchanges. + + .. note:: + + The following define **rate parameters**. + + fixed_rate: float, Dual, Dual2, Variable, :green:`optional` + The IRR of the composited :class:`~rateslib.periods.components.ZeroFixedPeriod`. + + .. note:: + + The following define **non-deliverable** parameters. If the *Leg* is directly + deliverable then do not set a non-deliverable ``pair`` or any ``fx_fixings``. + + pair: str, :green:`optional` + The currency pair for :class:`~rateslib.data.fixings.FXFixing` that determines *Period* + settlement. The *reference currency* is implied from ``pair``. Must include ``currency``. + fx_fixings: float, Dual, Dual2, Variable, Series, str, 2-tuple or list, :green:`optional` + The value of the :class:`~rateslib.data.fixings.FXFixing` for each *Period* according + to non-deliverability. Review the **notes** section non-deliverability. + mtm: bool, :green:`optional (set to False)` + Define whether the non-deliverability depends on a single + :class:`~rateslib.data.fixings.FXFixing` defined at the start of the *Leg*, or the end. + Review the **notes** section non-deliverability. + + .. note:: + The following parameters define **indexation**. The *Period* will be considered + indexed if any of ``index_method``, ``index_lag``, ``index_base``, ``index_fixings`` + are given. + + index_method : IndexMethod, str, :green:`optional (set by 'defaults')` + The interpolation method, or otherwise, to determine index values from reference dates. + index_lag: int, :green:`optional (set by 'defaults')` + The indexation lag, in months, applied to the determination of index values. + index_base: float, Dual, Dual2, Variable, :green:`optional` + The specific value applied as the base index value for all *Periods*. + If not given and ``index_fixings`` is a string fixings identifier that will be + used to determine the base index value. + index_fixings: float, Dual, Dual2, Variable, Series, str, 2-tuple or list, :green:`optional` + The index value for the reference date. + Best practice is to supply this value as string identifier relating to the global + ``fixings`` object. """ @property def settlement_params(self) -> _SettlementParams: """The :class:`~rateslib.periods.components.parameters._SettlementParams` associated with - the first :class:`~rateslib.periods.components.FloatPeriod`.""" + the :class:`~rateslib.periods.components.ZeroFixedPeriod`.""" return self._regular_periods[0].settlement_params @cached_property def periods(self) -> list[_BasePeriod]: - """Combine all period collection types into an ordered list.""" + """A list of all contained *Periods*.""" periods_: list[_BasePeriod] = [] if self._exchange_periods[0] is not None: @@ -731,28 +835,33 @@ def periods(self) -> list[_BasePeriod]: @property def schedule(self) -> Schedule: + """The :class:`~rateslib.scheduling.Schedule` object of *Leg*.""" return self._schedule @property def amortization(self) -> Amortization: + """ + The :class:`~rateslib.legs.components.Amortization` object associated with the schedule. + """ return self._amortization def __init__( self, schedule: Schedule, *, + # period + convention: str_ = NoInput(0), + # rate params fixed_rate: NoInput = NoInput(0), # settlement and currency notional: DualTypes_ = NoInput(0), currency: str_ = NoInput(0), + initial_exchange: bool = False, + final_exchange: bool = False, # non-deliverable pair: str_ = NoInput(0), fx_fixings: LegFixings = NoInput(0), mtm: bool = False, - # period - convention: str_ = NoInput(0), - initial_exchange: bool = False, - final_exchange: bool = False, # index params index_base: DualTypes_ = NoInput(0), index_lag: int_ = NoInput(0), @@ -849,6 +958,8 @@ def __init__( @property def fixed_rate(self) -> DualTypes_: + """The fixed rate parameter of the composited + :class:`~rateslib.periods.components.ZeroFixedPeriod`.""" return self._fixed_rate @fixed_rate.setter @@ -903,20 +1014,144 @@ def spread( class ZeroIndexLeg(_BaseLeg): """ - Create a zero coupon fixed leg composed of a single - :class:`~rateslib.periods.FixedPeriod` . + A *Leg* composed of *indexed* :class:`~rateslib.periods.components.Cashflow` at termination, + and possibly effective. + + .. rubric:: Examples + + .. ipython:: python + :suppress: + + from rateslib.legs.components import ZeroIndexLeg + from rateslib.scheduling import Schedule + from datetime import datetime as dt + from pandas import Series + + .. ipython:: python + + fixings.add("CPI_UK", Series(index=[dt(2000, 1, 1), dt(2002, 1, 1)], data=[100.0, 115.0])) + zil = ZeroIndexLeg( + schedule=Schedule( + effective=dt(2000, 2, 1), + termination=dt(2002, 2, 1), + frequency="Z", + ), + index_lag=1, + index_fixings="CPI_UK", + notional=10e6, + ) + zil.cashflows() + + .. ipython:: python + :suppress: + + fixings.pop("CPI_UK") + + .. role:: red + + .. role:: green + + Parameters + ---------- + schedule: Schedule, :red:`required` + The :class:`~rateslib.scheduling.Schedule` object which structures contiguous *Periods*. + The schedule object also contains data for payment dates, payment dates for notional + exchanges and ex-dividend dates for each period. Only the start and end of the schedule are + relevant for this *Zero* type *Leg*. + + .. note:: + + The following define generalised **settlement** parameters. + + currency : str, :green:`optional (set by 'defaults')` + The local settlement currency of the leg (3-digit code). + notional : float, Dual, Dual2, Variable, :green:`optional (set by 'defaults')` + The initial leg notional, defined in units of *reference currency*. + initial_exchange : bool, :green:`optional (set as False)` + Whether to also include an initial notional exchange. If *True* then ``final_exchange`` + **will** also be set to *True*. + final_exchange : bool, :green:`optional (set as initial_exchange)` + Whether to also include a final notional exchange and interim amortization + notional exchanges. + + .. note:: + + The following are **period parameters** combined with the ``schedule``. + + convention: str, :green:`optional (set by 'defaults')` + The day count convention applied to calculations of period accrual dates. + See :meth:`~rateslib.scheduling.dcf`. + + .. note:: + + The following define **non-deliverable** parameters. If the *Leg* is directly + deliverable then do not set a non-deliverable ``pair`` or any ``fx_fixings``. + + pair: str, :green:`optional` + The currency pair for :class:`~rateslib.data.fixings.FXFixing` that determines *Period* + settlement. The *reference currency* is implied from ``pair``. Must include ``currency``. + fx_fixings: float, Dual, Dual2, Variable, Series, str, 2-tuple or list, :green:`optional` + The value of the :class:`~rateslib.data.fixings.FXFixing` for each *Period* according + to non-deliverability. Review the **notes** section non-deliverability. + mtm: bool, :green:`optional (set to False)` + Define whether the non-deliverability depends on a single + :class:`~rateslib.data.fixings.FXFixing` defined at the start of the *Leg*, or the end. + Review the **notes** section non-deliverability. + + .. note:: + + The following parameters define **indexation**. The *Period* will be considered + indexed if any of ``index_method``, ``index_lag``, ``index_base``, ``index_fixings`` + are given. + + index_method : IndexMethod, str, :green:`optional (set by 'defaults')` + The interpolation method, or otherwise, to determine index values from reference dates. + index_lag: int, :green:`optional (set by 'defaults')` + The indexation lag, in months, applied to the determination of index values. + index_base: float, Dual, Dual2, Variable, :green:`optional` + The specific value applied as the base index value for all *Periods*. + If not given and ``index_fixings`` is a string fixings identifier that will be + used to determine the base index value. + index_fixings: float, Dual, Dual2, Variable, Series, str, 2-tuple or list, :green:`optional` + The index value for the reference date. + Best practice is to supply this value as string identifier relating to the global + ``fixings`` object. + + Notes + ----- + A :class:`~rateslib.legs.components.ZeroIndexLeg` contains, at most, two + :class:`~rateslib.periods.components.Cashflow`. Three structures can be configured: + + - One cashflow consisting of only the **indexed amount** relating to some notional value ( + ``initial_exchange`` and ``final_exchange`` are both *False*) + - One cashflow consisting of a notional amount **plus its indexed amount** (``final_exchange`` + is *True*) + - Two cashflows (of opposite directions) exchanging notionals (``initial_exchange`` and + ``final_exchange`` are both *True*) + + **Non-deliverability** + + Non-deliverability behaves in the same way as a :class:`~rateslib.legs.components.FixedLeg`. + If ``mtm`` is *False* then a single :class:`~rateslib.data.fixings.FXFixing` defined by + the ``effective`` date or an agreed transactional value is used for all cashflows. + + With notional exchanges this same principle applies, since there are only upto two cashflows. + + Without notional exchanges and setting ``mtm`` to *True* allows the + :class:`~rateslib.data.fixings.FXFixing` to have a delivery date equal to the future payment + date of the cashflow. """ @property def settlement_params(self) -> _SettlementParams: """The :class:`~rateslib.periods.components.parameters._SettlementParams` associated with - the first :class:`~rateslib.periods.components.FloatPeriod`.""" + the :class:`~rateslib.periods.components.Cashflow` at maturity.""" return self._regular_periods[0].settlement_params @cached_property def periods(self) -> list[_BasePeriod]: - """Combine all period collection types into an ordered list.""" + """A list of all contained *Periods*.""" periods_: list[_BasePeriod] = [] if self._exchange_periods[0] is not None: @@ -927,27 +1162,31 @@ def periods(self) -> list[_BasePeriod]: @property def schedule(self) -> Schedule: + """The :class:`~rateslib.scheduling.Schedule` object of *Leg*.""" return self._schedule @property def amortization(self) -> Amortization: + """ + The :class:`~rateslib.legs.components.Amortization` object associated with the schedule. + """ return self._amortization def __init__( self, schedule: Schedule, *, + # period + convention: str_ = NoInput(0), # settlement and currency notional: DualTypes_ = NoInput(0), currency: str_ = NoInput(0), + initial_exchange: bool = False, + final_exchange: bool = False, # non-deliverable pair: str_ = NoInput(0), fx_fixings: LegFixings = NoInput(0), mtm: bool = False, - # period - convention: str_ = NoInput(0), - initial_exchange: bool = False, - final_exchange: bool = False, # index params index_base: DualTypes_ = NoInput(0), index_lag: int_ = NoInput(0), @@ -994,7 +1233,9 @@ def __init__( # non-deliverable pair=pair, fx_fixings=fx_fixings_[0] if not mtm else fx_fixings_[-1], - delivery=self.schedule.pschedule2[0] if not mtm else self.schedule.pschedule2[-2], + delivery=self.schedule.pschedule2[-1] + if (mtm and not final_exchange_) + else self.schedule.pschedule2[0], # index parameters index_base=index_base, index_lag=index_lag, @@ -1006,3 +1247,33 @@ def __init__( ) self._exchange_periods = (_ini_cf,) self._regular_periods = (_final_cf,) + + def spread(self, *args: Any, **kwargs: Any) -> DualTypes: + return super().spread(*args, **kwargs) # type: ignore[safe-super] + + +def _fx_delivery( + i: int, + mtm: bool, + final_exchange: bool, + schedule: Schedule, + amortisation: bool, +) -> datetime: + """Based on the `mtm` parameter determine the FX fixing dates for period 'i'.""" + if not mtm: + # then ND type is a one-fixing only, so is determined by only a single rate of exchange + # this date is set to the initial payment exchange date of the schedule + return schedule.pschedule2[0] + else: + if final_exchange and not amortisation: + # then ND type is a XCS with notional exchanges, and the FX fixing is set in advance + # with a MTM cashflow handling FX fixing changes over the period. + return schedule.pschedule2[i] + elif final_exchange and amortisation: + # then this is an amortisation amount of whose fixing is measured at the end of the + # period + return schedule.pschedule2[i + 1] + else: + # then ND type is IRS without notional exchanges, and the FX Fixing is set + # as the payment date of the cashflow + return schedule.pschedule[i + 1] diff --git a/python/rateslib/legs/components/float.py b/python/rateslib/legs/components/float.py index dd35b1d7..520bc787 100644 --- a/python/rateslib/legs/components/float.py +++ b/python/rateslib/legs/components/float.py @@ -12,6 +12,7 @@ from rateslib.enums.generics import NoInput, _drb from rateslib.enums.parameters import FloatFixingMethod, SpreadCompoundMethod from rateslib.legs.components.amortization import Amortization, _AmortizationType, _get_amortization +from rateslib.legs.components.custom import CustomLeg from rateslib.legs.components.protocols import _BaseLeg, _WithExDiv from rateslib.periods.components import Cashflow, FloatPeriod, MtmCashflow, ZeroFloatPeriod from rateslib.periods.components.parameters import _FloatRateParams, _SettlementParams @@ -34,12 +35,37 @@ datetime_, int_, str_, + Sequence, ) class FloatLeg(_BaseLeg, _WithExDiv): """ - Define a *Leg* containing :class:`~rateslib.periods.components.FloatPeriod`. + A *Leg* containing :class:`~rateslib.periods.components.FloatPeriod`. + + .. rubric:: Examples + + .. ipython:: python + :suppress: + + from rateslib import fixings, Schedule + from pandas import Series + from rateslib.legs.components import FloatLeg + from datetime import datetime as dt + + .. ipython:: python + + fl = FloatLeg( + schedule=Schedule( + effective=dt(2000, 2, 1), + termination=dt(2002, 2, 1), + frequency="S", + ), + convention="Act360", + float_spread=25.0, + notional=10e6, + ) + fl.cashflows() .. role:: red @@ -145,6 +171,12 @@ class FloatLeg(_BaseLeg, _WithExDiv): A flag which indicates that the nominal amount is deducted from the cashflow leaving only the indexed up quantity. + Notes + ----- + The various combinations of **amortisation**, **non-deliverability**, **indexation**, + and **notional exchanges** are identical to, and demonstrated in the documentation for, a + :class:`~rateslib.legs.components.FixedLeg` object. + """ @property @@ -161,7 +193,7 @@ def settlement_params(self) -> _SettlementParams: @property def periods(self) -> list[_BasePeriod]: - """Combine all period collection types into an ordered list.""" + """A list of all contained *Periods*.""" periods_: list[_BasePeriod] = [] if self._exchange_periods[0] is not None: @@ -187,6 +219,8 @@ def periods(self) -> list[_BasePeriod]: @property def float_spread(self) -> DualTypes: + """The float spread parameter of each composited + :class:`~rateslib.periods.components.FloatPeriod`.""" return self._regular_periods[0].rate_params.float_spread @float_spread.setter @@ -196,10 +230,14 @@ def float_spread(self, value: DualTypes) -> None: @property def schedule(self) -> Schedule: + """The :class:`~rateslib.scheduling.Schedule` object of *Leg*.""" return self._schedule @property def amortization(self) -> Amortization: + """ + The :class:`~rateslib.legs.components.Amortization` object associated with the schedule. + """ return self._amortization def __init__( @@ -484,6 +522,130 @@ def s(g: DualTypes) -> DualTypes: class ZeroFloatLeg(_BaseLeg): + """ + A zero coupon *Leg* composd of a single :class:`~rateslib.periods.components.ZeroFloatPeriod`. + + .. rubric:: Examples + + .. ipython:: python + :suppress: + + from rateslib.legs.components import ZeroFloatLeg + from rateslib.scheduling import Schedule + from datetime import datetime as dt + from pandas import Series + + .. ipython:: python + + zfl = ZeroFloatLeg( + schedule=Schedule( + effective=dt(2000, 2, 1), + termination=dt(2002, 2, 1), + frequency="S", + ), + notional=10e6, + ) + zfl.cashflows() + zfl.float_periods.cashflows() + + .. role:: red + + .. role:: green + + Parameters + ---------- + schedule: Schedule, :red:`required` + The :class:`~rateslib.scheduling.Schedule` object which structures contiguous *Periods*. + The schedule object also contains data for payment dates, payment dates for notional + exchanges and ex-dividend dates for each period. + + .. note:: + + The following are **period parameters** combined with the ``schedule``. + + convention: str, :green:`optional (set by 'defaults')` + The day count convention applied to calculations of period accrual dates. + See :meth:`~rateslib.scheduling.dcf`. + + .. note:: + + The following define generalised **settlement** parameters. + + currency : str, :green:`optional (set by 'defaults')` + The local settlement currency of the leg (3-digit code). + notional : float, Dual, Dual2, Variable, :green:`optional (set by 'defaults')` + The initial leg notional, defined in units of *reference currency*. + initial_exchange : bool, :green:`optional (set as False)` + Whether to also include an initial notional exchange. If *True* then ``final_exchange`` + **will** also be set to *True*. + final_exchange : bool, :green:`optional (set as initial_exchange)` + Whether to also include a final notional exchange and interim amortization + notional exchanges. + + .. note:: + + The following define **rate parameters**. + + fixing_method: FloatFixingMethod, str, :green:`optional (set by 'defaults')` + The :class:`~rateslib.enums.parameters.FloatFixingMethod` describing the determination + of the floating rate for each period. + method_param: int, :green:`optional (set by 'defaults')` + A specific parameter that is used by the specific ``fixing_method``. + fixing_frequency: Frequency, str, :green:`optional (set by 'frequency' or '1B')` + The :class:`~rateslib.scheduling.Frequency` as a component of the + :class:`~rateslib.data.fixings.FloatRateIndex`. If not given is assumed to match the + frequency of the schedule for an IBOR type ``fixing_method`` or '1B' if RFR type. + fixing_series: FloatRateSeries, str, :green:`optional (implied by other parameters)` + The :class:`~rateslib.data.fixings.FloatRateSeries` as a component of the + :class:`~rateslib.data.fixings.FloatRateIndex`. If not given inherits attributes given + such as the ``calendar``, ``convention``, ``method_param`` etc. + float_spread: float, Dual, Dual2, Variable, :green:`optional (set as 0.0)` + The amount (in bps) added to the rate in each period rate determination. + spread_compound_method: SpreadCompoundMethod, str, :green:`optional (set by 'defaults')` + The :class:`~rateslib.enums.parameters.SpreadCompoundMethod` used in the calculation + of the period rate when combining a ``float_spread``. Used **only** with RFR type + ``fixing_method``. + rate_fixings: float, Dual, Dual2, Variable, Series, str, :green:`optional` + See XXX (working with fixings). + The value of the rate fixing. If a scalar, is used directly. If a string identifier, links + to the central ``fixings`` object and data loader. + + .. note:: + + The following define **non-deliverable** parameters. If the *Leg* is directly + deliverable then do not set a non-deliverable ``pair`` or any ``fx_fixings``. + + pair: str, :green:`optional` + The currency pair for :class:`~rateslib.data.fixings.FXFixing` that determines *Period* + settlement. The *reference currency* is implied from ``pair``. Must include ``currency``. + fx_fixings: float, Dual, Dual2, Variable, Series, str, 2-tuple or list, :green:`optional` + The value of the :class:`~rateslib.data.fixings.FXFixing` for each *Period* according + to non-deliverability. Review the **notes** section non-deliverability. + mtm: bool, :green:`optional (set to False)` + Define whether the non-deliverability depends on a single + :class:`~rateslib.data.fixings.FXFixing` defined at the start of the *Leg*, or the end. + Review the **notes** section non-deliverability. + + .. note:: + + The following parameters define **indexation**. The *Period* will be considered + indexed if any of ``index_method``, ``index_lag``, ``index_base``, ``index_fixings`` + are given. + + index_method : IndexMethod, str, :green:`optional (set by 'defaults')` + The interpolation method, or otherwise, to determine index values from reference dates. + index_lag: int, :green:`optional (set by 'defaults')` + The indexation lag, in months, applied to the determination of index values. + index_base: float, Dual, Dual2, Variable, :green:`optional` + The specific value applied as the base index value for all *Periods*. + If not given and ``index_fixings`` is a string fixings identifier that will be + used to determine the base index value. + index_fixings: float, Dual, Dual2, Variable, Series, str, 2-tuple or list, :green:`optional` + The index value for the reference date. + Best practice is to supply this value as string identifier relating to the global + ``fixings`` object. + """ + @property def settlement_params(self) -> _SettlementParams: """The :class:`~rateslib.periods.components.parameters._SettlementParams` associated with @@ -491,8 +653,8 @@ def settlement_params(self) -> _SettlementParams: return self._regular_periods[0].settlement_params @cached_property - def periods(self) -> list[_BasePeriod]: - """Combine all period collection types into an ordered list.""" + def periods(self) -> Sequence[_BasePeriod]: + """A list of all contained *Periods*.""" periods_: list[_BasePeriod] = [] if self._exchange_periods[0] is not None: @@ -505,10 +667,14 @@ def periods(self) -> list[_BasePeriod]: @property def schedule(self) -> Schedule: + """The :class:`~rateslib.scheduling.Schedule` object of *Leg*.""" return self._schedule @property def amortization(self) -> Amortization: + """ + The :class:`~rateslib.legs.components.Amortization` object associated with the schedule. + """ return self._amortization @property @@ -519,6 +685,8 @@ def rate_params(self) -> _FloatRateParams: @property def float_spread(self) -> DualTypes: + """The float spread parameter of each composited + :class:`~rateslib.periods.components.FloatPeriod`.""" return self._regular_periods[0].rate_params.float_spread @float_spread.setter @@ -526,6 +694,12 @@ def float_spread(self, value: DualTypes) -> None: for period in self._regular_periods: period.rate_params.float_spread = value + @property + def float_periods(self) -> CustomLeg: + """A :class:`~rateslib.legs.components.CustomLeg` containing the individual + :class:`~rateslib.periods.components.FloatPeriod`.""" + return CustomLeg(self._regular_periods[0].float_periods) + def __init__( self, schedule: Schedule, diff --git a/python/rateslib/legs/components/protocols/analytic_delta.py b/python/rateslib/legs/components/protocols/analytic_delta.py index 929c92b3..d2c4fac6 100644 --- a/python/rateslib/legs/components/protocols/analytic_delta.py +++ b/python/rateslib/legs/components/protocols/analytic_delta.py @@ -17,12 +17,18 @@ _BasePeriod, datetime_, str_, + Sequence, ) class _WithAnalyticDelta(Protocol): + """ + Protocol to calculate analytical rate delta sensitivities of any *Leg* type. + + """ + @property - def periods(self) -> list[_BasePeriod]: ... + def periods(self) -> Sequence[_BasePeriod]: ... def local_analytic_delta( self, diff --git a/python/rateslib/legs/components/protocols/analytic_fixings.py b/python/rateslib/legs/components/protocols/analytic_fixings.py index 90eacb25..3c072a48 100644 --- a/python/rateslib/legs/components/protocols/analytic_fixings.py +++ b/python/rateslib/legs/components/protocols/analytic_fixings.py @@ -15,12 +15,18 @@ _BaseCurve_, _BasePeriod, datetime_, + Sequence, ) class _WithAnalyticRateFixings(Protocol): + """ + Protocol to calculate analytical rate fixing sensitivities of any *Leg* type. + + """ + @property - def periods(self) -> list[_BasePeriod]: ... + def periods(self) -> Sequence[_BasePeriod]: ... def local_analytic_rate_fixings( self, diff --git a/python/rateslib/legs/components/protocols/cashflows.py b/python/rateslib/legs/components/protocols/cashflows.py index 0e980b5a..6b62f1c2 100644 --- a/python/rateslib/legs/components/protocols/cashflows.py +++ b/python/rateslib/legs/components/protocols/cashflows.py @@ -18,12 +18,18 @@ datetime, datetime_, str_, + Sequence, ) class _WithCashflows(Protocol): + """ + Protocol to generate cashflows of any *Leg* type. + + """ + @property - def periods(self) -> list[_BasePeriod]: ... + def periods(self) -> Sequence[_BasePeriod]: ... def cashflows( self, @@ -72,6 +78,11 @@ def cashflows( class _WithExDiv(Protocol): + """ + Protocol to determine if a *Leg* is ex-dividend on a given settlement. + + """ + @property def schedule(self) -> Schedule: ... diff --git a/python/rateslib/legs/components/protocols/npv.py b/python/rateslib/legs/components/protocols/npv.py index 08449be7..a29810d1 100644 --- a/python/rateslib/legs/components/protocols/npv.py +++ b/python/rateslib/legs/components/protocols/npv.py @@ -18,6 +18,7 @@ _BasePeriod, datetime_, str_, + Sequence, ) @@ -27,10 +28,8 @@ class _WithNPV(Protocol): """ - # _periods: list[_BasePeriod] - @property - def periods(self) -> list[_BasePeriod]: + def periods(self) -> Sequence[_BasePeriod]: ... # """List of *Periods* associated with the *Leg*.""" # return self._periods diff --git a/python/rateslib/periods/components/__init__.py b/python/rateslib/periods/components/__init__.py index 1586cd7c..ec2abb83 100644 --- a/python/rateslib/periods/components/__init__.py +++ b/python/rateslib/periods/components/__init__.py @@ -26,8 +26,6 @@ from rateslib.periods.components.protocols import _BasePeriod, _BasePeriodStatic __all__ = [ - "_BasePeriod", - "_BasePeriodStatic", "Cashflow", # "IndexCashflow", # "NonDeliverableCashflow", diff --git a/python/rateslib/periods/components/float_period.py b/python/rateslib/periods/components/float_period.py index d88f7f3d..9a74f6a1 100644 --- a/python/rateslib/periods/components/float_period.py +++ b/python/rateslib/periods/components/float_period.py @@ -702,6 +702,7 @@ def dcf(self) -> float: @property def float_spread(self) -> DualTypes: + """The float spread parameter of each :class:`~rateslib.periods.components.FloatPeriod`.""" return self._float_periods[0].rate_params.float_spread @float_spread.setter @@ -712,7 +713,7 @@ def float_spread(self, value: DualTypes) -> None: @property def float_periods(self) -> list[FloatPeriod]: """ - The individual :class:`~rateslib.periods.components.period.FloatPeriod` that are + The individual :class:`~rateslib.periods.components.FloatPeriod` that are compounded. """ return self._float_periods diff --git a/python/rateslib/periods/components/protocols/__init__.py b/python/rateslib/periods/components/protocols/__init__.py index adfecdf8..83b12af1 100644 --- a/python/rateslib/periods/components/protocols/__init__.py +++ b/python/rateslib/periods/components/protocols/__init__.py @@ -48,6 +48,8 @@ class _BasePeriodStatic( __all__ = [ + "_BasePeriod", + "_BasePeriodStatic", "_WithNPV", "_WithCashflows", "_WithAnalyticDelta", diff --git a/python/tests/instruments/test_instruments_legacy.py b/python/tests/instruments/test_instruments_legacy.py index ec4c8d0c..461a687d 100644 --- a/python/tests/instruments/test_instruments_legacy.py +++ b/python/tests/instruments/test_instruments_legacy.py @@ -1900,6 +1900,30 @@ def test_imm_dated_fixings_table(self, curve): assert isinstance(result, DataFrame) assert abs(result.iloc[0, 0] - analytic_delta) < 1 + def test_fra_ex_div_and_payment(self): + fra = FRA( + effective=dt(2024, 12, 18), + termination=dt(2025, 3, 19), + spec="sek_fra3", + roll="imm", + curves=curve, + notional=1e9, + payment_lag=2, + ex_div=-1, + ) + assert fra.leg1.periods[0].period_params.start == dt(2024, 12, 18) + assert fra.leg1.periods[0].settlement_params.payment == dt(2024, 12, 20) + assert fra.leg1.periods[0].settlement_params.ex_dividend == dt(2024, 12, 19) + + def test_fra_cashflows_no_curve(self): + fra = FRA( + effective=dt(2000, 1, 1), + termination="6m", + spec="eur_fra6", + fixed_rate=2.0, + ) + assert isinstance(fra.cashflows(), DataFrame) + class TestZCS: @pytest.mark.parametrize(("freq", "exp"), [("Q", 3.53163356950), ("S", 3.54722411409218)]) diff --git a/python/tests/legs/test_legs_legacy.py b/python/tests/legs/test_legs_legacy.py index 9d8c18a0..c8b756a7 100644 --- a/python/tests/legs/test_legs_legacy.py +++ b/python/tests/legs/test_legs_legacy.py @@ -1423,6 +1423,41 @@ def test_four_ways(self, only): assert abs(result1 - result3) < 1e-8 assert abs(result1 - result4) < 1e-8 + @pytest.mark.parametrize( + ("ini", "final", "mtm", "lenn", "nd_dt", "cf"), + [ + (False, False, False, 1, dt(2000, 1, 1), 500e3 * 2.0), + (False, False, True, 1, dt(2001, 1, 1), 500e3 * 3.0), + (False, True, False, 1, dt(2000, 1, 1), 1.5e6 * 2.0), + (False, True, True, 1, dt(2000, 1, 1), 1.5e6 * 2.0), + # (True, False, False, 2, dt(2000, 1, 1)), # final exch True by default + # (True, False, True, 2, dt(2000, 1, 1)), # final exch True by default + (True, True, False, 2, dt(2000, 1, 1), 1.5e6 * 2.0), + (True, True, True, 2, dt(2000, 1, 1), 1.5e6 * 2.0), + ], + ) + def test_attributes(self, ini, final, mtm, lenn, nd_dt, cf) -> None: + name = str(hash(os.urandom(8))) + fixings.add(name, Series(index=[dt(2000, 1, 1), dt(2001, 1, 1)], data=[10.0, 15.0])) + fixings.add(name + "fx", Series(index=[dt(2000, 1, 1), dt(2001, 1, 1)], data=[2.0, 3.0])) + leg = ZeroIndexLeg( + schedule=Schedule(effective=dt(2000, 1, 1), termination=dt(2001, 1, 1), frequency="Z"), + currency="usd", + initial_exchange=ini, + final_exchange=final, + pair="eurusd", + mtm=mtm, + fx_fixings=name + "fx", + index_lag=0, + index_fixings=name, + notional=-1e6, + ) + assert len(leg.periods) == lenn + assert leg.periods[-1].non_deliverable_params.delivery == nd_dt + assert leg.periods[-1].cashflow() == cf + fixings.pop(name) + fixings.pop(name + "fx") + class TestFloatLegExchange: @pytest.mark.skip(reason="v 2.2 removed ability to mutate notional")