From 8d4126b3adda3c59abd003c17da7f1135d862c33 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (win11)" Date: Mon, 13 Jan 2025 12:22:33 +0100 Subject: [PATCH 1/8] TYP: multi ccy derivs --- .../instruments/rates/multi_currency.py | 59 ++++++++++--------- python/rateslib/periods.py | 30 ++++++---- 2 files changed, 51 insertions(+), 38 deletions(-) diff --git a/python/rateslib/instruments/rates/multi_currency.py b/python/rateslib/instruments/rates/multi_currency.py index 0ed2d6a7..a9712225 100644 --- a/python/rateslib/instruments/rates/multi_currency.py +++ b/python/rateslib/instruments/rates/multi_currency.py @@ -125,7 +125,7 @@ def npv( """ self._set_pricing_mid(curves, solver, fx) - curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( + curves_, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( self.curves, solver, curves, @@ -139,10 +139,10 @@ def npv( "Must have some FX information to price FXExchange, either `fx` or " "`solver` containing an FX object.", ) - if not isinstance(fx_, FXRates | FXForwards): + elif not isinstance(fx_, FXRates | FXForwards): # force base_ leg1 currency to be converted consistent. - leg1_npv = self.leg1.npv(curves[0], curves[1], fx_, base_, local) - leg2_npv = self.leg2.npv(curves[2], curves[3], 1.0, base_, local) + leg1_npv = self.leg1.npv(curves_[0], curves_[1], fx_, base_, local) + leg2_npv = self.leg2.npv(curves_[2], curves_[3], 1.0, base_, local) warnings.warn( "When valuing multi-currency derivatives it not best practice to " "supply `fx` as numeric.\nYour input:\n" @@ -155,8 +155,8 @@ def npv( UserWarning, ) else: - leg1_npv = self.leg1.npv(curves[0], curves[1], fx_, base_, local) - leg2_npv = self.leg2.npv(curves[2], curves[3], fx_, base_, local) + leg1_npv = self.leg1.npv(curves_[0], curves_[1], fx_, base_, local) + leg2_npv = self.leg2.npv(curves_[2], curves_[3], fx_, base_, local) if local: return { @@ -178,7 +178,7 @@ def cashflows( For arguments see :meth:`BaseMixin.npv` """ self._set_pricing_mid(curves, solver, fx) - curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( + curves_, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( self.curves, solver, curves, @@ -187,8 +187,8 @@ def cashflows( NoInput(0), ) seq = [ - self.leg1.cashflows(curves[0], curves[1], fx_, base_), - self.leg2.cashflows(curves[2], curves[3], fx_, base_), + self.leg1.cashflows(curves_[0], curves_[1], fx_, base_), + self.leg2.cashflows(curves_[2], curves_[3], fx_, base_), ] _ = DataFrame.from_records(seq) _.index = MultiIndex.from_tuples([("leg1", 0), ("leg2", 0)]) @@ -206,7 +206,7 @@ def rate( For arguments see :meth:`BaseMixin.rate` """ - curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( + curves_, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( self.curves, solver, curves, @@ -215,7 +215,7 @@ def rate( self.leg1.currency, ) if isinstance(fx_, FXRates | FXForwards): - imm_fx = fx_.rate(self.pair) + imm_fx: FX_ = fx_.rate(self.pair) else: imm_fx = fx_ @@ -224,7 +224,7 @@ def rate( "`fx` must be supplied to price FXExchange object.\n" "Note: it can be attached to and then gotten from a Solver.", ) - _ = forward_fx(self.settlement, curves[1], curves[3], imm_fx) + _ = forward_fx(self.settlement, curves_[1], curves_[3], imm_fx) return _ def delta(self, *args: Any, **kwargs: Any) -> DataFrame: @@ -328,6 +328,9 @@ class XCS(BaseDerivative): Required keyword arguments for :class:`~rateslib.instruments.BaseDerivative`. """ + leg1: FixedLeg | FloatLeg + leg2: FixedLeg | FloatLeg | FloatLegMtm | FixedLegMtm + def __init__( self, *args: Any, @@ -734,7 +737,7 @@ def rate( return _ if _is_float_tgt_leg else _ * 0.01 - def spread(self, *args, **kwargs): + def spread(self, *args: Any, **kwargs: Any) -> DualTypes: """ Alias for :meth:`~rateslib.instruments.BaseXCS.rate` """ @@ -747,7 +750,7 @@ def cashflows( fx: FXForwards | NoInput = NoInput(0), base: str_ = NoInput(0), ): - curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( + curves_, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( self.curves, solver, curves, @@ -757,13 +760,13 @@ def cashflows( ) if self._is_unpriced: - self._set_pricing_mid(curves, solver, fx_) + self._set_pricing_mid(curves_, solver, fx_) self._set_fx_fixings(fx_) if self._is_mtm: self.leg2._do_not_repeat_set_periods = True - ret = super().cashflows(curves, solver, fx_, base_) + ret = super().cashflows(curves_, solver, fx_, base_) if self._is_mtm: self.leg2._do_not_repeat_set_periods = False # reset the mtm calc return ret @@ -810,7 +813,7 @@ def fixings_table( ------- DataFrame """ - curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( + curves_, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( self.curves, solver, curves, @@ -821,8 +824,8 @@ def fixings_table( try: df1 = self.leg1.fixings_table( - curve=curves[0], - disc_curve=curves[1], + curve=curves_[0], + disc_curve=curves_[1], fx=fx_, base=base_, approximate=approximate, @@ -835,8 +838,8 @@ def fixings_table( try: df2 = self.leg2.fixings_table( - curve=curves[2], - disc_curve=curves[3], + curve=curves_[2], + disc_curve=curves_[3], fx=fx_, base=base_, approximate=approximate, @@ -1146,7 +1149,7 @@ def _set_pricing_mid( curves: Curves_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards | NoInput = NoInput(0), - ): + ) -> None: # This function ASSUMES that the instrument is unpriced, i.e. all of # split_notional, fx_fixing and points have been initialised as None. @@ -1161,7 +1164,7 @@ def rate( solver: Solver_ = NoInput(0), fx: FXForwards | NoInput = NoInput(0), fixed_rate: bool = False, - ): + ) -> DualTypes: """ Return the mid-market pricing parameter of the FXSwapS. @@ -1188,7 +1191,7 @@ def rate( ------- float, Dual or Dual2 """ - curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( + curves_, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( self.curves, solver, curves, @@ -1197,11 +1200,11 @@ def rate( self.leg1.currency, ) # set the split notional from the curve if not available - self._set_split_notional(curve=curves[1]) + self._set_split_notional(curve=curves_[1]) # then we will set the fx_fixing and leg2 initial notional. # self._set_fx_fixings(fx) # this will be done by super().rate() - leg2_fixed_rate = super().rate(curves, solver, fx_, leg=2) + leg2_fixed_rate = super().rate(curves_, solver, fx_, leg=2) if fixed_rate: return leg2_fixed_rate @@ -1218,8 +1221,8 @@ def cashflows( solver: Solver_ = NoInput(0), fx: FXForwards | NoInput = NoInput(0), base: str_ = NoInput(0), - ): + ) -> DataFrame: if self._is_unpriced: self._set_pricing_mid(curves, solver, fx) - ret = super().cashflows(curves, solver, fx, base) + ret: DataFrame = super().cashflows(curves, solver, fx, base) return ret diff --git a/python/rateslib/periods.py b/python/rateslib/periods.py index c2dce30b..08a8ba80 100644 --- a/python/rateslib/periods.py +++ b/python/rateslib/periods.py @@ -61,7 +61,17 @@ from rateslib.splines import evaluate if TYPE_CHECKING: - from rateslib.typing import FX_, CalInput, CalTypes, Curve_, CurveOption_, DualTypes, Number + from rateslib.typing import ( + FX_, + NPV, + CalInput, + CalTypes, + Curve_, + CurveOption_, + DualTypes, + Number, + str_, + ) # Licence: Creative Commons - Attribution-NonCommercial-NoDerivatives 4.0 International # Commercial use of this code, and/or copying and redistribution is prohibited. @@ -2684,12 +2694,12 @@ def rate(self) -> DualTypes | None: def npv( self, - curve: Curve | NoInput = NoInput(0), - disc_curve: Curve | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), + curve: CurveOption_ = NoInput(0), + disc_curve: Curve_ = NoInput(0), + fx: FX_ = NoInput(0), + base: str_ = NoInput(0), local: bool = False, - ) -> DualTypes | dict[str, DualTypes]: + ) -> NPV: """ Return the NPV of the *Cashflow*. See @@ -2701,10 +2711,10 @@ def npv( def cashflows( self, - curve: Curve | NoInput = NoInput(0), - disc_curve: Curve | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), + curve: CurveOption_ = NoInput(0), + disc_curve: Curve_ = NoInput(0), + fx: FX_ = NoInput(0), + base: str_ = NoInput(0), ) -> dict[str, Any]: """ Return the cashflows of the *Cashflow*. From e58f3187bdbafb5c61df0a3523eacca597552c35 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (win11)" Date: Mon, 13 Jan 2025 13:08:30 +0100 Subject: [PATCH 2/8] TYP: multi ccy derivs --- python/rateslib/legs.py | 28 +++++++++++++++------------- python/rateslib/periods.py | 8 ++++---- python/rateslib/typing.py | 3 +++ 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/python/rateslib/legs.py b/python/rateslib/legs.py index 5f5c5733..417855e9 100644 --- a/python/rateslib/legs.py +++ b/python/rateslib/legs.py @@ -34,7 +34,7 @@ from rateslib.scheduling import Schedule if TYPE_CHECKING: - from rateslib.typing import FX_, CalInput, CurveOption_, DualTypes, FixingsRates, Period + from rateslib.typing import FX_, CalInput, CurveOption_, DualTypes, FixingsRates, Period, FixingsFx_ # Licence: Creative Commons - Attribution-NonCommercial-NoDerivatives 4.0 International # Commercial use of this code, and/or copying and redistribution is prohibited. @@ -2546,14 +2546,7 @@ def fx_fixings(self) -> list[DualTypes]: return self._fx_fixings @fx_fixings.setter - def fx_fixings( - self, - value: NoInput # type: ignore[type-var] - | DualTypes - | list[DualTypes] - | Series[DualTypes] - | tuple[DualTypes, Series[DualTypes]], - ) -> None: + def fx_fixings(self, value: FixingsFx_) -> None: # type: ignore[type-var] if isinstance(value, NoInput): self._fx_fixings: list[DualTypes] = [] elif isinstance(value, list): @@ -2611,14 +2604,20 @@ def _get_fx_fixings(self, fx: FX_) -> list[DualTypes]: raise ValueError( "`fx` is required when `fx_fixings` are not pre-set and " "if rateslib option `no_fx_fixings_for_xcs` is set to " - "'raise'.", + "'raise'.\nFurther info: You are trying to value a mark-to-market " + "leg on a multi-currency derivative.\nThese require FX fixings and if " + "those are not given then an FXForwards object should be provided which " + "will calculate the relevant FX rates." ) if n_given == 0: if defaults.no_fx_fixings_for_xcs.lower() == "warn": warnings.warn( "Using 1.0 for FX, no `fx` or `fx_fixing` given and " - "rateslib option `no_fx_fixings_for_xcs` is set to " - "'warn'.", + "the option `defaults.no_fx_fixings_for_xcs` is set to " + "'warn'.\nFurther info: You are trying to value a mark-to-market " + "leg on a multi-currency derivative.\nThese require FX fixings and if " + "those are not given then an FXForwards object should be provided which " + "will calculate the relevant FX rates.", UserWarning, ) fx_fixings_ = [1.0] * n_req @@ -2627,7 +2626,10 @@ def _get_fx_fixings(self, fx: FX_) -> list[DualTypes]: warnings.warn( "Using final FX fixing given for missing periods, " "rateslib option `no_fx_fixings_for_xcs` is set to " - "'warn'.", + "'warn'.\nFurther info: You are trying to value a mark-to-market " + "leg on a multi-currency derivative.\nThese require FX fixings and if " + "those are not given then an FXForwards object should be provided which " + "will calculate the relevant FX rates.", UserWarning, ) fx_fixings_.extend([fx_fixings_[-1]] * (n_req - n_given)) diff --git a/python/rateslib/periods.py b/python/rateslib/periods.py index 08a8ba80..106bc991 100644 --- a/python/rateslib/periods.py +++ b/python/rateslib/periods.py @@ -3257,10 +3257,10 @@ def real_cashflow(self) -> DualTypes: def cashflows( self, - curve: Curve | NoInput = NoInput(0), - disc_curve: Curve | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), + curve: CurveOption_ = NoInput(0), + disc_curve: Curve_ = NoInput(0), + fx: FX_ = NoInput(0), + base: str_ = NoInput(0), ) -> dict[str, Any]: """ Return the cashflows of the *IndexCashflow*. diff --git a/python/rateslib/typing.py b/python/rateslib/typing.py index e183ff81..5b11481f 100644 --- a/python/rateslib/typing.py +++ b/python/rateslib/typing.py @@ -88,6 +88,9 @@ FixingsRates: TypeAlias = "Series[DualTypes] | list[DualTypes | list[DualTypes] | Series[DualTypes] | NoInput] | tuple[DualTypes, Series[DualTypes]] | DualTypes | NoInput" +FixingsFx: TypeAlias = "DualTypes | list[DualTypes] | Series[DualTypes] | tuple[DualTypes, Series[DualTypes]]" +FixingsFx_: TypeAlias = "FixingsFx | NoInput" + str_: TypeAlias = "str | NoInput" from rateslib.curves import Curve as Curve # noqa: E402 From 1e03f4df591c593cdc9c5c0951e64ccbc445d985 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (win11)" Date: Mon, 13 Jan 2025 16:27:43 +0100 Subject: [PATCH 3/8] TYP: multi ccy derivs --- python/rateslib/curves/_parsers.py | 8 +++ python/rateslib/fx/fx_forwards.py | 2 +- .../instruments/rates/multi_currency.py | 51 ++++++++++++------- python/tests/test_instruments.py | 6 +++ 4 files changed, 49 insertions(+), 18 deletions(-) diff --git a/python/rateslib/curves/_parsers.py b/python/rateslib/curves/_parsers.py index c2682580..33a8ec1b 100644 --- a/python/rateslib/curves/_parsers.py +++ b/python/rateslib/curves/_parsers.py @@ -208,3 +208,11 @@ def _validate_disc_curves_are_not_dict(curves_tuple: Curves_Tuple) -> Curves_Dis curves_tuple[2], _validate_disc_curve_is_not_dict(curves_tuple[3]), ) + + +def _validate_curve_not_no_input(curve: Curve_) -> Curve: + if isinstance(curve, NoInput): + raise ValueError( + "`curve` must be supplied. Got NoInput or None." + ) + return curve diff --git a/python/rateslib/fx/fx_forwards.py b/python/rateslib/fx/fx_forwards.py index b1399688..a9fff142 100644 --- a/python/rateslib/fx/fx_forwards.py +++ b/python/rateslib/fx/fx_forwards.py @@ -1236,7 +1236,7 @@ def forward_fx( """ # noqa: E501 if date == fx_settlement: # noqa: SIM114 return fx_rate # noqa: SIM114 - elif date == curve_domestic.node_dates[0] and fx_settlement is NoInput.blank: # noqa: SIM114 + elif date == curve_domestic.node_dates[0] and isinstance(fx_settlement, NoInput): # noqa: SIM114 return fx_rate # noqa: SIM114 _: DualTypes = curve_domestic[date] / curve_foreign[date] diff --git a/python/rateslib/instruments/rates/multi_currency.py b/python/rateslib/instruments/rates/multi_currency.py index a9712225..8af0c15d 100644 --- a/python/rateslib/instruments/rates/multi_currency.py +++ b/python/rateslib/instruments/rates/multi_currency.py @@ -7,7 +7,7 @@ from pandas import DataFrame, DatetimeIndex, MultiIndex, Series from rateslib import defaults -from rateslib.curves import Curve +from rateslib.curves._parsers import _validate_curve_not_no_input from rateslib.default import NoInput, _drb from rateslib.dual import Dual, Dual2 from rateslib.dual.utils import _dual_float @@ -141,8 +141,8 @@ def npv( ) elif not isinstance(fx_, FXRates | FXForwards): # force base_ leg1 currency to be converted consistent. - leg1_npv = self.leg1.npv(curves_[0], curves_[1], fx_, base_, local) - leg2_npv = self.leg2.npv(curves_[2], curves_[3], 1.0, base_, local) + leg1_npv: NPV = self.leg1.npv(curves_[0], curves_[1], fx_, base_, local) + leg2_npv: NPV = self.leg2.npv(curves_[2], curves_[3], 1.0, base_, local) warnings.warn( "When valuing multi-currency derivatives it not best practice to " "supply `fx` as numeric.\nYour input:\n" @@ -160,10 +160,10 @@ def npv( if local: return { - k: leg1_npv.get(k, 0) + leg2_npv.get(k, 0) for k in set(leg1_npv) | set(leg2_npv) + k: leg1_npv.get(k, 0) + leg2_npv.get(k, 0) for k in set(leg1_npv) | set(leg2_npv) # type: ignore[union-attr, arg-type] } else: - return leg1_npv + leg2_npv + return leg1_npv + leg2_npv # type: ignore[operator] def cashflows( self, @@ -190,7 +190,7 @@ def cashflows( self.leg1.cashflows(curves_[0], curves_[1], fx_, base_), self.leg2.cashflows(curves_[2], curves_[3], fx_, base_), ] - _ = DataFrame.from_records(seq) + _: DataFrame = DataFrame.from_records(seq) _.index = MultiIndex.from_tuples([("leg1", 0), ("leg2", 0)]) return _ @@ -214,17 +214,20 @@ def rate( base, self.leg1.currency, ) - if isinstance(fx_, FXRates | FXForwards): - imm_fx: FX_ = fx_.rate(self.pair) - else: - imm_fx = fx_ + curves_1 = _validate_curve_not_no_input(curves_[1]) + curves_3 = _validate_curve_not_no_input(curves_[3]) - if isinstance(imm_fx, NoInput): + if isinstance(fx_, FXRates | FXForwards): + imm_fx: DualTypes = fx_.rate(self.pair) + elif isinstance(fx_, NoInput): raise ValueError( "`fx` must be supplied to price FXExchange object.\n" "Note: it can be attached to and then gotten from a Solver.", ) - _ = forward_fx(self.settlement, curves_[1], curves_[3], imm_fx) + else: + imm_fx = fx_ + + _: DualTypes = forward_fx(self.settlement, curves_1, curves_3, imm_fx) return _ def delta(self, *args: Any, **kwargs: Any) -> DataFrame: @@ -625,7 +628,7 @@ def rate( self, curves: Curves_ = NoInput(0), solver: Solver_ = NoInput(0), - fx: FXForwards | NoInput = NoInput(0), + fx: FX_ = NoInput(0), leg: int = 1, ): """ @@ -1052,15 +1055,24 @@ def _parse_split_flag(self, fx_fixings, points, split_notional): "Cannot initialise FXSwap with `split_notional` but without `fx_fixings`", ) - def _set_split_notional(self, curve: Curve | NoInput = NoInput(0), at_init: bool = False): + def _set_split_notional(self, curve: Curve_ = NoInput(0), at_init: bool = False) -> None: """ - Will set the fixed rate, if not zero, for leg1, given provided split not or forecast splnot. + Will set the fixed rate, if not zero, for leg1, given the provided split notional or the + forecast split notional calculated from a curve. - self._split_notional is used as a temporary storage when mid market price is determined. + self._split_notional is used as a temporary storage when mid-market price is determined. + + Parameters + ---------- + curve: Curve, optional + A curve used to determine the split notional in the case calculation is needed + at_init: bool + A construction flag to indicate if this method is being called during initialisation. """ if not self._is_split: self._split_notional = self.kwargs["notional"] # fixed rate at zero remains + return None # a split notional is given by a user and then this is set and never updated. elif not isinstance(self.kwargs["split_notional"], NoInput): @@ -1073,8 +1085,13 @@ def _set_split_notional(self, curve: Curve | NoInput = NoInput(0), at_init: bool # else new pricing parameters will affect and unpriced split notional else: if at_init: - self._split_notional = None + self._split_notional = NoInput(0) else: + if isinstance(curve, NoInput): + raise ValueError( + "A `curve` is required to determine a `split_notional` on an FXSwap if " + "the `split_notional` is not provided at initialisation." + ) dt1, dt2 = self.leg1.periods[0].payment, self.leg1.periods[2].payment self._split_notional = self.kwargs["notional"] * curve[dt1] / curve[dt2] self._set_leg1_fixed_rate() diff --git a/python/tests/test_instruments.py b/python/tests/test_instruments.py index 677557cb..f8d49c36 100644 --- a/python/tests/test_instruments.py +++ b/python/tests/test_instruments.py @@ -3224,6 +3224,12 @@ def test_transition_from_dual_to_dual2_rate(self, curve, curve2) -> None: fxf._set_ad_order(2) fxs.rate(curves=[None, fxf.curve("usd", "usd"), None, fxf.curve("nok", "usd")], fx=fxf) + def test_split_notional_raises(self): + # this is an unpriced FXswap with split notional + fxs = FXSwap(effective=dt(2022, 2, 1), termination="3m", pair="eurusd") + with pytest.raises(ValueError, match="A `curve` is required to determine a `split_notion"): + fxs.rate() + class TestSTIRFuture: def test_stir_rate(self, curve, curve2) -> None: From e25f5974e7410e211de357f38928091beae926a5 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (win11)" Date: Mon, 13 Jan 2025 17:18:33 +0100 Subject: [PATCH 4/8] TYP: multi ccy derivs --- .../instruments/rates/multi_currency.py | 30 +++++++++---------- .../instruments/rates/single_currency.py | 14 ++++----- python/rateslib/legs.py | 10 +++---- python/rateslib/typing.py | 3 +- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/python/rateslib/instruments/rates/multi_currency.py b/python/rateslib/instruments/rates/multi_currency.py index 8af0c15d..1dcb904c 100644 --- a/python/rateslib/instruments/rates/multi_currency.py +++ b/python/rateslib/instruments/rates/multi_currency.py @@ -36,7 +36,7 @@ # Contact info at rateslib.com if this code is observed outside its intended sphere of use. if TYPE_CHECKING: - from rateslib.typing import FX_, NPV, Any, Curves_, DualTypes, DualTypes_, Solver_, str_ + from rateslib.typing import FX_, NPV, Any, Curves_, DualTypes, DualTypes_, Solver_, str_, FixingsRates_, FixingsFx_ class FXExchange(Sensitivities, BaseMixin): @@ -59,8 +59,8 @@ class FXExchange(Sensitivities, BaseMixin): The signature should be: `[None, eur_curve, None, usd_curve]` for a "eurusd" pair. """ - leg1: Cashflow - leg2: Cashflow + leg1: Cashflow # type: ignore[assignment] + leg2: Cashflow # type: ignore[assignment] def __init__( self, @@ -332,7 +332,7 @@ class XCS(BaseDerivative): """ leg1: FixedLeg | FloatLeg - leg2: FixedLeg | FloatLeg | FloatLegMtm | FixedLegMtm + leg2: FixedLeg | FloatLeg | FloatLegMtm | FixedLegMtm # type: ignore[assignment] def __init__( self, @@ -342,7 +342,7 @@ def __init__( fixed_rate: float | NoInput = NoInput(0), float_spread: float | NoInput = NoInput(0), spread_compound_method: str_ = NoInput(0), - fixings: float | list | Series | NoInput = NoInput(0), + fixings: FixingsRates_ = NoInput(0), # type: ignore[type-var] fixing_method: str_ = NoInput(0), method_param: int | NoInput = NoInput(0), leg2_fixed: bool | NoInput = NoInput(0), @@ -350,11 +350,11 @@ def __init__( leg2_payment_lag_exchange: int | NoInput = NoInput(1), leg2_fixed_rate: float | NoInput = NoInput(0), leg2_float_spread: float | NoInput = NoInput(0), - leg2_fixings: float | list | NoInput = NoInput(0), + leg2_fixings: FixingsRates_ = NoInput(0), leg2_fixing_method: str_ = NoInput(0), leg2_method_param: int | NoInput = NoInput(0), leg2_spread_compound_method: str_ = NoInput(0), - fx_fixings: list | DualTypes | FXRates | FXForwards | NoInput = NoInput(0), + fx_fixings: FixingsFx_ = NoInput(0), # type: ignore[type-var] **kwargs: Any, ) -> None: super().__init__(*args, **kwargs) @@ -364,7 +364,7 @@ def __init__( leg2_fixed=False if isinstance(leg2_fixed, NoInput) else leg2_fixed, leg2_mtm=True if isinstance(leg2_mtm, NoInput) else leg2_mtm, ) - self.kwargs = _update_not_noinput(self.kwargs, default_kwargs) + self.kwargs: dict[str, Any] = _update_not_noinput(self.kwargs, default_kwargs) if self.kwargs["fixed"]: self.kwargs.pop("spread_compound_method", None) @@ -372,8 +372,8 @@ def __init__( self.kwargs.pop("method_param", None) self._fixed_rate_mixin = True self._fixed_rate = fixed_rate - leg1_user_kwargs = dict(fixed_rate=fixed_rate) - Leg1 = FixedLeg + leg1_user_kwargs: dict[str, Any] = dict(fixed_rate=fixed_rate) + Leg1: type[FixedLeg] | type[FloatLeg] = FixedLeg else: self._rate_scalar = 100.0 self._float_spread_mixin = True @@ -402,8 +402,8 @@ def __init__( self.kwargs.pop("leg2_method_param", None) self._leg2_fixed_rate_mixin = True self._leg2_fixed_rate = leg2_fixed_rate - leg2_user_kwargs = dict(leg2_fixed_rate=leg2_fixed_rate) - Leg2 = FixedLeg if not leg2_mtm else FixedLegMtm + leg2_user_kwargs: dict[str, Any] = dict(leg2_fixed_rate=leg2_fixed_rate) + Leg2: type[FloatLeg] | type[FixedLeg] | type[FloatLegMtm] | type[FixedLegMtm] = FixedLeg if not leg2_mtm else FixedLegMtm else: self._leg2_float_spread_mixin = True self._leg2_float_spread = leg2_float_spread @@ -437,8 +437,8 @@ def __init__( self.kwargs = _update_not_noinput(self.kwargs, {**leg1_user_kwargs, **leg2_user_kwargs}) - self.leg1 = Leg1(**_get(self.kwargs, leg=1, filter=["fixed"])) - self.leg2 = Leg2(**_get(self.kwargs, leg=2, filter=["leg2_fixed", "leg2_mtm"])) + self.leg1 = Leg1(**_get(self.kwargs, leg=1, filter=("fixed"))) + self.leg2 = Leg2(**_get(self.kwargs, leg=2, filter=("leg2_fixed", "leg2_mtm"))) self._initialise_fx_fixings(fx_fixings) @property @@ -450,7 +450,7 @@ def fx_fixings(self, value): self._fx_fixings = value self._set_leg2_notional(value) - def _initialise_fx_fixings(self, fx_fixings): + def _initialise_fx_fixings(self, fx_fixings: FixingsFx_) -> None: """ Sets the `fx_fixing` for non-mtm XCS instruments, which require only a single value. diff --git a/python/rateslib/instruments/rates/single_currency.py b/python/rateslib/instruments/rates/single_currency.py index 5b5e859e..3206b416 100644 --- a/python/rateslib/instruments/rates/single_currency.py +++ b/python/rateslib/instruments/rates/single_currency.py @@ -39,7 +39,7 @@ Curves_, DualTypes, FixedPeriod, - FixingsRates, + FixingsRates_, FloatPeriod, Solver_, ) @@ -179,7 +179,7 @@ def __init__( fixed_rate: DualTypes | NoInput = NoInput(0), leg2_float_spread: DualTypes | NoInput = NoInput(0), leg2_spread_compound_method: str | NoInput = NoInput(0), - leg2_fixings: FixingsRates = NoInput(0), # type: ignore[type-var] + leg2_fixings: FixingsRates_ = NoInput(0), # type: ignore[type-var] leg2_fixing_method: str | NoInput = NoInput(0), leg2_method_param: int | NoInput = NoInput(0), **kwargs: Any, @@ -539,7 +539,7 @@ def __init__( nominal: float | NoInput = NoInput(0), leg2_float_spread: float | NoInput = NoInput(0), leg2_spread_compound_method: str | NoInput = NoInput(0), - leg2_fixings: FixingsRates = NoInput(0), + leg2_fixings: FixingsRates_ = NoInput(0), leg2_fixing_method: str | NoInput = NoInput(0), leg2_method_param: int | NoInput = NoInput(0), **kwargs: Any, @@ -882,7 +882,7 @@ def __init__( fixed_rate: float | NoInput = NoInput(0), leg2_float_spread: float | NoInput = NoInput(0), leg2_spread_compound_method: str | NoInput = NoInput(0), - leg2_fixings: FixingsRates = NoInput(0), + leg2_fixings: FixingsRates_ = NoInput(0), leg2_fixing_method: str | NoInput = NoInput(0), leg2_method_param: int | NoInput = NoInput(0), **kwargs: Any, @@ -1228,12 +1228,12 @@ def __init__( *args: Any, float_spread: float | NoInput = NoInput(0), spread_compound_method: str | NoInput = NoInput(0), - fixings: FixingsRates = NoInput(0), + fixings: FixingsRates_ = NoInput(0), fixing_method: str | NoInput = NoInput(0), method_param: int | NoInput = NoInput(0), leg2_float_spread: float | NoInput = NoInput(0), leg2_spread_compound_method: str | NoInput = NoInput(0), - leg2_fixings: FixingsRates = NoInput(0), + leg2_fixings: FixingsRates_ = NoInput(0), leg2_fixing_method: str | NoInput = NoInput(0), leg2_method_param: int | NoInput = NoInput(0), **kwargs: Any, @@ -1541,7 +1541,7 @@ def __init__( *args: Any, fixed_rate: DualTypes | NoInput = NoInput(0), method_param: int | NoInput = NoInput(0), - fixings: FixingsRates = NoInput(0), + fixings: FixingsRates_ = NoInput(0), **kwargs: Any, ) -> None: super().__init__(*args, **kwargs) diff --git a/python/rateslib/legs.py b/python/rateslib/legs.py index 417855e9..af671131 100644 --- a/python/rateslib/legs.py +++ b/python/rateslib/legs.py @@ -34,7 +34,7 @@ from rateslib.scheduling import Schedule if TYPE_CHECKING: - from rateslib.typing import FX_, CalInput, CurveOption_, DualTypes, FixingsRates, Period, FixingsFx_ + from rateslib.typing import FX_, CalInput, CurveOption_, DualTypes, FixingsRates_, Period, FixingsFx_ # Licence: Creative Commons - Attribution-NonCommercial-NoDerivatives 4.0 International # Commercial use of this code, and/or copying and redistribution is prohibited. @@ -583,7 +583,7 @@ def _get_fixings_from_series( def _set_fixings( self, - fixings: FixingsRates, # type: ignore[type-var] + fixings: FixingsRates_, # type: ignore[type-var] ) -> None: """ Re-organises the fixings input to list structure for each period. @@ -1006,7 +1006,7 @@ def __init__( self, *args: Any, float_spread: DualTypes | NoInput = NoInput(0), - fixings: FixingsRates = NoInput(0), + fixings: FixingsRates_ = NoInput(0), fixing_method: str | NoInput = NoInput(0), method_param: int | NoInput = NoInput(0), spread_compound_method: str | NoInput = NoInput(0), @@ -1307,7 +1307,7 @@ def __init__( self, *args: Any, float_spread: DualTypes | NoInput = NoInput(0), - fixings: FixingsRates = NoInput(0), + fixings: FixingsRates_ = NoInput(0), fixing_method: str | NoInput = NoInput(0), method_param: int | NoInput = NoInput(0), spread_compound_method: str | NoInput = NoInput(0), @@ -2869,7 +2869,7 @@ def __init__( self, *args: Any, float_spread: DualTypes | NoInput = NoInput(0), - fixings: FixingsRates = NoInput(0), + fixings: FixingsRates_ = NoInput(0), fixing_method: str | NoInput = NoInput(0), method_param: int | NoInput = NoInput(0), spread_compound_method: str | NoInput = NoInput(0), diff --git a/python/rateslib/typing.py b/python/rateslib/typing.py index 5b11481f..082a893d 100644 --- a/python/rateslib/typing.py +++ b/python/rateslib/typing.py @@ -86,7 +86,8 @@ Arr1dObj: TypeAlias = "np.ndarray[tuple[int], np.dtype[np.object_]]" Arr2dObj: TypeAlias = "np.ndarray[tuple[int, int], np.dtype[np.object_]]" -FixingsRates: TypeAlias = "Series[DualTypes] | list[DualTypes | list[DualTypes] | Series[DualTypes] | NoInput] | tuple[DualTypes, Series[DualTypes]] | DualTypes | NoInput" +FixingsRates: TypeAlias = "Series[DualTypes] | list[DualTypes | list[DualTypes] | Series[DualTypes] | NoInput] | tuple[DualTypes, Series[DualTypes]] | DualTypes" +FixingsRates_: TypeAlias = "FixingsRates | NoInput" FixingsFx: TypeAlias = "DualTypes | list[DualTypes] | Series[DualTypes] | tuple[DualTypes, Series[DualTypes]]" FixingsFx_: TypeAlias = "FixingsFx | NoInput" From 95e52d02269bf7af0eeb328e456d7fa368cc1fd3 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (M1)" Date: Mon, 13 Jan 2025 21:09:23 +0100 Subject: [PATCH 5/8] TYP/REF: separate directories --- python/rateslib/curves/_parsers.py | 4 +- .../instruments/rates/multi_currency.py | 48 ++++++++++++------- python/rateslib/legs.py | 10 +++- python/rateslib/typing.py | 4 +- 4 files changed, 45 insertions(+), 21 deletions(-) diff --git a/python/rateslib/curves/_parsers.py b/python/rateslib/curves/_parsers.py index 33a8ec1b..e6185b8a 100644 --- a/python/rateslib/curves/_parsers.py +++ b/python/rateslib/curves/_parsers.py @@ -212,7 +212,5 @@ def _validate_disc_curves_are_not_dict(curves_tuple: Curves_Tuple) -> Curves_Dis def _validate_curve_not_no_input(curve: Curve_) -> Curve: if isinstance(curve, NoInput): - raise ValueError( - "`curve` must be supplied. Got NoInput or None." - ) + raise ValueError("`curve` must be supplied. Got NoInput or None.") return curve diff --git a/python/rateslib/instruments/rates/multi_currency.py b/python/rateslib/instruments/rates/multi_currency.py index 1dcb904c..d9b8e099 100644 --- a/python/rateslib/instruments/rates/multi_currency.py +++ b/python/rateslib/instruments/rates/multi_currency.py @@ -4,7 +4,7 @@ from datetime import datetime from typing import TYPE_CHECKING -from pandas import DataFrame, DatetimeIndex, MultiIndex, Series +from pandas import DataFrame, DatetimeIndex, MultiIndex from rateslib import defaults from rateslib.curves._parsers import _validate_curve_not_no_input @@ -36,7 +36,20 @@ # Contact info at rateslib.com if this code is observed outside its intended sphere of use. if TYPE_CHECKING: - from rateslib.typing import FX_, NPV, Any, Curves_, DualTypes, DualTypes_, Solver_, str_, FixingsRates_, FixingsFx_ + from rateslib.typing import ( + FX, + FX_, + NPV, + Any, + Curve_, + Curves_, + DualTypes, + DualTypes_, + FixingsFx_, + FixingsRates_, + Solver_, + str_, + ) class FXExchange(Sensitivities, BaseMixin): @@ -160,7 +173,8 @@ def npv( if local: return { - k: leg1_npv.get(k, 0) + leg2_npv.get(k, 0) for k in set(leg1_npv) | set(leg2_npv) # type: ignore[union-attr, arg-type] + k: leg1_npv.get(k, 0) + leg2_npv.get(k, 0) # type: ignore[union-attr] + for k in set(leg1_npv) | set(leg2_npv) # type: ignore[arg-type] } else: return leg1_npv + leg2_npv # type: ignore[operator] @@ -403,7 +417,9 @@ def __init__( self._leg2_fixed_rate_mixin = True self._leg2_fixed_rate = leg2_fixed_rate leg2_user_kwargs: dict[str, Any] = dict(leg2_fixed_rate=leg2_fixed_rate) - Leg2: type[FloatLeg] | type[FixedLeg] | type[FloatLegMtm] | type[FixedLegMtm] = FixedLeg if not leg2_mtm else FixedLegMtm + Leg2: type[FloatLeg] | type[FixedLeg] | type[FloatLegMtm] | type[FixedLegMtm] = ( + FixedLeg if not leg2_mtm else FixedLegMtm + ) else: self._leg2_float_spread_mixin = True self._leg2_float_spread = leg2_float_spread @@ -437,16 +453,16 @@ def __init__( self.kwargs = _update_not_noinput(self.kwargs, {**leg1_user_kwargs, **leg2_user_kwargs}) - self.leg1 = Leg1(**_get(self.kwargs, leg=1, filter=("fixed"))) + self.leg1 = Leg1(**_get(self.kwargs, leg=1, filter=("fixed",))) self.leg2 = Leg2(**_get(self.kwargs, leg=2, filter=("leg2_fixed", "leg2_mtm"))) self._initialise_fx_fixings(fx_fixings) @property - def fx_fixings(self): + def fx_fixings(self) -> FixingsFx_: return self._fx_fixings @fx_fixings.setter - def fx_fixings(self, value): + def fx_fixings(self, value: FX_) -> None: self._fx_fixings = value self._set_leg2_notional(value) @@ -473,7 +489,7 @@ def _initialise_fx_fixings(self, fx_fixings: FixingsFx_) -> None: else: self._fx_fixings = fx_fixings - def _set_fx_fixings(self, fx): + def _set_fx_fixings(self, fx: FX_) -> None: """ Checks the `fx_fixings` and sets them according to given object if null. @@ -512,7 +528,7 @@ def _set_fx_fixings(self, fx): else: self._set_leg2_notional(fx) - def _set_leg2_notional(self, fx_arg: float | FXForwards): + def _set_leg2_notional(self, fx_arg: FX_) -> None: """ Update the notional on leg2 (foreign leg) if the initial fx rate is unfixed. @@ -538,7 +554,7 @@ def _set_leg2_notional(self, fx_arg: float | FXForwards): self.leg2.amortization = self.leg2_amortization @property - def _is_unpriced(self): + def _is_unpriced(self) -> bool: if getattr(self, "_unpriced", None) is True: return True if self._fixed_rate_mixin and self._leg2_fixed_rate_mixin: @@ -566,7 +582,7 @@ def _set_pricing_mid( curves: Curves_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards | NoInput = NoInput(0), - ): + ) -> None: leg: int = 1 lookup = { 1: ["_fixed_rate_mixin", "_float_spread_mixin"], @@ -592,7 +608,7 @@ def npv( fx: FXForwards | NoInput = NoInput(0), base: str_ = NoInput(0), local: bool = False, - ): + ) -> NPV: """ Return the NPV of the derivative by summing legs. @@ -630,7 +646,7 @@ def rate( solver: Solver_ = NoInput(0), fx: FX_ = NoInput(0), leg: int = 1, - ): + ) -> DualTypes: """ Return the mid-market pricing parameter of the XCS. @@ -752,7 +768,7 @@ def cashflows( solver: Solver_ = NoInput(0), fx: FXForwards | NoInput = NoInput(0), base: str_ = NoInput(0), - ): + ) -> DataFrame: curves_, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( self.curves, solver, @@ -778,11 +794,11 @@ def fixings_table( self, curves: Curves_ = NoInput(0), solver: Solver_ = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), + fx: FX_ = NoInput(0), base: str_ = NoInput(0), approximate: bool = False, right: datetime | NoInput = NoInput(0), - ): + ) -> DataFrame: """ Return a DataFrame of fixing exposures on any :class:`~rateslib.legs.FloatLeg` or :class:`~rateslib.legs.FloatLegMtm` associated with the *XCS*. diff --git a/python/rateslib/legs.py b/python/rateslib/legs.py index af671131..fc417b7c 100644 --- a/python/rateslib/legs.py +++ b/python/rateslib/legs.py @@ -34,7 +34,15 @@ from rateslib.scheduling import Schedule if TYPE_CHECKING: - from rateslib.typing import FX_, CalInput, CurveOption_, DualTypes, FixingsRates_, Period, FixingsFx_ + from rateslib.typing import ( + FX_, + CalInput, + CurveOption_, + DualTypes, + FixingsFx_, + FixingsRates_, + Period, + ) # Licence: Creative Commons - Attribution-NonCommercial-NoDerivatives 4.0 International # Commercial use of this code, and/or copying and redistribution is prohibited. diff --git a/python/rateslib/typing.py b/python/rateslib/typing.py index 082a893d..3499a186 100644 --- a/python/rateslib/typing.py +++ b/python/rateslib/typing.py @@ -89,7 +89,9 @@ FixingsRates: TypeAlias = "Series[DualTypes] | list[DualTypes | list[DualTypes] | Series[DualTypes] | NoInput] | tuple[DualTypes, Series[DualTypes]] | DualTypes" FixingsRates_: TypeAlias = "FixingsRates | NoInput" -FixingsFx: TypeAlias = "DualTypes | list[DualTypes] | Series[DualTypes] | tuple[DualTypes, Series[DualTypes]]" +FixingsFx: TypeAlias = ( + "DualTypes | list[DualTypes] | Series[DualTypes] | tuple[DualTypes, Series[DualTypes]]" +) FixingsFx_: TypeAlias = "FixingsFx | NoInput" str_: TypeAlias = "str | NoInput" From bbdbbcccdc10abd9d5f6a4bec1714f9e84712b0a Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (M1)" Date: Mon, 13 Jan 2025 21:58:10 +0100 Subject: [PATCH 6/8] TYP/REF: separate directories --- .../instruments/rates/multi_currency.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/python/rateslib/instruments/rates/multi_currency.py b/python/rateslib/instruments/rates/multi_currency.py index d9b8e099..dcb7ba60 100644 --- a/python/rateslib/instruments/rates/multi_currency.py +++ b/python/rateslib/instruments/rates/multi_currency.py @@ -457,15 +457,6 @@ def __init__( self.leg2 = Leg2(**_get(self.kwargs, leg=2, filter=("leg2_fixed", "leg2_mtm"))) self._initialise_fx_fixings(fx_fixings) - @property - def fx_fixings(self) -> FixingsFx_: - return self._fx_fixings - - @fx_fixings.setter - def fx_fixings(self, value: FX_) -> None: - self._fx_fixings = value - self._set_leg2_notional(value) - def _initialise_fx_fixings(self, fx_fixings: FixingsFx_) -> None: """ Sets the `fx_fixing` for non-mtm XCS instruments, which require only a single @@ -489,6 +480,15 @@ def _initialise_fx_fixings(self, fx_fixings: FixingsFx_) -> None: else: self._fx_fixings = fx_fixings + @property + def fx_fixings(self) -> FixingsFx_: + return self._fx_fixings + + @fx_fixings.setter + def fx_fixings(self, value: FX_) -> None: + self._fx_fixings = value + self._set_leg2_notional(value) + def _set_fx_fixings(self, fx: FX_) -> None: """ Checks the `fx_fixings` and sets them according to given object if null. From 750c00c6e514579a02da23821b052790ce51d4bf Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (M1)" Date: Mon, 13 Jan 2025 22:26:20 +0100 Subject: [PATCH 7/8] TYP/REF: separate directories --- .../rateslib/instruments/rates/multi_currency.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/python/rateslib/instruments/rates/multi_currency.py b/python/rateslib/instruments/rates/multi_currency.py index dcb7ba60..dccaaa8d 100644 --- a/python/rateslib/instruments/rates/multi_currency.py +++ b/python/rateslib/instruments/rates/multi_currency.py @@ -619,7 +619,7 @@ def npv( See :meth:`BaseDerivative.npv`. """ - curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( + curves_, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( self.curves, solver, curves, @@ -629,13 +629,13 @@ def npv( ) if self._is_unpriced: - self._set_pricing_mid(curves, solver, fx_) + self._set_pricing_mid(curves_, solver, fx_) self._set_fx_fixings(fx_) if self._is_mtm: self.leg2._do_not_repeat_set_periods = True - ret = super().npv(curves, solver, fx_, base_, local) + ret = super().npv(curves_, solver, fx_, base_, local) if self._is_mtm: self.leg2._do_not_repeat_set_periods = False # reset for next calculation return ret @@ -687,7 +687,7 @@ def rate( Examples -------- """ - curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( + curves_, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( self.curves, solver, curves, @@ -697,11 +697,11 @@ def rate( ) if leg == 1: - tgt_fore_curve, tgt_disc_curve = curves[0], curves[1] - alt_fore_curve, alt_disc_curve = curves[2], curves[3] + tgt_fore_curve, tgt_disc_curve = curves_[0], curves_[1] + alt_fore_curve, alt_disc_curve = curves_[2], curves_[3] else: - tgt_fore_curve, tgt_disc_curve = curves[2], curves[3] - alt_fore_curve, alt_disc_curve = curves[0], curves[1] + tgt_fore_curve, tgt_disc_curve = curves_[2], curves_[3] + alt_fore_curve, alt_disc_curve = curves_[0], curves_[1] leg2 = 1 if leg == 2 else 2 # tgt_str, alt_str = "" if leg == 1 else "leg2_", "" if leg2 == 1 else "leg2_" From 52be5d49f61e7e105689d75406ed57be6dad605f Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (M1)" Date: Mon, 13 Jan 2025 22:28:14 +0100 Subject: [PATCH 8/8] TYP/REF: separate directories --- python/rateslib/instruments/rates/multi_currency.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/rateslib/instruments/rates/multi_currency.py b/python/rateslib/instruments/rates/multi_currency.py index dccaaa8d..e3544b73 100644 --- a/python/rateslib/instruments/rates/multi_currency.py +++ b/python/rateslib/instruments/rates/multi_currency.py @@ -37,7 +37,6 @@ if TYPE_CHECKING: from rateslib.typing import ( - FX, FX_, NPV, Any,