diff --git a/pyproject.toml b/pyproject.toml index 87f69fe4..2386ef12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -137,7 +137,7 @@ ignore = [ [tool.ruff.lint.per-file-ignores] "__init__.py" = ["E402", "N801"] -"typing.py" = ["E501"] +"typing.py" = ["E501", "E402"] "python/tests/*" = ["F401", "B", "N", "S", "ANN", "D"] "rust/*" = ["D"] @@ -149,9 +149,7 @@ max-complexity = 14 files = ["python/"] exclude = [ "python/tests", - "/instruments/components/bonds", - "/instruments/fx_options", - # "/instruments/components/protocols", + # "/instruments/bonds/bond_future.py", ] strict = true #packages = [ @@ -163,12 +161,3 @@ omit = [ "/typing.py", # "python/tests/*" ] - -[tool.isort] -profile = "black" -line_length = 100 -src_paths = ["python"] - -[tool.black] -line-length = 100 -target-version = ['py310'] \ No newline at end of file diff --git a/python/rateslib/curves/curves.py b/python/rateslib/curves/curves.py index 57bdd401..dc684149 100644 --- a/python/rateslib/curves/curves.py +++ b/python/rateslib/curves/curves.py @@ -2982,7 +2982,7 @@ def _try_index_value( return _index_value_from_mixed_series_and_curve( index_lag=index_lag, index_method=index_method, - index_fixings=fixings_series[1], # type: ignore[arg-type] + index_fixings=fixings_series[1], index_date=index_date, index_curve=index_curve, ) diff --git a/python/rateslib/curves/rs.py b/python/rateslib/curves/rs.py index c7299089..7b6562a2 100644 --- a/python/rateslib/curves/rs.py +++ b/python/rateslib/curves/rs.py @@ -25,7 +25,12 @@ from rateslib.scheduling.convention import _get_convention if TYPE_CHECKING: - from rateslib.typing import CalInput, CurveInterpolator, DualTypes, Number + from rateslib.typing import ( # pragma: no cover + CalInput, + CurveInterpolator, + DualTypes, + Number, + ) class CurveRs: diff --git a/python/rateslib/instruments/bonds/bill.py b/python/rateslib/instruments/bonds/bill.py index cce94748..c9be4756 100644 --- a/python/rateslib/instruments/bonds/bill.py +++ b/python/rateslib/instruments/bonds/bill.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING from rateslib import defaults +from rateslib.curves._parsers import _validate_obj_not_no_input from rateslib.dual import Variable, gradient from rateslib.dual.utils import _dual_float, _to_number from rateslib.enums.generics import NoInput, _drb @@ -15,7 +16,7 @@ from rateslib.instruments.protocols.kwargs import _convert_to_schedule_kwargs, _KWArgs from rateslib.instruments.protocols.pricing import ( _Curves, - _maybe_get_curve_or_dict_maybe_from_solver, + _maybe_get_curve_maybe_from_solver, _Vol, ) from rateslib.legs import FixedLeg @@ -24,12 +25,12 @@ if TYPE_CHECKING: from rateslib.typing import ( # pragma: no cover CalInput, - CurveOption_, - Curves_, + CurvesT_, DualTypes, DualTypes_, FXForwards_, Number, + Sequence, Solver_, VolT_, _BaseLeg, @@ -200,7 +201,7 @@ def leg1(self) -> FixedLeg: return self._leg1 @property - def legs(self) -> list[_BaseLeg]: + def legs(self) -> Sequence[_BaseLeg]: """A list of the *Legs* of the *Instrument*.""" return self._legs @@ -217,7 +218,7 @@ def __init__( convention: str_ = NoInput(0), settle: int_ = NoInput(0), calc_mode: BillCalcMode | str_ = NoInput(0), - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), spec: str_ = NoInput(0), metric: str = "price", ): @@ -271,7 +272,7 @@ def __init__( def _parse_vol(self, vol: VolT_) -> _Vol: return _Vol() - def _parse_curves(self, curves: CurveOption_) -> _Curves: + def _parse_curves(self, curves: CurvesT_) -> _Curves: """ A Bill has one curve requirements: a disc_curve. @@ -298,15 +299,17 @@ def _parse_curves(self, curves: CurveOption_) -> _Curves: raise ValueError( f"{type(self).__name__} requires only 1 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( - disc_curve=curves, + disc_curve=curves, # type: ignore[arg-type] ) def rate( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), vol: VolT_ = NoInput(0), @@ -314,7 +317,7 @@ def rate( settlement: datetime_ = NoInput(0), forward: datetime_ = NoInput(0), metric: str_ = NoInput(0), - ) -> DualTypes_: + ) -> DualTypes: """ Return various pricing metrics of the security calculated from :class:`~rateslib.curves.Curve` s. @@ -345,17 +348,20 @@ def rate( ------- float, Dual, Dual2 """ - disc_curve_ = _maybe_get_curve_or_dict_maybe_from_solver( - solver=solver, - name="disc_curve", - curves=self._parse_curves(curves), - curves_meta=self.kwargs.meta["curves"], + disc_curve_ = _validate_obj_not_no_input( + _maybe_get_curve_maybe_from_solver( + solver=solver, + name="disc_curve", + curves=self._parse_curves(curves), + curves_meta=self.kwargs.meta["curves"], + ), + "disc_curve", ) settlement_ = self._maybe_get_settlement(settlement=settlement, disc_curve=disc_curve_) # scale price to par 100 and make a fwd adjustment according to curve price = ( - self.npv(curves=curves, solver=solver, fx=fx, base=base, local=False) + self.npv(curves=curves, solver=solver, local=False) # type: ignore[operator] * 100 / (-self.leg1.settlement_params.notional * disc_curve_[settlement_]) ) @@ -387,7 +393,7 @@ def simple_rate(self, price: DualTypes, settlement: datetime) -> DualTypes: """ acc_frac = self.kwargs.meta["calc_mode"]._settle_accrual(self, settlement, 0) dcf = (1 - acc_frac) * self.leg1._regular_periods[0].period_params.dcf - return ((100 / price - 1) / dcf) * 100 + return ((100 / price - 1) / dcf) * 100 # type: ignore[no-any-return] def discount_rate(self, price: DualTypes, settlement: datetime) -> DualTypes: """ @@ -407,7 +413,7 @@ def discount_rate(self, price: DualTypes, settlement: datetime) -> DualTypes: acc_frac = self.kwargs.meta["calc_mode"]._settle_accrual(self, settlement, 0) dcf = (1 - acc_frac) * self.leg1._regular_periods[0].period_params.dcf rate = ((1 - price / 100) / dcf) * 100 - return rate + return rate # type: ignore[no-any-return] def price( self, @@ -444,14 +450,14 @@ def price( def _price_discount(self, rate: DualTypes, settlement: datetime) -> DualTypes: acc_frac = self.kwargs.meta["calc_mode"]._settle_accrual(self, settlement, 0) dcf = (1 - acc_frac) * self.leg1._regular_periods[0].period_params.dcf - return 100 - rate * dcf + return 100 - rate * dcf # type: ignore[no-any-return] def _price_simple(self, rate: DualTypes, settlement: datetime) -> DualTypes: acc_frac = self.kwargs.meta["calc_mode"]._settle_accrual(self, settlement, 0) dcf = (1 - acc_frac) * self.leg1._regular_periods[0].period_params.dcf - return 100 / (1 + rate * dcf / 100) + return 100 / (1 + rate * dcf / 100) # type: ignore[no-any-return] - def ytm( + def ytm( # type: ignore[override] self, price: DualTypes, settlement: datetime, @@ -492,11 +498,11 @@ def ytm( while quasi_ustart > settlement: quasi_ustart = frequency.uprevious(quasi_ustart) - equiv_bond = FixedRateBond( + equiv_bond = FixedRateBond( # type: ignore[abstract] effective=quasi_ustart, termination=self.leg1.schedule.utermination, fixed_rate=0.0, - **calc_mode_._ytm_clone_kwargs, + **calc_mode_._ytm_clone_kwargs, # type: ignore[arg-type] ) return equiv_bond.ytm(price, settlement) @@ -527,9 +533,7 @@ def duration(self, ytm: DualTypes, settlement: datetime, metric: str = "risk") - # TODO: this is not AD safe: returns only float ytm_: float = _dual_float(ytm) if metric == "duration": - price_: Dual | Dual2 = _to_number( # type: ignore[assignment] - self.price(Variable(ytm_, ["y"]), settlement, dirty=True) - ) + price_ = _to_number(self.price(Variable(ytm_, ["y"]), settlement, dirty=True)) freq = _get_frequency( self.kwargs.meta["frequency"], self.leg1.schedule.utermination.day, diff --git a/python/rateslib/instruments/bonds/bond_future.py b/python/rateslib/instruments/bonds/bond_future.py index d97e9ada..477ac5ee 100644 --- a/python/rateslib/instruments/bonds/bond_future.py +++ b/python/rateslib/instruments/bonds/bond_future.py @@ -22,10 +22,8 @@ if TYPE_CHECKING: from rateslib.typing import ( Any, - Curves_, CurvesT_, DualTypes, - DualTypes_, FixedRateBond, FXForwards_, Solver_, @@ -240,7 +238,7 @@ def __init__( calc_mode=calc_mode, metric=metric, ) - instrument_args = dict() + instrument_args: dict[str, Any] = dict() # set defaults for missing values default_args = dict( calc_mode=defaults.calc_mode_futures, @@ -273,7 +271,7 @@ def __init__( elif isinstance(kw["delivery"], NoInput): raise ValueError("`delivery` must be a datetime or sequence of datetimes.") else: - kw["delivery"] = tuple(kw["delivery"]) # type: ignore[assignment, arg-type] + kw["delivery"] = tuple(kw["delivery"]) if isinstance(kw["coupon"], NoInput): raise ValueError("`coupon` must be value.") @@ -292,8 +290,8 @@ def notional(self) -> DualTypes: ------- float """ - nominal: DualTypes = self.kwargs.meta["nominal"] # type: ignore[assignment] - contracts: DualTypes = self.kwargs.meta["contracts"] # type: ignore[assignment] + nominal: DualTypes = self.kwargs.meta["nominal"] + contracts: DualTypes = self.kwargs.meta["contracts"] _: DualTypes = nominal * contracts * -1 return _ # long positions is negative notn @@ -369,23 +367,23 @@ def _cf_funcs(self) -> dict[str, ConversionFactorFunction]: } def _conversion_factors(self) -> tuple[DualTypes, ...]: - calc_mode: str = self.kwargs.meta["calc_mode"].lower() # type: ignore[union-attr] - basket: tuple[FixedRateBond, ...] = self.kwargs.meta["basket"] # type: ignore[assignment] + calc_mode: str = self.kwargs.meta["calc_mode"].lower() + basket: tuple[FixedRateBond, ...] = self.kwargs.meta["basket"] try: return tuple(self._cf_funcs[calc_mode](bond) for bond in basket) except KeyError: raise ValueError("`calc_mode` must be in {'ytm', 'ust_short', 'ust_long'}") def _cfs_ytm(self, bond: FixedRateBond) -> DualTypes: - coupon: DualTypes = self.kwargs.meta["coupon"] # type: ignore[assignment] - delivery: tuple[datetime, datetime] = self.kwargs.meta["delivery"] # type: ignore[assignment] + coupon: DualTypes = self.kwargs.meta["coupon"] + delivery: tuple[datetime, datetime] = self.kwargs.meta["delivery"] return bond.price(coupon, delivery[0]) / 100 def _cfs_ust(self, bond: FixedRateBond, short: bool) -> float: # TODO: This method is not AD safe: it uses "round" function which destroys derivatives # See CME pdf in doc Notes for formula. - coupon = _dual_float(bond.fixed_rate / 100.0) - delivery: datetime = self.kwargs.meta["delivery"][0] # type: ignore[assignment, index] + coupon = _dual_float(bond.fixed_rate / 100.0) # type: ignore[operator] # fixed rate is given + delivery: datetime = self.kwargs.meta["delivery"][0] n, z = _get_years_and_months(delivery, bond.leg1.schedule.termination) if not short: mapping = { @@ -429,7 +427,7 @@ def _cfs_ust_long(self, bond: FixedRateBond) -> float: def _cfs_eurex_eur(self, bond: FixedRateBond) -> float: # TODO: This method is not AD safe: it uses "round" function which destroys derivatives # See EUREX specs - dd: datetime = self.kwargs.meta["delivery"][1] # type: ignore[index, assignment, misc] + dd: datetime = self.kwargs.meta["delivery"][1] i = bond.leg1._period_index(dd) ncd = bond.leg1._regular_periods[i].period_params.end ncd1y = add_tenor(ncd, "-1y", "none") @@ -449,9 +447,9 @@ def _cfs_eurex_eur(self, bond: FixedRateBond) -> float: act2 = float((ncd1y - ncd2y).days) f = 1.0 + d_e / act1 - c = bond.fixed_rate + c: DualTypes = bond.fixed_rate # type: ignore[assignment] n = round((bond.leg1.schedule.termination - ncd).days / 365.25) - not_: DualTypes = self.kwargs.meta["coupon"] # type: ignore[assignment] + not_: DualTypes = self.kwargs.meta["coupon"] _ = 1.0 + not_ / 100 @@ -463,7 +461,7 @@ def _cfs_eurex_chf(self, bond: FixedRateBond) -> float: # TODO: This method is not AD safe: it uses "round" function which destroys derivatives # See EUREX specs - dd: datetime = self.kwargs.meta["delivery"][1] # type: ignore[index, assignment, misc] + dd: datetime = self.kwargs.meta["delivery"][1] mat = bond.leg1.schedule.termination # get full years and full months cal = Cal([], []) @@ -500,8 +498,8 @@ def _cfs_eurex_chf(self, bond: FixedRateBond) -> float: # n = n - 1 f = f / 12.0 - c = bond.fixed_rate - not_: DualTypes = self.kwargs.meta["coupon"] # type: ignore[assignment] + c: DualTypes = bond.fixed_rate # type: ignore[assignment] + not_: DualTypes = self.kwargs.meta["coupon"] v = 1.0 / (1.0 + not_ / 100.0) cf = v**f * (c / not_ * (1.0 + not_ / 100.0 - v**n) + v**n) - c * (1 - f) / 100.0 @@ -542,7 +540,7 @@ def dlv( ------- DataFrame """ - basket: tuple[FixedRateBond, ...] = self.kwargs.meta["basket"] # type: ignore[assignment] + basket: tuple[FixedRateBond, ...] = self.kwargs.meta["basket"] if not isinstance(repo_rate, tuple | list): r_ = (repo_rate,) * len(basket) else: @@ -622,10 +620,10 @@ def cms( ----- This method only operates when the CTD basket has multiple securities """ - basket: tuple[FixedRateBond, ...] = self.kwargs.meta["basket"] # type: ignore[assignment] + basket: tuple[FixedRateBond, ...] = self.kwargs.meta["basket"] if len(basket) == 1: raise ValueError("Multi-security analysis cannot be performed with one security.") - delivery = _drb(self.kwargs.meta["delivery"][1], delivery) # type: ignore[index, misc] + delivery = _drb(self.kwargs.meta["delivery"][1], delivery) # build a curve for pricing today = basket[0].leg1.schedule.calendar.lag_bus_days( @@ -704,7 +702,7 @@ def gross_basis( ------- tuple """ - basket: tuple[FixedRateBond, ...] = self.kwargs.meta["basket"] # type: ignore[assignment] + basket: tuple[FixedRateBond, ...] = self.kwargs.meta["basket"] if dirty: if isinstance(settlement, NoInput): raise ValueError("`settlement` must be specified if `dirty` is True.") @@ -751,8 +749,8 @@ def net_basis( ------- tuple """ - basket: tuple[FixedRateBond, ...] = self.kwargs.meta["basket"] # type: ignore[assignment] - f_settlement: datetime = _drb(self.kwargs.meta["delivery"][1], delivery) # type: ignore[index, misc] + basket: tuple[FixedRateBond, ...] = self.kwargs.meta["basket"] + f_settlement: datetime = _drb(self.kwargs.meta["delivery"][1], delivery) if not isinstance(repo_rate, Sequence): r_: Sequence[DualTypes] = (repo_rate,) * len(basket) @@ -821,8 +819,8 @@ def implied_repo( ------- tuple """ - basket: tuple[FixedRateBond, ...] = self.kwargs.meta["basket"] # type: ignore[assignment] - f_settlement: datetime = _drb(self.kwargs.meta["delivery"][1], delivery) # type: ignore[index, misc] + basket: tuple[FixedRateBond, ...] = self.kwargs.meta["basket"] + f_settlement: datetime = _drb(self.kwargs.meta["delivery"][1], delivery) implied_repos: tuple[DualTypes, ...] = tuple() for i, bond in enumerate(basket): @@ -859,8 +857,8 @@ def ytm( ------- tuple """ - basket: tuple[FixedRateBond, ...] = self.kwargs.meta["basket"] # type: ignore[assignment] - settlement: datetime = _drb(self.kwargs.meta["delivery"][1], delivery) # type: ignore[index, misc] + basket: tuple[FixedRateBond, ...] = self.kwargs.meta["basket"] + settlement: datetime = _drb(self.kwargs.meta["delivery"][1], delivery) adjusted_prices = [future_price * cf for cf in self.cfs] yields = tuple(bond.ytm(adjusted_prices[i], settlement) for i, bond in enumerate(basket)) return yields @@ -906,8 +904,8 @@ def duration( future.ytm(112.98) future.ytm(112.98 + risk[0] / 100) """ - basket: tuple[FixedRateBond, ...] = self.kwargs.meta["basket"] # type: ignore[assignment] - f_settlement: datetime = _drb(self.kwargs.meta["delivery"][1], delivery) # type: ignore[index, misc] + basket: tuple[FixedRateBond, ...] = self.kwargs.meta["basket"] + f_settlement: datetime = _drb(self.kwargs.meta["delivery"][1], delivery) _: tuple[float, ...] = () for i, bond in enumerate(basket): @@ -960,8 +958,8 @@ def convexity( future.duration(112.98 + risk[0] / 100) """ # TODO: Not AD safe becuase dependent convexity method is not AD safe. Returns float. - basket: tuple[FixedRateBond, ...] = self.kwargs.meta["basket"] # type: ignore[assignment] - f_settlement: datetime = _drb(self.kwargs.meta["delivery"][1], delivery) # type: ignore[index, misc] + basket: tuple[FixedRateBond, ...] = self.kwargs.meta["basket"] + f_settlement: datetime = _drb(self.kwargs.meta["delivery"][1], delivery) _: tuple[float, ...] = () for i, bond in enumerate(basket): @@ -1024,7 +1022,7 @@ def ctd_index( def rate( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), vol: VolT_ = NoInput(0), @@ -1032,7 +1030,7 @@ def rate( settlement: datetime_ = NoInput(0), forward: datetime_ = NoInput(0), metric: str_ = NoInput(0), - ) -> DualTypes_: + ) -> DualTypes: """ Return various pricing metrics of the security calculated from :class:`~rateslib.curves.Curve` s. @@ -1071,12 +1069,12 @@ def rate( This method determines the *'futures_price'* and *'ytm'* by assuming a net basis of zero and pricing from the cheapest to delivery (CTD). """ - basket: tuple[FixedRateBond, ...] = self.kwargs.meta["basket"] # type: ignore[assignment] + basket: tuple[FixedRateBond, ...] = self.kwargs.meta["basket"] metric_ = _drb(self.kwargs.meta["metric"], metric).lower() if metric_ not in ["future_price", "ytm"]: raise ValueError("`metric` must be in {'future_price', 'ytm'}.") - f_settlement = _drb(self.kwargs.meta["delivery"][1], settlement) # type: ignore[index, misc] + f_settlement = _drb(self.kwargs.meta["delivery"][1], settlement) prices_: list[DualTypes] = [ bond.rate( curves=curves, diff --git a/python/rateslib/instruments/bonds/conventions/__init__.py b/python/rateslib/instruments/bonds/conventions/__init__.py index 74a7a7f7..bb21399a 100644 --- a/python/rateslib/instruments/bonds/conventions/__init__.py +++ b/python/rateslib/instruments/bonds/conventions/__init__.py @@ -12,13 +12,13 @@ ) if TYPE_CHECKING: - from rateslib.instruments.bonds.conventions.accrued import AccrualFunction - from rateslib.instruments.bonds.conventions.discounting import ( + from rateslib.instruments.bonds.conventions.accrued import AccrualFunction # pragma: no cover + from rateslib.instruments.bonds.conventions.discounting import ( # pragma: no cover CashflowFunction, YtmDiscountFunction, YtmStubDiscountFunction, ) - from rateslib.typing import Security + from rateslib.typing import Any # pragma: no cover class BondCalcMode: @@ -724,7 +724,7 @@ def _get_bill_calc_mode(calc_mode: str | BillCalcMode) -> BillCalcMode: def _get_calc_mode_for_class( - obj: Security, calc_mode: str | BondCalcMode | BillCalcMode + obj: Any, calc_mode: str | BondCalcMode | BillCalcMode ) -> BondCalcMode | BillCalcMode: if isinstance(calc_mode, str): map_: dict[str, dict[str, BondCalcMode] | dict[str, BillCalcMode]] = { diff --git a/python/rateslib/instruments/bonds/conventions/accrued.py b/python/rateslib/instruments/bonds/conventions/accrued.py index 7d31ffff..40be9ec1 100644 --- a/python/rateslib/instruments/bonds/conventions/accrued.py +++ b/python/rateslib/instruments/bonds/conventions/accrued.py @@ -8,8 +8,6 @@ if TYPE_CHECKING: from rateslib.typing import ( # pragma: no cover Any, - BondMixin, - Security, _SupportsFixedFloatLeg1, ) @@ -24,7 +22,7 @@ class AccrualFunction(Protocol): # Callable type for Accrual Functions def __call__( - self, obj: Security | BondMixin, settlement: datetime, acc_idx: int, *args: Any + self, obj: _SupportsFixedFloatLeg1, settlement: datetime, acc_idx: int, *args: Any ) -> float: ... @@ -58,13 +56,13 @@ def _acc_linear_proportion_by_days_long_stub_split( """ # TODO: handle this union attribute by segregating Securities periods into different # categories, perhaps when also integrating deterministic amortised bonds. - if obj.leg1.periods[acc_idx].period_params.stub: # type: ignore[union-attr] + if obj.leg1._regular_periods[acc_idx].period_params.stub: f = obj.leg1.schedule.periods_per_annum freq = obj.leg1.schedule.frequency_obj adjuster = obj.leg1.schedule.accrual_adjuster calendar = obj.leg1.schedule.calendar - if obj.leg1.periods[acc_idx].period_params.dcf * f > 1: # type: ignore[union-attr] + if obj.leg1._regular_periods[acc_idx].period_params.dcf * f > 1: # long stub if acc_idx > 0: @@ -128,8 +126,8 @@ def _acc_linear_proportion_by_days_long_stub_split( r_bar_u = (quasi_acoupon - obj.leg1.schedule.aschedule[acc_idx]).days return (r_bar_u / s_bar_u + r_u / s_u) / ( - obj.leg1.periods[acc_idx].period_params.dcf * f - ) # type: ignore[union-attr] + obj.leg1._regular_periods[acc_idx].period_params.dcf * f + ) return _acc_linear_proportion_by_days(obj, settlement, acc_idx, *args) @@ -144,7 +142,7 @@ def _acc_30e360_backward( If stub revert to linear proportioning. """ - if obj.leg1.periods[acc_idx].period_params.stub: # type: ignore[union-attr] + if obj.leg1._regular_periods[acc_idx].period_params.stub: return _acc_linear_proportion_by_days(obj, settlement, acc_idx) f = obj.leg1.schedule.periods_per_annum _: float = ( @@ -194,7 +192,7 @@ def _acc_act365_with_1y_and_stub_adjustment( If the period is a stub reverts to a straight line interpolation [this is primarily designed for Canadian Government Bonds] """ - if obj.leg1._regular_periods[acc_idx].period_params.stub: # type: ignore[union-attr] + if obj.leg1._regular_periods[acc_idx].period_params.stub: return _acc_linear_proportion_by_days(obj, settlement, acc_idx) f = obj.leg1.schedule.periods_per_annum r = (settlement - obj.leg1.schedule.aschedule[acc_idx]).days diff --git a/python/rateslib/instruments/bonds/conventions/discounting.py b/python/rateslib/instruments/bonds/conventions/discounting.py index 62a13246..25dfdcec 100644 --- a/python/rateslib/instruments/bonds/conventions/discounting.py +++ b/python/rateslib/instruments/bonds/conventions/discounting.py @@ -148,14 +148,14 @@ def _v1_compounded( Method: compounds "v2" with exponent in terms of the accrual fraction of the period. """ acc_frac = accrual(obj, settlement, acc_idx) - if obj.leg1.periods[acc_idx].period_params.stub: # type: ignore[union-attr] + if obj.leg1.periods[acc_idx].period_params.stub: # type: ignore[attr-defined] # If it is a stub then the remaining fraction must be scaled by the relative size of the # stub period compared with a regular period. - fd0 = obj.leg1.periods[acc_idx].period_params.dcf * f * (1 - acc_frac) # type: ignore[union-attr] + fd0 = obj.leg1.periods[acc_idx].period_params.dcf * f * (1 - acc_frac) # type: ignore[attr-defined] else: # 1 minus acc_fra is the fraction of the period remaining until the next cashflow. fd0 = 1 - acc_frac - return v2**fd0 + return v2**fd0 # type: ignore[no-any-return] def _v1_simple( @@ -172,14 +172,14 @@ def _v1_simple( Use simple rates with a yield which matches the frequency of the coupon. """ acc_frac = accrual(obj, settlement, acc_idx) - if obj.leg1.periods[acc_idx].period_params.stub: # type: ignore[union-attr] + if obj.leg1.periods[acc_idx].period_params.stub: # type: ignore[attr-defined] # is a stub so must account for discounting in a different way. - fd0 = obj.leg1.periods[acc_idx].period_params.dcf * f * (1 - acc_frac) # type: ignore[union-attr] + fd0 = obj.leg1.periods[acc_idx].period_params.dcf * f * (1 - acc_frac) # type: ignore[attr-defined] else: fd0 = 1 - acc_frac v_ = 1 / (1 + fd0 * ytm / (100 * f)) - return v_ + return v_ # type: ignore[no-any-return] def _v1_simple_act365f( @@ -211,18 +211,18 @@ def _v1_simple_pay_adjust( period_idx: int, ) -> DualTypes: acc_frac = accrual(obj, settlement, acc_idx) - if obj.leg1.periods[acc_idx].period_params.stub: # type: ignore[union-attr] + if obj.leg1._regular_periods[acc_idx].period_params.stub: # is a stub so must account for discounting in a different way. fd0 = ( - obj.leg1.periods[acc_idx].period_params.dcf + obj.leg1.periods[acc_idx].period_params.dcf # type: ignore[attr-defined] * f * (1 - acc_frac + _pay_adj(obj, period_idx)) - ) # type: ignore[union-attr] + ) else: fd0 = 1 - acc_frac + _pay_adj(obj, period_idx) v_ = 1 / (1 + fd0 * ytm / (100 * f)) - return v_ + return v_ # type: ignore[no-any-return] def _v1_compounded_pay_adjust( @@ -236,18 +236,18 @@ def _v1_compounded_pay_adjust( period_idx: int, ) -> DualTypes: acc_frac = accrual(obj, settlement, acc_idx) - if obj.leg1.periods[acc_idx].period_params.stub: # type: ignore[union-attr] + if obj.leg1._regular_periods[acc_idx].period_params.stub: # If it is a stub then the remaining fraction must be scaled by the relative size of the # stub period compared with a regular period. fd0 = ( - obj.leg1.periods[acc_idx].period_params.dcf + obj.leg1.periods[acc_idx].period_params.dcf # type: ignore[attr-defined] * f * (1 - acc_frac + _pay_adj(obj, period_idx)) - ) # type: ignore[union-attr] + ) else: # 1 minus acc_fra is the fraction of the period remaining until the next cashflow. fd0 = 1 - acc_frac + _pay_adj(obj, period_idx) - return v2**fd0 + return v2**fd0 # type: ignore[no-any-return] def _v1_compounded_final_simple( @@ -331,7 +331,7 @@ def _v1_comp_stub_act365f( period_idx: int, ) -> DualTypes: """Compounds the yield. In a stub period the act365f DCF is used""" - if not obj.leg1.periods[acc_idx].period_params.stub: # type: ignore[union-attr] + if not obj.leg1.periods[acc_idx].period_params.stub: # type: ignore[attr-defined] return _v1_compounded(obj, ytm, f, settlement, acc_idx, v2, accrual, period_idx) else: fd0 = dcf(settlement, obj.leg1.schedule.aschedule[acc_idx + 1], "Act365F") @@ -355,19 +355,19 @@ def _v1_simple_long_stub( discount param ``v``. """ if ( - obj.leg1.periods[acc_idx].period_params.stub - and obj.leg1.periods[acc_idx].period_params.dcf * f > 1 - ): # type: ignore[union-attr] + obj.leg1._regular_periods[acc_idx].period_params.stub + and obj.leg1._regular_periods[acc_idx].period_params.dcf * f > 1 + ): # long stub acc_frac = accrual(obj, settlement, acc_idx) - fd0 = obj.leg1.periods[acc_idx].period_params.dcf * f * (1 - acc_frac) # type: ignore[union-attr] + fd0 = obj.leg1.periods[acc_idx].period_params.dcf * f * (1 - acc_frac) # type: ignore[attr-defined] if fd0 > 1.0: # then there is a whole quasi-coupon period until payment of next cashflow v_ = v2 * 1 / (1 + (fd0 - 1) * ytm / (100 * f)) else: # this is standard _v1_simple formula v_ = 1 / (1 + fd0 * ytm / (100 * f)) - return v_ + return v_ # type: ignore[no-any-return] else: return _v1_simple(obj, ytm, f, settlement, acc_idx, v2, accrual, period_idx) @@ -391,13 +391,13 @@ def _v3_compounded( Final period uses a compounding approach where the power is determined by the DCF of that period under the bond's specified convention. """ - if obj.leg1.periods[acc_idx].period_params.stub: # type: ignore[union-attr] + if obj.leg1.periods[acc_idx].period_params.stub: # type: ignore[attr-defined] # If it is a stub then the remaining fraction must be scaled by the relative size of the # stub period compared with a regular period. - fd0 = obj.leg1.periods[acc_idx].period_params.dcf * f # type: ignore[union-attr] + fd0 = obj.leg1.periods[acc_idx].period_params.dcf * f # type: ignore[attr-defined] else: fd0 = 1 - return v2**fd0 + return v2**fd0 # type: ignore[no-any-return] def _v3_compounded_pay_adjust( @@ -434,10 +434,10 @@ def _v3_30e360_u_simple( The YTM is assumed to have the same frequency as the coupons. """ d_ = dcf( - obj.leg1.periods[acc_idx].period_params.start, - obj.leg1.periods[acc_idx].period_params.end, + obj.leg1._regular_periods[acc_idx].period_params.start, + obj.leg1._regular_periods[acc_idx].period_params.end, "30E360", - ) # type: ignore[union-attr] + ) return 1 / (1 + d_ * ytm / 100) # simple interest @@ -451,14 +451,14 @@ def _v3_simple( accrual: AccrualFunction, period_idx: int, ) -> DualTypes: - if obj.leg1.periods[acc_idx].period_params.stub: # type: ignore[union-attr] + if obj.leg1.periods[acc_idx].period_params.stub: # type: ignore[attr-defined] # is a stub so must account for discounting in a different way. - fd0 = obj.leg1.periods[acc_idx].period_params.dcf * f # type: ignore[union-attr] + fd0 = obj.leg1.periods[acc_idx].period_params.dcf * f # type: ignore[attr-defined] else: fd0 = 1.0 v_ = 1 / (1 + fd0 * ytm / (100 * f)) - return v_ + return v_ # type: ignore[no-any-return] def _v3_simple_pay_adjust( @@ -471,14 +471,14 @@ def _v3_simple_pay_adjust( accrual: AccrualFunction, period_idx: int, ) -> DualTypes: - if obj.leg1.periods[acc_idx].period_params.stub: # type: ignore[union-attr] + if obj.leg1.periods[acc_idx].period_params.stub: # type: ignore[attr-defined] # is a stub so must account for discounting in a different way. - fd0 = (1.0 + _pay_adj(obj, period_idx)) * obj.leg1.periods[acc_idx].period_params.dcf * f # type: ignore[union-attr] + fd0 = (1.0 + _pay_adj(obj, period_idx)) * obj.leg1.periods[acc_idx].period_params.dcf * f # type: ignore[attr-defined] else: fd0 = 1.0 + _pay_adj(obj, period_idx) v_ = 1 / (1 + fd0 * ytm / (100 * f)) - return v_ + return v_ # type: ignore[no-any-return] V1_FUNCS: dict[str, YtmStubDiscountFunction] = { @@ -522,7 +522,7 @@ def _c_from_obj( Return the cashflow as it has been calculated directly on the object according to the native schedule and conventions, for the specified period index. """ - return obj._period_cashflow(obj.leg1._regular_periods[p_idx], curve) # type: ignore[arg-type] + return obj._period_cashflow(obj.leg1._regular_periods[p_idx], curve) # type: ignore[no-any-return, attr-defined] def _c_full_coupon( @@ -538,7 +538,7 @@ def _c_full_coupon( Ignore the native schedule and conventions and return an amount based on the period notional, the bond coupon, and the bond frequency. """ - return -obj.leg1._regular_periods[p_idx].settlement_params.notional * obj.fixed_rate / (100 * f) # type: ignore[operator, union-attr] + return -obj.leg1._regular_periods[p_idx].settlement_params.notional * obj.fixed_rate / (100 * f) # type: ignore[attr-defined, no-any-return] C_FUNCS: dict[str, CashflowFunction] = { diff --git a/python/rateslib/instruments/bonds/fixed_rate_bond.py b/python/rateslib/instruments/bonds/fixed_rate_bond.py index 7c7d9d3b..e652e553 100644 --- a/python/rateslib/instruments/bonds/fixed_rate_bond.py +++ b/python/rateslib/instruments/bonds/fixed_rate_bond.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING from rateslib import defaults +from rateslib.curves._parsers import _validate_obj_not_no_input from rateslib.enums.generics import NoInput, _drb from rateslib.instruments.bonds.conventions import ( BondCalcMode, @@ -12,7 +13,7 @@ from rateslib.instruments.protocols.kwargs import _convert_to_schedule_kwargs, _KWArgs from rateslib.instruments.protocols.pricing import ( _Curves, - _maybe_get_curve_or_dict_maybe_from_solver, + _maybe_get_curve_maybe_from_solver, _Vol, ) from rateslib.legs import FixedLeg @@ -20,13 +21,13 @@ if TYPE_CHECKING: from rateslib.typing import ( # pragma: no cover CalInput, - CurveOption_, - Curves_, + CurvesT_, DualTypes, DualTypes_, Frequency, FXForwards_, RollDay, + Sequence, Solver_, VolT_, _BaseLeg, @@ -163,7 +164,7 @@ def leg1(self) -> FixedLeg: return self._leg1 @property - def legs(self) -> list[_BaseLeg]: + def legs(self) -> Sequence[_BaseLeg]: """A list of the *Legs* of the *Instrument*.""" return self._legs @@ -188,11 +189,11 @@ def __init__( # settlement parameters currency: str_ = NoInput(0), notional: float_ = NoInput(0), - amortization: float_ = NoInput(0), + # amortization: float_ = NoInput(0), # rate parameters fixed_rate: DualTypes_ = NoInput(0), # meta parameters - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), calc_mode: BondCalcMode | str_ = NoInput(0), settle: int_ = NoInput(0), spec: str_ = NoInput(0), @@ -217,7 +218,7 @@ def __init__( # settlement currency=currency, notional=notional, - amortization=amortization, + # amortization=amortization, # rate fixed_rate=fixed_rate, # meta @@ -257,7 +258,7 @@ def __init__( def _parse_vol(self, vol: VolT_) -> _Vol: return _Vol() - def _parse_curves(self, curves: CurveOption_) -> _Curves: + def _parse_curves(self, curves: CurvesT_) -> _Curves: """ An FRB has one curve requirements: a disc_curve. @@ -284,15 +285,17 @@ def _parse_curves(self, curves: CurveOption_) -> _Curves: raise ValueError( f"{type(self).__name__} requires only 1 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( - disc_curve=curves, + disc_curve=curves, # type: ignore[arg-type] ) def rate( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), vol: VolT_ = NoInput(0), @@ -302,28 +305,31 @@ def rate( metric: str_ = NoInput(0), ) -> DualTypes: metric_ = _drb(self.kwargs.meta["metric"], metric).lower() - if metric_ in ["clean_price", "dirty_price", "ytm"]: - _curves = self._parse_curves(curves) - disc_curve = _maybe_get_curve_or_dict_maybe_from_solver( + + _curves = self._parse_curves(curves) + disc_curve = _validate_obj_not_no_input( + _maybe_get_curve_maybe_from_solver( curves_meta=self.kwargs.meta["curves"], curves=_curves, name="disc_curve", solver=solver, - ) - settlement_ = self._maybe_get_settlement(settlement=settlement, disc_curve=disc_curve) - npv = self.leg1.local_npv( - disc_curve=disc_curve, - settlement=settlement_, - forward=settlement_, - ) - # scale price to par 100 (npv is already projected forward to settlement) - dirty_price = npv * 100 / -self.leg1.settlement_params.notional - - if metric_ == "dirty_price": - return dirty_price - elif metric_ == "clean_price": - return dirty_price - self.accrued(settlement_) - elif metric_ == "ytm": - return self.ytm(dirty_price, settlement_, True) + ), + "disc_curve", + ) + settlement_ = self._maybe_get_settlement(settlement=settlement, disc_curve=disc_curve) + npv = self.leg1.local_npv( + disc_curve=disc_curve, + settlement=settlement_, + forward=settlement_, + ) + # scale price to par 100 (npv is already projected forward to settlement) + dirty_price = npv * 100 / -self.leg1.settlement_params.notional + + if metric_ == "dirty_price": + return dirty_price + elif metric_ == "clean_price": + return dirty_price - self.accrued(settlement_) + elif metric_ == "ytm": + return self.ytm(dirty_price, settlement_, True) else: raise ValueError("`metric` must be in {'dirty_price', 'clean_price', 'ytm'}.") diff --git a/python/rateslib/instruments/bonds/float_rate_note.py b/python/rateslib/instruments/bonds/float_rate_note.py index 4498b45c..755c6f87 100644 --- a/python/rateslib/instruments/bonds/float_rate_note.py +++ b/python/rateslib/instruments/bonds/float_rate_note.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING from rateslib import defaults +from rateslib.curves._parsers import _validate_obj_not_no_input from rateslib.enums.generics import NoInput, _drb from rateslib.enums.parameters import FloatFixingMethod from rateslib.instruments.bonds.conventions import ( @@ -13,6 +14,7 @@ from rateslib.instruments.protocols.kwargs import _convert_to_schedule_kwargs, _KWArgs from rateslib.instruments.protocols.pricing import ( _Curves, + _maybe_get_curve_maybe_from_solver, _maybe_get_curve_or_dict_maybe_from_solver, _Vol, ) @@ -24,10 +26,13 @@ from rateslib.typing import ( # pragma: no cover CalInput, CurveOption_, - Curves_, + CurvesT_, DualTypes, DualTypes_, + FloatRateSeries, FXForwards_, + LegFixings, + Sequence, Solver_, VolT_, _BaseLeg, @@ -158,7 +163,7 @@ def leg1(self) -> FloatLeg: return self._leg1 @property - def legs(self) -> list[_BaseLeg]: + def legs(self) -> Sequence[_BaseLeg]: """A list of the *Legs* of the *Instrument*.""" return self._legs @@ -186,13 +191,13 @@ def __init__( # rate params float_spread: DualTypes_ = NoInput(0), spread_compound_method: str_ = NoInput(0), - rate_fixings: FixingsRates_ = NoInput(0), # type: ignore[type-var] + rate_fixings: LegFixings = NoInput(0), # type: ignore[type-var] fixing_method: str_ = NoInput(0), method_param: int_ = NoInput(0), fixing_frequency: Frequency | str_ = NoInput(0), fixing_series: FloatRateSeries | str_ = NoInput(0), # meta parameters - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), calc_mode: BondCalcMode | str_ = NoInput(0), settle: int_ = NoInput(0), spec: str_ = NoInput(0), @@ -262,7 +267,7 @@ def __init__( def _parse_vol(self, vol: VolT_) -> _Vol: return _Vol() - def _parse_curves(self, curves: CurveOption_) -> _Curves: + def _parse_curves(self, curves: CurvesT_) -> _Curves: """ An FRN has two curve requirements: a leg2_rate_curve and a disc_curve used by both legs. @@ -292,16 +297,18 @@ 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( - rate_curve=curves, - disc_curve=curves, + rate_curve=curves, # type: ignore[arg-type] + disc_curve=curves, # type: ignore[arg-type] ) def rate( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), vol: VolT_ = NoInput(0), @@ -313,11 +320,14 @@ def rate( metric = _drb(self.kwargs.meta["metric"], metric).lower() _curves = self._parse_curves(curves) if metric in ["clean_price", "dirty_price", "spread", "ytm"]: - disc_curve = _maybe_get_curve_or_dict_maybe_from_solver( - solver=solver, - curves=_curves, - curves_meta=self.kwargs.meta["curves"], - name="disc_curve", + disc_curve = _validate_obj_not_no_input( + _maybe_get_curve_maybe_from_solver( + solver=solver, + curves=_curves, + curves_meta=self.kwargs.meta["curves"], + name="disc_curve", + ), + "disc_curve", ) rate_curve = _maybe_get_curve_or_dict_maybe_from_solver( solver=solver, @@ -463,9 +473,9 @@ def accrued( -self.leg1._regular_periods[acc_idx].settlement_params.notional * self.leg1._regular_periods[acc_idx].period_params.dcf * rate - / 100 + / 100.0 ) - return frac * cashflow / -self.leg1.settlement_params.notional * 100 + return frac * cashflow / -self.leg1.settlement_params.notional * 100.0 # type: ignore[no-any-return] else: # is "rfr" p = FloatPeriod( @@ -499,11 +509,16 @@ def accrued( return 0.0 rate_to_settle = p.rate(rate_curve) - accrued_to_settle = 100 * p.period_params.dcf * rate_to_settle / 100 + accrued_to_settle = 100.0 * p.period_params.dcf * rate_to_settle / 100.0 if is_ex_div: rate_to_end = self.leg1._regular_periods[acc_idx].rate(rate_curve=rate_curve) - accrued_to_end = 100 * self.leg1._regular_periods[acc_idx].dcf * rate_to_end / 100 + accrued_to_end = ( + 100.0 + * self.leg1._regular_periods[acc_idx].period_params.dcf + * rate_to_end + / 100.0 + ) return accrued_to_settle - accrued_to_end else: return accrued_to_settle diff --git a/python/rateslib/instruments/bonds/index_fixed_rate_bond.py b/python/rateslib/instruments/bonds/index_fixed_rate_bond.py index fe75325f..fd052ffd 100644 --- a/python/rateslib/instruments/bonds/index_fixed_rate_bond.py +++ b/python/rateslib/instruments/bonds/index_fixed_rate_bond.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING from rateslib import defaults +from rateslib.curves._parsers import _validate_obj_not_no_input from rateslib.enums.generics import NoInput, _drb from rateslib.instruments.bonds.conventions import ( BondCalcMode, @@ -12,7 +13,7 @@ from rateslib.instruments.protocols.kwargs import _convert_to_schedule_kwargs, _KWArgs from rateslib.instruments.protocols.pricing import ( _Curves, - _maybe_get_curve_or_dict_maybe_from_solver, + _maybe_get_curve_maybe_from_solver, _Vol, ) from rateslib.legs import FixedLeg @@ -21,15 +22,15 @@ if TYPE_CHECKING: from rateslib.typing import ( # pragma: no cover CalInput, - CurveOption_, - Curves_, + CurvesT_, DualTypes, DualTypes_, Frequency, FXForwards_, IndexMethod, + LegFixings, RollDay, - Series, + Sequence, Solver_, VolT_, _BaseCurve_, @@ -167,7 +168,7 @@ def leg1(self) -> FixedLeg: return self._leg1 @property - def legs(self) -> list[_BaseLeg]: + def legs(self) -> Sequence[_BaseLeg]: """A list of the *Legs* of the *Instrument*.""" return self._legs @@ -192,16 +193,16 @@ def __init__( # settlement parameters currency: str_ = NoInput(0), notional: float_ = NoInput(0), - amortization: float_ = NoInput(0), + # amortization: float_ = NoInput(0), # 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), + index_fixings: LegFixings = NoInput(0), # type: ignore[type-var] # rate parameters fixed_rate: DualTypes_ = NoInput(0), # meta parameters - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), calc_mode: BondCalcMode | str_ = NoInput(0), settle: int_ = NoInput(0), spec: str_ = NoInput(0), @@ -226,7 +227,7 @@ def __init__( # settlement currency=currency, notional=notional, - amortization=amortization, + # amortization=amortization, # index_params index_base=index_base, index_lag=index_lag, @@ -275,13 +276,11 @@ def __init__( def _parse_vol(self, vol: VolT_) -> _Vol: return _Vol() - def _parse_curves(self, curves: CurveOption_) -> _Curves: + def _parse_curves(self, curves: CurvesT_) -> _Curves: """ An IFRB has two curve requirements: an index_curve and a disc_curve. - 1 element will be assigned as the discount curve only. fixings might be published. - - When given as 2 elements the first is treated as the index curve and the 2nd as disc curve. + No available index curve can be input as None or NoInput """ if isinstance(curves, NoInput): return _Curves() @@ -291,25 +290,19 @@ def _parse_curves(self, curves: CurveOption_) -> _Curves: index_curve=curves.get("index_curve", NoInput(0)), ) elif isinstance(curves, list | tuple): - if len(curves) == 1: - return _Curves( - disc_curve=curves[0], - index_curve=NoInput(0), - ) - elif len(curves) == 2: + if len(curves) == 2: return _Curves( - index_curve=curves[0], + index_curve=curves[0] if curves[0] is not None else NoInput(0), disc_curve=curves[1], ) else: raise ValueError( f"{type(self).__name__} requires 2 curve types. Got {len(curves)}." ) - else: # `curves` is just a single input which is set as the discount curve. - return _Curves( - disc_curve=curves, - index_curve=NoInput(0), - ) + elif isinstance(curves, _Curves): + return curves + else: + raise ValueError(f"{type(self).__name__} requires 2 curve types. Got 1.") def index_ratio(self, settlement: datetime, index_curve: _BaseCurve_ = NoInput(0)) -> DualTypes: """ @@ -355,7 +348,7 @@ def index_ratio(self, settlement: datetime, index_curve: _BaseCurve_ = NoInput(0 """ # noqa: E501 left_index = self.leg1._period_index(settlement) - period_index_params = self.leg1._regular_periods[left_index].index_params + period_index_params: _IndexParams = self.leg1._regular_periods[left_index].index_params # type: ignore[assignment] new_index_params = _IndexParams( _index_method=period_index_params.index_method, @@ -371,7 +364,7 @@ def index_ratio(self, settlement: datetime, index_curve: _BaseCurve_ = NoInput(0 def rate( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), vol: VolT_ = NoInput(0), @@ -379,92 +372,54 @@ def rate( settlement: datetime_ = NoInput(0), forward: datetime_ = NoInput(0), metric: str_ = NoInput(0), - ) -> DualTypes_: - """ - Return various pricing metrics of the security calculated from - :class:`~rateslib.curves.Curve` s. - - Parameters - ---------- - curves : Curve, str or list of such - A single :class:`Curve` or id or a list of such. A list defines the - following curves in the order: - - - Forecasting :class:`Curve` for ``leg1``. - - Discounting :class:`Curve` for ``leg1``. - solver : Solver, optional - The numerical :class:`Solver` that constructs ``Curves`` from calibrating - instruments. - fx : float, FXRates, FXForwards, optional - The immediate settlement FX rate that will be used to convert values - into another currency. A given `float` is used directly. If giving a - ``FXRates`` or ``FXForwards`` object, converts from local currency - into ``base``. - base : str, optional - The base currency to convert cashflows into (3-digit code), set by default. - Only used if ``fx`` is an ``FXRates`` or ``FXForwards`` object. - metric : str, optional - Metric returned by the method. Available options are {"clean_price", - "dirty_price", "ytm", "index_clean_price", "index_dirty_price"} - forward_settlement : datetime, optional - The forward settlement date. If not given uses the discount *Curve* and the ``settle`` - attribute of the bond. - - Returns - ------- - float, Dual, Dual2 - """ + ) -> DualTypes: metric_ = _drb(self.kwargs.meta["metric"], metric).lower() - if metric_ in [ - "clean_price", - "dirty_price", - "index_clean_price", - "ytm", - "index_dirty_price", - ]: - _curves = self._parse_curves(curves) - disc_curve = _maybe_get_curve_or_dict_maybe_from_solver( + _curves = self._parse_curves(curves) + disc_curve = _validate_obj_not_no_input( + _maybe_get_curve_maybe_from_solver( curves_meta=self.kwargs.meta["curves"], curves=_curves, name="disc_curve", solver=solver, - ) - index_curve = _maybe_get_curve_or_dict_maybe_from_solver( - curves_meta=self.kwargs.meta["curves"], - curves=_curves, - name="index_curve", - solver=solver, - ) + ), + "disc_curve", + ) + index_curve = _maybe_get_curve_maybe_from_solver( + curves_meta=self.kwargs.meta["curves"], + curves=_curves, + name="index_curve", + solver=solver, + ) - if isinstance(settlement, NoInput): - settlement_ = self.leg1.schedule.calendar.lag_bus_days( - disc_curve.nodes.initial, - self.kwargs.meta["settle"], - True, - ) - else: - settlement_ = settlement - npv = self.leg1.local_npv( - index_curve=index_curve, - disc_curve=disc_curve, - settlement=settlement_, - forward=settlement_, + if isinstance(settlement, NoInput): + settlement_ = self.leg1.schedule.calendar.lag_bus_days( + disc_curve.nodes.initial, + self.kwargs.meta["settle"], + True, ) - # scale price to par 100 (npv is already projected forward to settlement) - index_dirty_price = npv * 100 / -self.leg1.settlement_params.notional - index_ratio = self.index_ratio(settlement_, index_curve) - dirty_price = index_dirty_price / index_ratio - - if metric_ == "dirty_price": - return dirty_price - elif metric_ == "clean_price": - return dirty_price - self.accrued(settlement_) - elif metric_ == "ytm": - return self.ytm(dirty_price, settlement_, True) - elif metric_ == "index_dirty_price": - return index_dirty_price - elif metric_ == "index_clean_price": - return index_dirty_price - self.accrued(settlement_) * index_ratio + else: + settlement_ = settlement + npv = self.leg1.local_npv( + index_curve=index_curve, + disc_curve=disc_curve, + settlement=settlement_, + forward=settlement_, + ) + # scale price to par 100 (npv is already projected forward to settlement) + index_dirty_price = npv * 100 / -self.leg1.settlement_params.notional + index_ratio = self.index_ratio(settlement_, index_curve) + dirty_price = index_dirty_price / index_ratio + + if metric_ == "dirty_price": + return dirty_price + elif metric_ == "clean_price": + return dirty_price - self.accrued(settlement_) + elif metric_ == "ytm": + return self.ytm(dirty_price, settlement_, True) + elif metric_ == "index_dirty_price": + return index_dirty_price + elif metric_ == "index_clean_price": + return index_dirty_price - self.accrued(settlement_) * index_ratio else: raise ValueError( "`metric` must be in {'dirty_price', 'clean_price', 'ytm', " @@ -751,6 +706,6 @@ def duration( metric=metric, ) if metric == "risk" and indexed: - return value * self.index_ratio(settlement=settlement, index_curve=index_curve) + return value * self.index_ratio(settlement=settlement, index_curve=index_curve) # type: ignore[return-value] else: return value diff --git a/python/rateslib/instruments/bonds/protocols/__init__.py b/python/rateslib/instruments/bonds/protocols/__init__.py index 21ba8eaa..eafdc2ea 100644 --- a/python/rateslib/instruments/bonds/protocols/__init__.py +++ b/python/rateslib/instruments/bonds/protocols/__init__.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING +from rateslib.curves._parsers import _validate_obj_not_no_input from rateslib.enums.generics import NoInput, _drb from rateslib.instruments.bonds.protocols.accrued import _WithAccrued from rateslib.instruments.bonds.protocols.cashflows import _WithExDiv @@ -11,19 +12,18 @@ from rateslib.instruments.bonds.protocols.ytm import _WithYTM from rateslib.instruments.protocols import _BaseInstrument from rateslib.instruments.protocols.pricing import ( - _maybe_get_curve_or_dict_maybe_from_solver, + _maybe_get_curve_maybe_from_solver, ) if TYPE_CHECKING: from rateslib.typing import ( # pragma: no cover - Curves_, + CurvesT_, DataFrame, DualTypes, FXForwards_, - FXVolOption_, Solver_, VolT_, - _BaseCurve_, + _BaseCurve, datetime, datetime_, str_, @@ -41,7 +41,7 @@ class _BaseBondInstrument( def npv( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), vol: VolT_ = NoInput(0), @@ -50,11 +50,17 @@ def npv( settlement: datetime_ = NoInput(0), forward: datetime_ = NoInput(0), ) -> DualTypes | dict[str, DualTypes]: - _curves = self._parse_curves(curves) - disc_curve = _maybe_get_curve_or_dict_maybe_from_solver( - curves_meta=self.kwargs.meta["curves"], curves=_curves, name="disc_curve", solver=solver - ) if isinstance(settlement, NoInput): + _curves = self._parse_curves(curves) + disc_curve = _validate_obj_not_no_input( + _maybe_get_curve_maybe_from_solver( + curves_meta=self.kwargs.meta["curves"], + curves=_curves, + name="disc_curve", + solver=solver, + ), + "disc_curve", + ) settlement_ = self.leg1.schedule.calendar.lag_bus_days( disc_curve.nodes.initial, self.kwargs.meta["settle"], @@ -79,7 +85,7 @@ def npv( def cashflows( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), vol: VolT_ = NoInput(0), @@ -100,7 +106,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), vol: VolT_ = NoInput(0), @@ -119,7 +125,7 @@ def local_analytic_rate_fixings( def analytic_delta( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), vol: VolT_ = NoInput(0), @@ -131,11 +137,14 @@ def analytic_delta( ) -> DualTypes | dict[str, DualTypes]: settlement_ = self._maybe_get_settlement( settlement=settlement, - disc_curve=_maybe_get_curve_or_dict_maybe_from_solver( - curves_meta=self.kwargs.meta["curves"], - curves=self._parse_curves(curves), - name="disc_curve", - solver=solver, + disc_curve=_validate_obj_not_no_input( + _maybe_get_curve_maybe_from_solver( + curves_meta=self.kwargs.meta["curves"], + curves=self._parse_curves(curves), + name="disc_curve", + solver=solver, + ), + "disc_curve", ), ) @@ -230,7 +239,7 @@ def price(self, ytm: DualTypes, settlement: datetime, dirty: bool = False) -> Du def _maybe_get_settlement( self, settlement: datetime_, - disc_curve: _BaseCurve_, + disc_curve: _BaseCurve, ) -> datetime: if isinstance(settlement, NoInput): return self.leg1.schedule.calendar.lag_bus_days( diff --git a/python/rateslib/instruments/bonds/protocols/accrued.py b/python/rateslib/instruments/bonds/protocols/accrued.py index 1f88608d..5a731e66 100644 --- a/python/rateslib/instruments/bonds/protocols/accrued.py +++ b/python/rateslib/instruments/bonds/protocols/accrued.py @@ -5,8 +5,18 @@ from rateslib.enums.generics import NoInput if TYPE_CHECKING: - from rateslib.instruments.bonds.conventions import AccrualFunction - from rateslib.typing import DualTypes, FixedLeg, FloatLeg, _KWArgs, datetime # pragma: no cover + from rateslib.instruments.bonds.conventions.accrued import AccrualFunction # pragma: no cover + from rateslib.typing import ( # pragma: no cover + Cashflow, + DualTypes, + FixedLeg, + FixedPeriod, + FloatLeg, + FloatPeriod, + _BaseCurveOrDict_, + _KWArgs, + datetime, + ) class _WithAccrued(Protocol): @@ -14,6 +24,10 @@ class _WithAccrued(Protocol): Protocol to determine the *yield-to-maturity* of a bond type *Instrument*. """ + def _period_cashflow( + self, period: Cashflow | FixedPeriod | FloatPeriod, rate_curve: _BaseCurveOrDict_ + ) -> DualTypes: ... + @property def leg1(self) -> FixedLeg | FloatLeg: ... diff --git a/python/rateslib/instruments/bonds/protocols/duration.py b/python/rateslib/instruments/bonds/protocols/duration.py index a8ba52c2..de7cdfb9 100644 --- a/python/rateslib/instruments/bonds/protocols/duration.py +++ b/python/rateslib/instruments/bonds/protocols/duration.py @@ -6,7 +6,13 @@ from rateslib.dual.utils import _dual_float if TYPE_CHECKING: - from rateslib.typing import DualTypes, FixedLeg, FloatLeg, datetime # pragma: no cover + from rateslib.typing import ( # pragma: no cover + Any, + DualTypes, + FixedLeg, + FloatLeg, + datetime, + ) class _WithDuration(Protocol): @@ -14,7 +20,7 @@ class _WithDuration(Protocol): Protocol to determine the *yield-to-maturity* of a bond type *Instrument*. """ - def price(self, *args, **kwargs) -> DualTypes: ... + def price(self, *args: Any, **kwargs: Any) -> DualTypes: ... @property def leg1(self) -> FixedLeg | FloatLeg: ... diff --git a/python/rateslib/instruments/bonds/protocols/oaspread.py b/python/rateslib/instruments/bonds/protocols/oaspread.py index 306f1e43..8fda778b 100644 --- a/python/rateslib/instruments/bonds/protocols/oaspread.py +++ b/python/rateslib/instruments/bonds/protocols/oaspread.py @@ -6,24 +6,26 @@ from rateslib import defaults from rateslib.curves._parsers import ( _maybe_set_ad_order, + _validate_obj_not_no_input, ) from rateslib.dual import ift_1dim from rateslib.enums.generics import NoInput, _drb from rateslib.instruments.bonds.protocols import _WithAccrued from rateslib.instruments.protocols.pricing import ( + _maybe_get_curve_maybe_from_solver, _maybe_get_curve_or_dict_maybe_from_solver, ) if TYPE_CHECKING: from rateslib.typing import ( # pragma: no cover - CurveOption_, - Curves_, + CurvesT_, DualTypes, DualTypes_, FXForwards_, - FXVolOption_, Solver_, + VolT_, _BaseCurve, + _BaseCurveOrDict_, _Curves, datetime_, float_, @@ -36,15 +38,15 @@ class _WithOASpread(_WithAccrued, Protocol): Protocol to determine the *yield-to-maturity* of a bond type *Instrument*. """ - def _parse_curves(self, curves: Curves_) -> _Curves: ... + def _parse_curves(self, curves: CurvesT_) -> _Curves: ... def rate( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), - fx_vol: FXVolOption_ = NoInput(0), + vol: VolT_ = NoInput(0), base: str_ = NoInput(0), settlement: datetime_ = NoInput(0), forward: datetime_ = NoInput(0), @@ -54,9 +56,10 @@ def rate( def oaspread( self, *, - curves: Curves_ = NoInput(0), + curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), + vol: VolT_ = NoInput(0), base: str_ = NoInput(0), price: DualTypes_ = NoInput(0), metric: str_ = NoInput(0), @@ -145,7 +148,7 @@ def oaspread( _curves = self._parse_curves(curves) def s_with_args( - g: DualTypes, curve: CurveOption_, disc_curve: _BaseCurve, metric: str_ + g: DualTypes, curve: _BaseCurveOrDict_, disc_curve: _BaseCurve, metric: str_ ) -> DualTypes: """ Return the price of a bond given an OASpread. @@ -164,10 +167,13 @@ def s_with_args( DualTypes """ _shifted_discount_curve = disc_curve.shift(g) - return self.rate(curves=[curve, _shifted_discount_curve], metric=metric) + return self.rate(curves=[curve, _shifted_discount_curve], metric=metric) # type: ignore[list-item] - disc_curve_ = _maybe_get_curve_or_dict_maybe_from_solver( - self.kwargs.meta["curves"], _curves, "disc_curve", solver + disc_curve_ = _validate_obj_not_no_input( + _maybe_get_curve_maybe_from_solver( + self.kwargs.meta["curves"], _curves, "disc_curve", solver + ), + "disc_curve", ) _ad_disc = _maybe_set_ad_order(disc_curve_, 0) diff --git a/python/rateslib/instruments/bonds/protocols/repo.py b/python/rateslib/instruments/bonds/protocols/repo.py index 2e0c4d4e..fa073bbf 100644 --- a/python/rateslib/instruments/bonds/protocols/repo.py +++ b/python/rateslib/instruments/bonds/protocols/repo.py @@ -99,7 +99,7 @@ def fwd_from_repo( for p_idx in range(settlement_idx, fwd_settlement_idx): # deduct accrued coupon from dirty price c_period = self.leg1._regular_periods[p_idx] - c_cashflow: DualTypes = c_period.cashflow() # type: ignore[assignment] + c_cashflow: DualTypes = c_period.cashflow() # TODO handle FloatPeriod cashflow fetch if need a curve. if method.lower() == "proceeds": dcf_ = dcf(c_period.settlement_params.payment, forward_settlement, convention_) @@ -196,7 +196,7 @@ def repo_from_fwd( for p_idx in range(settlement_idx, fwd_settlement_idx): # deduct accrued coupon from dirty price c_period = self.leg1._regular_periods[p_idx] - c_cashflow: DualTypes = c_period.cashflow() # type: ignore[assignment] + c_cashflow: DualTypes = c_period.cashflow() # TODO handle FloatPeriod if it needs a Curve to forecast cashflow dcf_ = dcf( start=c_period.settlement_params.payment, diff --git a/python/rateslib/instruments/bonds/protocols/ytm.py b/python/rateslib/instruments/bonds/protocols/ytm.py index d9705c37..77ead56e 100644 --- a/python/rateslib/instruments/bonds/protocols/ytm.py +++ b/python/rateslib/instruments/bonds/protocols/ytm.py @@ -9,16 +9,23 @@ if TYPE_CHECKING: from rateslib.instruments.bonds.conventions import ( # pragma: no cover - AccrualFunction, BondCalcMode, + ) + from rateslib.instruments.bonds.conventions.accrued import ( # pragma: no cover + AccrualFunction, + ) + from rateslib.instruments.bonds.conventions.discounting import ( # pragma: no cover CashflowFunction, YtmDiscountFunction, ) from rateslib.typing import ( # pragma: no cover + Cashflow, CurveOption_, DualTypes, FixedLeg, + FixedPeriod, FloatLeg, + FloatPeriod, Number, _KWArgs, datetime, @@ -134,7 +141,7 @@ def _generic_price_from_ytm( d += cfi * v2 ** (i - 1) * v2i * v1 # Add the redemption payment discounted by relevant factors - redemption: Cashflow | IndexCashflow = self.leg1._exchange_periods[1] # type: ignore[assignment] + redemption: Cashflow = self.leg1._exchange_periods[1] # type: ignore[assignment] if i == 0: # only looped 1 period, only use the last discount d += self._period_cashflow(redemption, rate_curve) * v1 elif i == 1: # only looped 2 periods, no need for v2 @@ -257,7 +264,7 @@ def ytm( return self._ytm(price=price, settlement=settlement, dirty=dirty, rate_curve=rate_curve) def _period_cashflow( - self, period: Cashflow | FixedPeriod, rate_curve: CurveOption_ - ) -> DualTypes: # type: ignore[override] + self, period: Cashflow | FixedPeriod | FloatPeriod, rate_curve: CurveOption_ + ) -> DualTypes: """Nominal fixed rate bonds use the known "cashflow" attribute on the *Period*.""" - return period.unindexed_cashflow(rate_curve=rate_curve) # type: ignore[return-value] # FixedRate on bond cannot be NoInput + return period.unindexed_cashflow(rate_curve=rate_curve) diff --git a/python/rateslib/instruments/fx_forward.py b/python/rateslib/instruments/fx_forward.py index 1f22da1d..0718aab9 100644 --- a/python/rateslib/instruments/fx_forward.py +++ b/python/rateslib/instruments/fx_forward.py @@ -262,7 +262,7 @@ def rate( ) -> DualTypes: _curves = self._parse_curves(curves) fx_ = _get_fx_maybe_from_solver(solver=solver, fx=fx) - if isinstance(fx_, FXRates | FXForwards): + if isinstance(fx_, FXForwards | FXRates): imm_fx: DualTypes = fx_.rate(self.kwargs.leg2["pair"]) elif isinstance(fx_, NoInput): raise ValueError( @@ -270,7 +270,8 @@ def rate( "Note: it can be attached to, and then fetched from, a Solver.", ) else: - imm_fx = fx_ + # this is a mypy error since FXForwards is a case above + imm_fx = fx_ # type: ignore[assignment] curve_domestic = _maybe_get_curve_maybe_from_solver( self.kwargs.meta["curves"], _curves, "disc_curve", solver diff --git a/python/rateslib/instruments/fx_options/brokerfly.py b/python/rateslib/instruments/fx_options/brokerfly.py index faccbbff..29dc3aef 100644 --- a/python/rateslib/instruments/fx_options/brokerfly.py +++ b/python/rateslib/instruments/fx_options/brokerfly.py @@ -181,7 +181,7 @@ def __init__( strike: tuple[tuple[DualTypes | str, DualTypes | str], DualTypes | str], pair: str_ = NoInput(0), *, - notional: tuple[DualTypes_, DualTypes_] = NoInput(0), + notional: tuple[DualTypes_, DualTypes_] | NoInput = NoInput(0), eval_date: datetime | NoInput = NoInput(0), calendar: CalInput = NoInput(0), modifier: str_ = NoInput(0), @@ -202,10 +202,10 @@ def __init__( ) -> None: vol_ = self._parse_vol(vol) if isinstance(notional, NoInput): - notional_ = (defaults.notional, NoInput(0)) + notional_: tuple[DualTypes_, DualTypes_] = (defaults.notional, NoInput(0)) elif isinstance(notional, tuple | list): notional_ = notional - notional_[1] = NoInput(0) if notional_[1] is None else notional_[1] + notional_[1] = NoInput(0) if notional_[1] is None else notional_[1] # type: ignore[index] else: raise ValueError("FXBrokerFly `notional` must be a 2 element sequence if given.") strategies = [ @@ -227,7 +227,7 @@ def __init__( premium=premium[0], premium_ccy=premium_ccy, curves=curves, - vol=vol_[0], # type: ignore[arg-type, index] + vol=vol_[0], # type: ignore[arg-type] metric=NoInput(0), spec=spec, ), @@ -249,7 +249,7 @@ def __init__( premium=premium[1], premium_ccy=premium_ccy, curves=curves, - vol=vol_[1], # type: ignore[arg-type, index] + vol=vol_[1], # type: ignore[arg-type] metric=NoInput(0), spec=spec, ), @@ -264,6 +264,12 @@ def __init__( ) self.kwargs.leg1["notional"] = notional_ + @property + def instruments(self) -> tuple[FXStrangle, FXStraddle]: + """A tuple containing the :class:`~rateslib.instruments.FXStrangle` and + :class:`~rateslib.instruments.FXStraddle` of the *Fly*.""" + return self.kwargs.meta["instruments"] # type: ignore[no-any-return] + @classmethod def _parse_vol(cls, vol: FXVolStrat_) -> tuple[FXVolStrat_, FXVolStrat_]: # type: ignore[override] if not isinstance(vol, list | tuple): @@ -276,7 +282,7 @@ def _maybe_set_vega_neutral_notional( solver: Solver_, fx: FXForwards_, base: str_, - vol: FXVolStrat_, + vol: tuple[FXVolStrat_, FXVolStrat_], metric: str_, ) -> None: """ @@ -292,7 +298,7 @@ def _maybe_set_vega_neutral_notional( "pips_or_%", "premium", ]: - self.instruments[0]._rate( # type: ignore[union-attr] + self.instruments[0]._rate( curves, solver, fx, @@ -317,7 +323,7 @@ def _maybe_set_vega_neutral_notional( self.instruments[1].kwargs.leg1["notional"] = _dual_float( self.instruments[0].kwargs.leg1["notional"] * -scalar, ) - self.instruments[1]._set_notionals(self.instruments[1].kwargs.leg1["notional"]) # type: ignore[union-attr] + self.instruments[1]._set_notionals(self.instruments[1].kwargs.leg1["notional"]) # BrokerFly -> Strangle -> FXPut -> FXPutPeriod def rate( @@ -353,9 +359,12 @@ def rate( more standardised choice. """ # Get curves and vol - vol_ = [ - _drb(d, b) for (d, b) in zip(self.kwargs.meta["vol"], self._parse_vol(vol), strict=True) - ] + vol_ = tuple( + [ + _drb(d, b) + for (d, b) in zip(self.kwargs.meta["vol"], self._parse_vol(vol), strict=True) + ] + ) _curves = self._parse_curves(curves) metric_ = _drb(self.kwargs.meta["metric"], metric).lower() @@ -363,8 +372,8 @@ def rate( if metric_ == "pips_or_%": straddle_scalar = ( - self.instruments[1].instruments[0]._option.settlement_params.notional # type: ignore[union-attr] - / self.instruments[0].instruments[0]._option.settlement_params.notional # type: ignore[union-attr] + self.instruments[1].instruments[0]._option.settlement_params.notional + / self.instruments[0].instruments[0]._option.settlement_params.notional ) weights: Sequence[DualTypes] = [1.0, straddle_scalar] elif metric_ == "premium": @@ -402,11 +411,11 @@ def analytic_greeks( vol_ = self._parse_vol(vol) # TODO: this meth can be optimised because it calculates greeks at multiple times in frames - g_grks = self.instruments[0].analytic_greeks(curves, solver, fx, base, vol_[0]) # type: ignore[index, arg-type] - d_grks = self.instruments[1].analytic_greeks(curves, solver, fx, base, vol_[1]) # type: ignore[index, arg-type] + g_grks = self.instruments[0].analytic_greeks(curves, solver, fx, base, vol_[0]) + d_grks = self.instruments[1].analytic_greeks(curves, solver, fx, base, vol_[1]) sclr = abs( - self.instruments[1].instruments[0]._option.settlement_params.notional # type: ignore[union-attr] - / self.instruments[0].instruments[0]._option.settlement_params.notional, # type: ignore[union-attr] + self.instruments[1].instruments[0]._option.settlement_params.notional + / self.instruments[0].instruments[0]._option.settlement_params.notional, ) _unit_attrs = ["delta", "gamma", "vega", "vomma", "vanna", "_kega", "_kappa", "__bs76"] @@ -445,124 +454,3 @@ def _plot_payoff( vol_ = self._parse_vol(vol) self._maybe_set_vega_neutral_notional(curves, solver, fx, base, vol_, metric="pips_or_%") return super()._plot_payoff(window, curves, solver, fx, base, local, vol_) - - -# -# -# class FXBrokerFly(FXOptionStrat): -# """ -# Create an *FX BrokerFly* option strategy. -# -# An *FXBrokerFly* is composed of an :class:`~rateslib.instruments.FXStrangle` and an -# :class:`~rateslib.instruments.FXStraddle`, in that order. -# -# For additional arguments see :class:`~rateslib.instruments.FXOption`. -# -# Parameters -# ---------- -# args: tuple -# Positional arguments to :class:`~rateslib.instruments.FXOption`. -# strike: 2-element sequence -# The first element should be a 2-element sequence of strikes of the *FXStrangle*. -# The second element should be a single element for the strike of the *FXStraddle*. -# call, e.g. `[["-25d", "25d"], "atm_delta"]`. -# premium: 2-element sequence, optional -# The premiums associated with each option of the strategy; -# The first element contains 2 values for the premiums of each *FXOption* in the *Strangle*. -# The second element contains 2 values for the premiums of each *FXOption* in the *Straddle*. -# notional: 2-element sequence, optional -# The first element is the notional associated with the *Strangle*. If the second element -# is *None*, it will be implied in a vega neutral sense at price time. -# metric: str, optional -# The default metric to apply in the method :meth:`~rateslib.instruments.FXOptionStrat.rate` -# kwargs: tuple -# Keyword arguments to :class:`~rateslib.instruments.FXOption`. -# -# Notes -# ----- -# Buying a *BrokerFly* equates to buying an :class:`~rateslib.instruments.FXStrangle` and -# selling a :class:`~rateslib.instruments.FXStraddle`, where the convention is to set the -# notional on the *Straddle* such that the entire strategy is *vega* neutral at inception. -# -# When supplying ``strike`` as a string delta the strike will be determined at price time from -# the provided volatility. -# -# .. warning:: -# -# The default ``metric`` for an *FXBrokerFly* is *'single_vol'*, which requires an iterative -# algorithm to solve. -# For defined strikes it is accurate but for strikes defined by delta it -# will return a solution within 0.1 pips. This means it is both slower than other instruments -# and inexact. -# -# """ -# -# rate_weight = [1.0, 1.0] -# rate_weight_vol = [1.0, -1.0] -# _rate_scalar = 100.0 -# -# periods: list[FXOptionStrat] # type: ignore[assignment] -# vol: FXVolStrat_ -# -# def __init__( -# self, -# *args: Any, -# strike: tuple[tuple[DualTypes | str_, DualTypes | str_], DualTypes | str_] = ( -# (NoInput(0), NoInput(0)), -# NoInput(0), -# ), -# premium: tuple[tuple[DualTypes_, DualTypes_], tuple[DualTypes_, DualTypes_]] = ( -# (NoInput(0), NoInput(0)), -# (NoInput(0), NoInput(0)), -# ), -# notional: tuple[DualTypes_, DualTypes_] = (NoInput(0), NoInput(0)), -# metric: str = "single_vol", -# **kwargs: Any, -# ) -> None: -# super(FXOptionStrat, self).__init__( # type: ignore[misc] -# *args, -# premium=list(premium), # type: ignore[arg-type] -# strike=list(strike), # type: ignore[arg-type] -# notional=list(notional), # type: ignore[arg-type] -# **kwargs, -# ) -# self.kwargs["notional"][1] = ( -# NoInput(0) if self.kwargs["notional"][1] is None else self.kwargs["notional"][1] -# ) -# self.kwargs["metric"] = metric -# self._strat_elements = ( -# FXStrangle( -# pair=self.kwargs["pair"], -# expiry=self.kwargs["expiry"], -# delivery_lag=self.kwargs["delivery"], -# payment_lag=self.kwargs["payment"], -# calendar=self.kwargs["calendar"], -# modifier=self.kwargs["modifier"], -# strike=self.kwargs["strike"][0], -# notional=self.kwargs["notional"][0], -# option_fixing=self.kwargs["option_fixing"], -# delta_type=self.kwargs["delta_type"], -# premium=self.kwargs["premium"][0], -# premium_ccy=self.kwargs["premium_ccy"], -# metric=self.kwargs["metric"], -# curves=self.curves, -# vol=self.vol, -# ), -# FXStraddle( -# pair=self.kwargs["pair"], -# expiry=self.kwargs["expiry"], -# delivery_lag=self.kwargs["delivery"], -# payment_lag=self.kwargs["payment"], -# calendar=self.kwargs["calendar"], -# modifier=self.kwargs["modifier"], -# strike=self.kwargs["strike"][1], -# notional=self.kwargs["notional"][1], -# option_fixing=self.kwargs["option_fixing"], -# delta_type=self.kwargs["delta_type"], -# premium=self.kwargs["premium"][1], -# premium_ccy=self.kwargs["premium_ccy"], -# metric="vol" if self.kwargs["metric"] == "single_vol" else self.kwargs["metric"], -# curves=self.curves, -# vol=self.vol, -# ), -# ) diff --git a/python/rateslib/instruments/fx_options/call_put.py b/python/rateslib/instruments/fx_options/call_put.py index 3811009b..36d04790 100644 --- a/python/rateslib/instruments/fx_options/call_put.py +++ b/python/rateslib/instruments/fx_options/call_put.py @@ -252,7 +252,7 @@ def __init__( self._leg1 = CustomLeg( [ - FXCallPeriod( + FXCallPeriod( # type: ignore[abstract] pair=self.kwargs.leg1["pair"], expiry=self.kwargs.leg1["expiry"], delivery=self.kwargs.leg1["delivery"], @@ -267,7 +267,7 @@ def __init__( metric=self.kwargs.meta["metric_period"], ) if call - else FXPutPeriod( + else FXPutPeriod( # type: ignore[abstract] pair=self.kwargs.leg1["pair"], expiry=self.kwargs.leg1["expiry"], delivery=self.kwargs.leg1["delivery"], @@ -286,7 +286,7 @@ def __init__( self._leg2 = CustomLeg( [ Cashflow( - notional=self.kwargs.leg2["premium"], + notional=_drb(0.0, self.kwargs.leg2["premium"]), payment=self.kwargs.leg2["payment"], currency=self.kwargs.leg2["premium_ccy"], ), @@ -619,6 +619,34 @@ def cashflows( settlement: datetime_ = NoInput(0), forward: datetime_ = NoInput(0), ) -> DataFrame: + try: + _curves = self._parse_curves(curves) + _vol = self._parse_vol(vol) + rate_curve = _maybe_get_curve_maybe_from_solver( + curves=_curves, + curves_meta=self.kwargs.meta["curves"], + solver=solver, + name="rate_curve", + ) + disc_curve = _maybe_get_curve_maybe_from_solver( + curves=_curves, + curves_meta=self.kwargs.meta["curves"], + solver=solver, + name="disc_curve", + ) + fx_vol = _maybe_get_fx_vol_maybe_from_solver( + vol=_vol, vol_meta=self.kwargs.meta["vol"], solver=solver + ) + fx_ = _get_fx_forwards_maybe_from_solver(solver=solver, fx=fx) + self._set_strike_and_vol( + rate_curve=rate_curve, disc_curve=disc_curve, fx=fx_, vol=fx_vol + ) + self._set_premium( + rate_curve=rate_curve, disc_curve=disc_curve, fx=fx_, pricing=self._pricing + ) + except Exception: # noqa: S110 + pass # `cashflows` proceed without pricing determined values + return self._cashflows_from_legs( curves=curves, solver=solver, @@ -626,6 +654,7 @@ def cashflows( base=base, settlement=settlement, forward=forward, + vol=vol, ) def analytic_greeks( @@ -732,7 +761,7 @@ def _analytic_greeks_reduced( self, curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), - fx: FX_ = NoInput(0), + fx: FXForwards_ = NoInput(0), base: str_ = NoInput(0), vol: FXVol_ = NoInput(0), set_metrics: bool_ = True, @@ -763,7 +792,7 @@ def _analytic_greeks_reduced( rate_curve=_validate_obj_not_no_input(rate_curve, "rate_curve"), disc_curve=_validate_obj_not_no_input(disc_curve, "disc_curve"), fx=_validate_fx_as_forwards(fx_), - fx_vol=self._pricing.vol, + fx_vol=self._pricing.vol, # type: ignore[arg-type] # vol is set and != None premium=NoInput(0), _reduced=True, ) # none of the reduced greeks need a VolObj - faster to reuse from _pricing.vol @@ -835,6 +864,31 @@ def plot_payoff( ) return plot([x], [y]) # type: ignore + def local_analytic_rate_fixings( + self, + *, + curves: CurvesT_ = NoInput(0), + solver: Solver_ = NoInput(0), + fx: FXForwards_ = NoInput(0), + vol: VolT_ = NoInput(0), + settlement: datetime_ = NoInput(0), + forward: datetime_ = NoInput(0), + ) -> DataFrame: + return DataFrame() + + def spread( + self, + *, + curves: CurvesT_ = NoInput(0), + solver: Solver_ = NoInput(0), + fx: FXForwards_ = NoInput(0), + vol: VolT_ = NoInput(0), + base: str_ = NoInput(0), + settlement: datetime_ = NoInput(0), + forward: datetime_ = NoInput(0), + ) -> DualTypes: + raise NotImplementedError(f"`spread` is not implemented for type: {type(self).__name__}") + class FXCall(FXOption): """ diff --git a/python/rateslib/instruments/fx_options/risk_reversal.py b/python/rateslib/instruments/fx_options/risk_reversal.py index 5679f889..2084b654 100644 --- a/python/rateslib/instruments/fx_options/risk_reversal.py +++ b/python/rateslib/instruments/fx_options/risk_reversal.py @@ -144,7 +144,7 @@ def __init__( # if isinstance(obj, FXOptionStrat): # ret.append(obj._parse_vol_sequence(vol_)) # else: - # assert isinstance(vol_, str) or not isinstance(vol_, Sequence) # noqa: S101 + # assert isinstance(vol_, str) or not isinstance(vol_, Sequence) # ret.append(vol_) # return ret # @@ -162,7 +162,9 @@ def __init__( # ret.append(obj._get_fxvol_maybe_from_solver_recursive(vol__, solver)) # else: # assert isinstance(vol__, str) or not isinstance(vol__, Sequence) # noqa: S101 - # ret.append(_get_fxvol_maybe_from_solver(vol_attr=obj.vol, vol=vol__, solver=solver)) + # ret.append( + # _get_fxvol_maybe_from_solver(vol_attr=obj.vol, vol=vol__, solver=solver) + # ) # return ret @classmethod @@ -306,11 +308,11 @@ def npv( fx=fx, base=base, local=local, - vol=vol__, # type: ignore[arg-type] + vol=vol__, forward=forward, settlement=settlement, ) - for (option, vol__) in zip(self.instruments, vol_, strict=True) # type: ignore[arg-type] + for (option, vol__) in zip(self.instruments, vol_, strict=True) ] if local: @@ -657,7 +659,7 @@ def __init__( premium=premium[0], premium_ccy=premium_ccy, curves=curves, - vol=vol_[0], # type: ignore[index] + vol=vol_[0], metric=NoInput(0), spec=spec, ), @@ -679,7 +681,7 @@ def __init__( premium=premium[1], premium_ccy=premium_ccy, curves=curves, - vol=vol_[1], # type: ignore[index] + vol=vol_[1], metric=NoInput(0), spec=spec, ), diff --git a/python/rateslib/instruments/fx_options/straddle.py b/python/rateslib/instruments/fx_options/straddle.py index fe1ff9b0..f7fb9f9b 100644 --- a/python/rateslib/instruments/fx_options/straddle.py +++ b/python/rateslib/instruments/fx_options/straddle.py @@ -213,7 +213,7 @@ def __init__( premium=premium[0], premium_ccy=premium_ccy, curves=curves, - vol=vol_[0], # type: ignore[index] + vol=vol_[0], metric=NoInput(0), spec=spec, ), @@ -235,7 +235,7 @@ def __init__( premium=premium[1], premium_ccy=premium_ccy, curves=curves, - vol=vol_[1], # type: ignore[index] + vol=vol_[1], metric=NoInput(0), spec=spec, ), diff --git a/python/rateslib/instruments/fx_options/strangle.py b/python/rateslib/instruments/fx_options/strangle.py index e17e49a0..6a280a6c 100644 --- a/python/rateslib/instruments/fx_options/strangle.py +++ b/python/rateslib/instruments/fx_options/strangle.py @@ -30,10 +30,10 @@ FXForwards, FXForwards_, FXOptionPeriod, - FXVol, FXVolStrat_, Solver_, VolT_, + _FXVolOption, _Vol, bool_, datetime, @@ -232,7 +232,7 @@ def __init__( premium=premium[0], premium_ccy=premium_ccy, curves=curves, - vol=vol_[0], # type: ignore[index] + vol=vol_[0], metric=NoInput(0), spec=spec, ), @@ -254,7 +254,7 @@ def __init__( premium=premium[1], premium_ccy=premium_ccy, curves=curves, - vol=vol_[1], # type: ignore[index] + vol=vol_[1], metric=NoInput(0), spec=spec, ), @@ -395,15 +395,21 @@ def _rate_single_vol( ), "disc_curve", ) - vol_0 = _maybe_get_fx_vol_maybe_from_solver( - vol_meta=self.kwargs.meta["vol"][0], - vol=_vol[0], - solver=solver, + vol_0: _FXVolOption = _validate_obj_not_no_input( # type: ignore[assignment] + _maybe_get_fx_vol_maybe_from_solver( + vol_meta=self.kwargs.meta["vol"][0], + vol=_vol[0], + solver=solver, + ), + "`vol` at index [0]", ) - vol_1 = _maybe_get_fx_vol_maybe_from_solver( - vol_meta=self.kwargs.meta["vol"][1], - vol=_vol[1], - solver=solver, + vol_1: _FXVolOption = _validate_obj_not_no_input( # type: ignore[assignment] + _maybe_get_fx_vol_maybe_from_solver( + vol_meta=self.kwargs.meta["vol"][1], + vol=_vol[1], + solver=solver, + ), + "`vol` at index [1]", ) # Get initial data from objects in their native AD order @@ -608,7 +614,7 @@ def _d_c_mkt_d_sigma_hat( g: dict[str, Any], # greeks sg: dict[str, Any], # smile_greeks expiry: datetime, - vol: FXVol, + vol: _FXVolOption, eta1: float | None, fixed_delta: bool, fzw1zw0: DualTypes | None, diff --git a/python/rateslib/instruments/fx_swap.py b/python/rateslib/instruments/fx_swap.py index 94caf1ae..69a0d3e4 100644 --- a/python/rateslib/instruments/fx_swap.py +++ b/python/rateslib/instruments/fx_swap.py @@ -27,7 +27,6 @@ DualTypes, DualTypes_, FXForwards_, - PeriodFixings, RollDay, Sequence, Solver_, @@ -252,8 +251,8 @@ def __init__( leg2_notional: DualTypes_ = NoInput(0), split_notional: DualTypes_ = NoInput(0), # rate - fx_fixings: PeriodFixings = NoInput(0), - leg2_fx_fixings: PeriodFixings = NoInput(0), + fx_fixings: DualTypes_ = NoInput(0), + leg2_fx_fixings: DualTypes_ = NoInput(0), points: DualTypes_ = NoInput(0), # meta curves: CurvesT_ = NoInput(0), @@ -394,8 +393,8 @@ def _validate_init_combinations( self, notional: DualTypes_, leg2_notional: DualTypes_, - fx_fixings: PeriodFixings, - leg2_fx_fixings: PeriodFixings, + fx_fixings: DualTypes_, + leg2_fx_fixings: DualTypes_, points: DualTypes_, ) -> None: if not isinstance(fx_fixings, NoInput): diff --git a/python/rateslib/instruments/fx_vol_value.py b/python/rateslib/instruments/fx_vol_value.py index 7d2aa36c..9689bb7d 100644 --- a/python/rateslib/instruments/fx_vol_value.py +++ b/python/rateslib/instruments/fx_vol_value.py @@ -173,13 +173,25 @@ def rate( return vol_._get_index( delta_index=self.kwargs.leg1["index_value"], expiry=self.kwargs.leg1["expiry"] ) - elif isinstance(vol_, FXSabrSmile | FXSabrSurface): + elif isinstance(vol_, FXSabrSmile): fx_ = _validate_fx_as_forwards( _get_fx_forwards_maybe_from_solver(solver=solver, fx=fx) ) + # if Sabr VolObj is not initialised with a `pair` this will create an error + pair: str = vol_.meta.pair # type: ignore[assignment] return vol_.get_from_strike( k=self.kwargs.leg1["index_value"], - f=fx_.rate(pair=vol_.meta.pair, settlement=vol_.meta.delivery), + f=fx_.rate(pair=pair, settlement=vol_.meta.delivery), + expiry=self.kwargs.leg1["expiry"], + )[1] + elif isinstance(vol_, FXSabrSurface): + fx_ = _validate_fx_as_forwards( + _get_fx_forwards_maybe_from_solver(solver=solver, fx=fx) + ) + # if Sabr VolObj is not initialised with a `pair` this will create an error + return vol_.get_from_strike( + k=self.kwargs.leg1["index_value"], + f=fx_, expiry=self.kwargs.leg1["expiry"], )[1] else: diff --git a/python/rateslib/instruments/iirs.py b/python/rateslib/instruments/iirs.py index 2ddf388d..6b507595 100644 --- a/python/rateslib/instruments/iirs.py +++ b/python/rateslib/instruments/iirs.py @@ -26,8 +26,8 @@ Frequency, FXForwards_, IndexMethod, + LegFixings, RollDay, - Series, Solver_, VolT_, _BaseLeg, @@ -308,7 +308,7 @@ def __init__( index_base: DualTypes_ = NoInput(0), index_lag: int_ = NoInput(0), index_method: IndexMethod | str_ = NoInput(0), - index_fixings: Series[DualTypes] | str_ = NoInput(0), + index_fixings: LegFixings = NoInput(0), # type: ignore[type-var] # rate params fixed_rate: DualTypes_ = NoInput(0), leg2_float_spread: DualTypes_ = NoInput(0), diff --git a/python/rateslib/instruments/ndf.py b/python/rateslib/instruments/ndf.py index b1734ab4..828e0981 100644 --- a/python/rateslib/instruments/ndf.py +++ b/python/rateslib/instruments/ndf.py @@ -223,9 +223,9 @@ def __init__( eom: bool_ = NoInput(0), # rate fx_rate: DualTypes_ = NoInput(0), - fx_fixings: PeriodFixings = NoInput(0), + fx_fixings: PeriodFixings = NoInput(0), # type: ignore[type-var] leg2_fx_fixings: PeriodFixings = NoInput(0), - reversed: bool = False, + reversed: bool = False, # noqa: A002 leg2_reversed: bool = False, # meta curves: CurvesT_ = NoInput(0), diff --git a/python/rateslib/instruments/protocols/cashflows.py b/python/rateslib/instruments/protocols/cashflows.py index 9aaaf484..ec7030a5 100644 --- a/python/rateslib/instruments/protocols/cashflows.py +++ b/python/rateslib/instruments/protocols/cashflows.py @@ -174,6 +174,7 @@ def _cashflows_from_instruments(self, *args: Any, **kwargs: Any) -> DataFrame: def cashflows_table( self, + *, curves: CurvesT_ = NoInput(0), solver: Solver_ = NoInput(0), fx: FXForwards_ = NoInput(0), diff --git a/python/rateslib/instruments/protocols/pricing.py b/python/rateslib/instruments/protocols/pricing.py index c6ecfe65..5e5f6935 100644 --- a/python/rateslib/instruments/protocols/pricing.py +++ b/python/rateslib/instruments/protocols/pricing.py @@ -134,7 +134,7 @@ def __init__( self._fx_vol = fx_vol @property - def fx_vol(self) -> _FXVolOption_: + def fx_vol(self) -> FXVol_: """The FX vol object used for modelling FX volatility.""" return self._fx_vol @@ -332,7 +332,7 @@ def _parse_curve_or_id_from_solver_(curve: _BaseCurveOrId, solver: Solver) -> _B # Proxy curves and MultiCsaCurves can exist outside of Solvers but be constructed # directly from an FXForwards object tied to a Solver using only a Solver's # dependent curves and AD variables. - return curve + return curve # type: ignore[no-any-return] # mypy error else: try: # it is a safeguard to load curves from solvers when a solver is @@ -376,7 +376,7 @@ def _maybe_get_fx_vol_maybe_from_solver( vol: _Vol, # name: str, = "fx_vol" solver: Solver_, -) -> _FXVolObj | NoInput: +) -> _FXVolOption_: fx_vol_ = _drb(vol_meta.fx_vol, vol.fx_vol) if isinstance(fx_vol_, NoInput | float | Dual | Dual2 | Variable): return fx_vol_ @@ -444,7 +444,7 @@ def _get_fx_forwards_maybe_from_solver(solver: Solver_, fx: FXForwards_) -> FXFo fx_ = NoInput(0) else: # TODO disallow `fx` on Solver as FXRates. Only allow FXForwards. - fx_ = solver._get_fx() # type: ignore[assignment] + fx_ = solver._get_fx() else: fx_ = fx if ( @@ -462,7 +462,7 @@ def _get_fx_forwards_maybe_from_solver(solver: Solver_, fx: FXForwards_) -> FXFo return fx_ -def _get_fx_maybe_from_solver(solver: Solver_, fx: FX_) -> FX_: +def _get_fx_maybe_from_solver(solver: Solver_, fx: FXForwards_) -> FXForwards_: if isinstance(fx, NoInput): if isinstance(solver, NoInput): fx_: FX_ = NoInput(0) @@ -470,7 +470,7 @@ def _get_fx_maybe_from_solver(solver: Solver_, fx: FX_) -> FX_: if isinstance(solver.fx, NoInput): fx_ = NoInput(0) else: - fx_ = solver._get_fx() + fx_ = solver._get_fx() # will validate the state else: fx_ = fx if ( @@ -485,4 +485,4 @@ def _get_fx_maybe_from_solver(solver: Solver_, fx: FX_) -> FX_: UserWarning, ) - return fx_ + return fx_ # type: ignore[return-value] diff --git a/python/rateslib/instruments/sbs.py b/python/rateslib/instruments/sbs.py index ba1f7a0b..15d65168 100644 --- a/python/rateslib/instruments/sbs.py +++ b/python/rateslib/instruments/sbs.py @@ -25,6 +25,7 @@ FloatRateSeries, Frequency, FXForwards_, + LegFixings, RollDay, Sequence, Solver_, @@ -305,14 +306,14 @@ def __init__( # rate parameters float_spread: DualTypes_ = NoInput(0), spread_compound_method: str_ = NoInput(0), - rate_fixings: FixingsRates_ = NoInput(0), + rate_fixings: LegFixings = NoInput(0), # type: ignore[type-var] 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_rate_fixings: LegFixings = NoInput(0), leg2_fixing_method: str_ = NoInput(0), leg2_method_param: int_ = NoInput(0), leg2_fixing_frequency: Frequency | str_ = NoInput(0), diff --git a/python/rateslib/instruments/xcs.py b/python/rateslib/instruments/xcs.py index 6ac2e2b4..aa431e18 100644 --- a/python/rateslib/instruments/xcs.py +++ b/python/rateslib/instruments/xcs.py @@ -318,7 +318,11 @@ class XCS(_BaseInstrument): """ # noqa: E501 - _rate_scalar = 1.0 + def _rate_scalar_calc(self) -> float: + if self.kwargs.meta["metric"] == "leg1": + return 1.0 if isinstance(self.leg1, FixedLeg) else 100.0 + else: + return 1.0 if isinstance(self.leg2, FixedLeg) else 100.0 @property def fixed_rate(self) -> DualTypes_: @@ -455,7 +459,7 @@ def __init__( method_param: int_ = NoInput(0), fixing_frequency: Frequency | str_ = NoInput(0), fixing_series: FloatRateSeries | str_ = NoInput(0), - fx_fixings: LegFixings = NoInput(0), + fx_fixings: LegFixings = NoInput(0), # type: ignore[type-var] leg2_fixed: bool = False, leg2_mtm: bool = False, leg2_fixed_rate: DualTypes_ = NoInput(0), @@ -637,6 +641,7 @@ def __init__( else: self._leg2 = FloatLeg(**_convert_to_schedule_kwargs(self.kwargs.leg2, 1)) self._legs = [self.leg1, self.leg2] + self._rate_scalar = self._rate_scalar_calc() def rate( self, @@ -823,6 +828,25 @@ def _set_pricing_mid( ) self.leg2.fixed_rate = _dual_float(mid_price) + elif ( + isinstance(self.leg1, FloatLeg) + and isinstance(self.kwargs.leg1["float_spread"], NoInput) + and isinstance(self.leg2, FloatLeg) + and isinstance(self.kwargs.leg2["float_spread"], NoInput) + ): + # then no FloatLeg pricing parameters are provided + mid_price = self.rate( + curves=curves, + solver=solver, + fx=fx, + settlement=settlement, + forward=forward, + ) + if self.kwargs.meta["metric"].lower() == "leg1": + self.leg1.float_spread = _dual_float(mid_price) + else: + self.leg2.float_spread = _dual_float(mid_price) + def _parse_vol(self, vol: VolT_) -> _Vol: return _Vol() diff --git a/python/rateslib/instruments/zcis.py b/python/rateslib/instruments/zcis.py index ed928604..c0091c32 100644 --- a/python/rateslib/instruments/zcis.py +++ b/python/rateslib/instruments/zcis.py @@ -24,8 +24,8 @@ Frequency, FXForwards_, IndexMethod, + LegFixings, RollDay, - Series, Solver_, VolT_, _BaseLeg, @@ -238,7 +238,7 @@ def __init__( leg2_index_base: DualTypes_ = NoInput(0), leg2_index_lag: int_ = NoInput(0), leg2_index_method: IndexMethod | str_ = NoInput(0), - leg2_index_fixings: Series[DualTypes] | str_ = NoInput(0), + leg2_index_fixings: LegFixings = NoInput(0), # type: ignore[type-var] # meta parameters curves: CurvesT_ = NoInput(0), spec: str_ = NoInput(0), diff --git a/python/rateslib/legs/fixed.py b/python/rateslib/legs/fixed.py index e4349e79..3e699ee1 100644 --- a/python/rateslib/legs/fixed.py +++ b/python/rateslib/legs/fixed.py @@ -515,7 +515,7 @@ def __init__( index_base: DualTypes_ = NoInput(0), index_lag: int_ = NoInput(0), index_method: IndexMethod | str_ = NoInput(0), - index_fixings: Series[DualTypes] | str_ = NoInput(0), # type: ignore[type-var] + index_fixings: LegFixings = NoInput(0), index_only: bool = False, ) -> None: self._fixed_rate = fixed_rate diff --git a/python/rateslib/legs/float.py b/python/rateslib/legs/float.py index d227441b..f8a252a2 100644 --- a/python/rateslib/legs/float.py +++ b/python/rateslib/legs/float.py @@ -258,7 +258,7 @@ def __init__( final_exchange: bool = False, # rate params float_spread: DualTypes_ = NoInput(0), - rate_fixings: DualTypes | Series[DualTypes] | str_ = NoInput(0), # type: ignore[type-var] + rate_fixings: LegFixings = NoInput(0), fixing_method: FloatFixingMethod | str_ = NoInput(0), method_param: int_ = NoInput(0), spread_compound_method: SpreadCompoundMethod | str_ = NoInput(0), @@ -268,7 +268,7 @@ def __init__( index_base: DualTypes_ = NoInput(0), index_lag: int_ = NoInput(0), index_method: IndexMethod | str_ = NoInput(0), - index_fixings: Series[DualTypes] | str_ = NoInput(0), # type: ignore[type-var] + index_fixings: LegFixings = NoInput(0), ) -> None: self._schedule = schedule self._notional: DualTypes = _drb(defaults.notional, notional) diff --git a/python/rateslib/periods/float_period.py b/python/rateslib/periods/float_period.py index 4edfb7ab..c55f1612 100644 --- a/python/rateslib/periods/float_period.py +++ b/python/rateslib/periods/float_period.py @@ -42,6 +42,7 @@ Any, Arr1dObj, CalInput, + Convention, CurveOption_, DualTypes, DualTypes_, @@ -285,7 +286,7 @@ def __init__( start: datetime, end: datetime, frequency: Frequency | str, - convention: str_ = NoInput(0), + convention: Convention | str_ = NoInput(0), termination: datetime_ = NoInput(0), stub: bool = False, roll: RollDay | int | str_ = NoInput(0), diff --git a/python/rateslib/periods/fx_volatility.py b/python/rateslib/periods/fx_volatility.py index 90f8cbb2..2067bc5c 100644 --- a/python/rateslib/periods/fx_volatility.py +++ b/python/rateslib/periods/fx_volatility.py @@ -594,7 +594,7 @@ def _index_vol_and_strike_from_atm( return self._index_vol_and_strike_from_atm_sabr(f, eta_0, vol) else: # DualTypes | FXDeltaVolSmile | FXDeltaVolSurface f_: DualTypes = f # type: ignore[assignment] - vol_: DualTypes | FXDeltaVolSmile | FXDeltaVolSurface = vol + vol_: DualTypes | FXDeltaVolSmile | FXDeltaVolSurface = vol # type: ignore[assignment] return self._index_vol_and_strike_from_atm_dv( f_, eta_0, @@ -620,7 +620,7 @@ def _index_vol_and_strike_from_atm_sabr( f_d: DualTypes = f.rate(self.fx_option_params.pair, self.fx_option_params.delivery) # _ad = _set_ad_order_objects([0], [f]) # GH755 else: - f_d = f + f_d = f # type: ignore[assignment] def root1d( k: DualTypes, f_d: DualTypes, fx: DualTypes | FXForwards, as_float: bool @@ -638,7 +638,7 @@ def root1d( if isinstance(vol, FXSabrSmile): alpha = vol.nodes.alpha else: # FXSabrSurface - vol_: FXSabrSurface = vol + vol_: FXSabrSurface = vol # type: ignore[assignment] expiry_posix = self.fx_option_params.expiry.replace(tzinfo=UTC).timestamp() e_idx, _ = _surface_index_left(vol_.meta.expiries_posix, expiry_posix) alpha = vol_.smiles[e_idx].nodes.alpha @@ -765,7 +765,7 @@ def _index_vol_and_strike_from_delta( return self._index_vol_and_strike_from_delta_sabr(delta, delta_type, vol, z_w, f) else: # DualTypes | FXDeltaVolSmile | FXDeltaVolSurface f_: DualTypes = f # type: ignore[assignment] - vol_: DualTypes | FXDeltaVolSmile = vol + vol_: DualTypes | FXDeltaVolSmile = vol # type: ignore[assignment] return self._index_vol_and_strike_from_delta_dv( f_, delta, @@ -854,7 +854,7 @@ def _index_vol_and_strike_from_delta_sabr( f_d: DualTypes = f.rate(self.fx_option_params.pair, self.fx_option_params.delivery) # _ad = _set_ad_order_objects([0], [f]) # GH755 else: - f_d = f + f_d = f # type: ignore[assignment] def root1d( k: DualTypes, @@ -893,7 +893,7 @@ def root1d( if isinstance(vol, FXSabrSmile): alpha = vol.nodes.alpha else: # FXSabrSurface - vol_: FXSabrSurface = vol + vol_: FXSabrSurface = vol # type: ignore[assignment] expiry_posix = self.fx_option_params.expiry.replace(tzinfo=UTC).timestamp() e_idx, _ = _surface_index_left(vol_.meta.expiries_posix, expiry_posix) alpha = vol_.smiles[e_idx].nodes.alpha diff --git a/python/rateslib/periods/utils.py b/python/rateslib/periods/utils.py index ebbc23c6..353c9b14 100644 --- a/python/rateslib/periods/utils.py +++ b/python/rateslib/periods/utils.py @@ -18,6 +18,7 @@ Any, CurveOption_, DualTypes, + FXForwards_, _BaseCurve, _BaseCurve_, _FXVolOption_, @@ -30,7 +31,7 @@ def _maybe_local( value: DualTypes, local: bool, currency: str, - fx: FX_, + fx: FXForwards_, base: str_, forward: datetime_, ) -> dict[str, DualTypes] | DualTypes: @@ -48,7 +49,7 @@ def _maybe_local( def _maybe_fx_converted( value: DualTypes, currency: str, - fx: FX_, + fx: FXForwards_, base: str_, forward: datetime_, ) -> DualTypes: @@ -61,7 +62,7 @@ def _maybe_fx_converted( def _get_immediate_fx_scalar_and_base( currency: str, - fx: FX_, + fx: FXForwards_, base: str_, ) -> tuple[DualTypes, str]: """ @@ -70,11 +71,11 @@ def _get_immediate_fx_scalar_and_base( FX rate is 1.0 """ if isinstance(base, NoInput) or base is None: - if isinstance(fx, NoInput | (FXRates | FXForwards)): + if isinstance(fx, NoInput | FXRates | FXForwards): return 1.0, currency else: # fx is DualTypes - if abs(fx - 1.0) < 1e-10: - return fx, currency # base is assumed + if abs(fx - 1.0) < 1e-10: # type: ignore[operator] + return fx, currency # type: ignore[return-value] # base is assumed else: warnings.warn( "It is not best practice to provide `fx` as numeric since this can " @@ -86,7 +87,7 @@ def _get_immediate_fx_scalar_and_base( f"[fx=FXRates({{'{currency}bas': {fx}}}), base='bas'].", UserWarning, ) - return fx, "Unspecified" # base is unknown + return fx, "Unspecified" # type: ignore[return-value] # base is unknown else: # base is str if isinstance(fx, NoInput): if base != currency: @@ -104,7 +105,7 @@ def _get_immediate_fx_scalar_and_base( else: return fx.rate(pair=f"{currency}{base}"), base else: # FX is DualTypes - if abs(fx - 1.0) < 1e-10: + if abs(fx - 1.0) < 1e-10: # type: ignore[operator] pass # no warning when fx == 1.0 elif base == currency: raise ValueError( @@ -122,7 +123,7 @@ def _get_immediate_fx_scalar_and_base( f"[fx=FXRates({{'{currency}{base}': {fx}}}), base='{base}'].", DeprecationWarning, ) - return fx, base + return fx, base # type: ignore[return-value] def _get_vol_maybe_from_obj( diff --git a/python/rateslib/solver.py b/python/rateslib/solver.py index f7582c61..acaace7d 100644 --- a/python/rateslib/solver.py +++ b/python/rateslib/solver.py @@ -44,6 +44,7 @@ DualTypes, FXDeltaVolSmile, FXDeltaVolSurface, + FXForwards_, FXSabrSmile, FXSabrSurface, Sequence, @@ -92,7 +93,7 @@ class Gradients: pre_variables: tuple[str, ...] # string tags for AD coordination pre_rate_scalars: list[float] # scalars for the rate attribute of instruments _ad: int # ad order - instruments: tuple[tuple[SupportsRate, tuple[Any, ...], dict[str, Any]], ...] # calibrators + instruments: tuple[tuple[SupportsRate, dict[str, Any]], ...] # calibrators @property def J(self) -> NDArray[Nf64]: @@ -1039,7 +1040,7 @@ def __init__( s: Sequence[DualTypes] = (), weights: Sequence[float] | NoInput = NoInput(0), algorithm: str_ = NoInput(0), - fx: FXForwards | FXRates | NoInput = NoInput(0), + fx: FXForwards_ = NoInput(0), instrument_labels: Sequence[str] | NoInput = NoInput(0), id: str_ = NoInput(0), # noqa: A002 pre_solvers: Sequence[Solver] = (), @@ -1147,13 +1148,11 @@ def __init__( # Final elements self._ad = 1 - self.fx: FXRates | FXForwards | NoInput = fx + self.fx: FXForwards_ = fx if isinstance(self.fx, FXRates | FXForwards): self.fx._set_ad_order(1) elif not isinstance(self.fx, NoInput): - raise ValueError( - "`fx` argument to Solver must be either FXRates, FXForwards or NoInput(0)." - ) + raise ValueError("`fx` argument to Solver must be either FXForwards or NoInput(0).") self.instruments: tuple[tuple[SupportsRate, dict[str, Any]], ...] = tuple( self._parse_instrument(inst) for inst in instruments ) @@ -1366,7 +1365,7 @@ def _get_pre_fxvol(self, obj: str) -> FXVols: ) @_validate_states - def _get_fx(self) -> FXRates | FXForwards | NoInput: + def _get_fx(self) -> FXForwards_: return self.fx # Attributes @@ -1699,7 +1698,7 @@ def delta( association exists and a direct ``fx`` object is supplied a warning may be emitted if they are not the same object. """ - self._do_not_validate = True # state is validated prior to the call + # self._do_not_validate = True # state is validated prior to the call base, fx = self._get_base_and_fx(base, fx) if isinstance(fx, FXRates | FXForwards): fx_vars: tuple[str, ...] = fx.variables @@ -1752,7 +1751,7 @@ def delta( sorted_cols = df.columns.sort_values() ret: DataFrame = df.loc[:, sorted_cols].astype("float64") - self._do_not_validate = False + # self._do_not_validate = False return ret def _get_base_and_fx(self, base: str_, fx: FX_) -> tuple[str_, FX_]: @@ -1770,8 +1769,11 @@ def _get_base_and_fx(self, base: str_, fx: FX_) -> tuple[str_, FX_]: # then a valid fx object that can convert is required. if not isinstance(fx, FXRates | FXForwards) and isinstance(self.fx, NoInput): raise ValueError( - "`base` is given but `fx` is not given as either FXRates or FXForwards, " - "and Solver does not contain its own `fx` attributed which can be substituted." + f"`base` is given as '{base}', but `fx` is not available.\n" + "Either provide an FXForwards object directly as `fx` or ensure that Solver.fx " + "is a valid object.\n" + "Alternatively, omit the `base` argument altogether and get results displayed " + "in local currency without base currency conversion." ) if isinstance(fx, NoInput): diff --git a/python/rateslib/typing.py b/python/rateslib/typing.py index fab44818..46e18033 100644 --- a/python/rateslib/typing.py +++ b/python/rateslib/typing.py @@ -101,6 +101,9 @@ NullInterpolator, UnionCal, ) + +CurveInterpolator: TypeAlias = "FlatBackwardInterpolator | FlatForwardInterpolator | LinearInterpolator | LogLinearInterpolator | LinearZeroRateInterpolator | NullInterpolator" + from rateslib.rs import Convention as Convention from rateslib.rs import Dual as Dual from rateslib.rs import Dual2 as Dual2 @@ -197,24 +200,24 @@ FXRevised_: TypeAlias = "FXRates | FXForwards | NoInput" FXForwards_: TypeAlias = "FXForwards | NoInput" -NPV: TypeAlias = "DualTypes | dict[str, DualTypes]" - -CurveInterpolator: TypeAlias = "FlatBackwardInterpolator | FlatForwardInterpolator | LinearInterpolator | LogLinearInterpolator | LinearZeroRateInterpolator | NullInterpolator" -Leg: TypeAlias = "FixedLeg | FloatLeg | IndexFixedLeg | ZeroFloatLeg | ZeroFixedLeg | ZeroIndexLeg | CreditPremiumLeg | CreditProtectionLeg" -Period: TypeAlias = "FixedPeriod | FloatPeriod | Cashflow | IndexFixedPeriod | IndexCashflow | CreditPremiumPeriod | CreditProtectionPeriod" - -Security: TypeAlias = "FixedRateBond | FloatRateNote | Bill | IndexFixedRateBond" -FXOptionTypes: TypeAlias = ( - "FXCall | FXPut | FXRiskReversal | FXStraddle | FXStrangle | FXBrokerFly | FXOptionStrat" -) -RatesDerivative: TypeAlias = "IRS | SBS | FRA | ZCS | STIRFuture" -IndexDerivative: TypeAlias = "IIRS | ZCIS" -CurrencyDerivative: TypeAlias = "XCS | FXSwap | FXExchange" -Combinations: TypeAlias = "Portfolio | Fly | Spread | Value | VolValue" - -Instrument: TypeAlias = ( - "Combinations | Security | FXOptionTypes | RatesDerivative | CDS | CurrencyDerivative" -) +# NPV: TypeAlias = "DualTypes | dict[str, DualTypes]" +# + +# Leg: TypeAlias = "FixedLeg | FloatLeg | ZeroFloatLeg | ZeroFixedLeg | ZeroIndexLeg | CreditPremiumLeg | CreditProtectionLeg" +# Period: TypeAlias = "FixedPeriod | FloatPeriod | Cashflow | CreditPremiumPeriod | CreditProtectionPeriod" +# +# Security: TypeAlias = "FixedRateBond | FloatRateNote | Bill | IndexFixedRateBond" +# FXOptionTypes: TypeAlias = ( +# "FXCall | FXPut | FXRiskReversal | FXStraddle | FXStrangle | FXBrokerFly | FXOptionStrat" +# ) +# RatesDerivative: TypeAlias = "IRS | SBS | FRA | ZCS | STIRFuture" +# IndexDerivative: TypeAlias = "IIRS | ZCIS" +# CurrencyDerivative: TypeAlias = "XCS | FXSwap | FXForward" +# Combinations: TypeAlias = "Portfolio | Fly | Spread | Value | VolValue" +# +# Instrument: TypeAlias = ( +# "Combinations | Security | FXOptionTypes | RatesDerivative | CDS | CurrencyDerivative" +# ) class SupportsRate: @@ -225,7 +228,7 @@ def rate(self, *args: Any, **kwargs: Any) -> DualTypes: ... # type: ignore[empt class SupportsMetrics: def rate(self, *args: Any, **kwargs: Any) -> DualTypes: ... # type: ignore[empty-body] - def npv(self, *args: Any, **kwargs: Any) -> NPV: ... # type: ignore[empty-body] + def npv(self, *args: Any, **kwargs: Any) -> DualTypes | dict[str, DualTypes]: ... # type: ignore[empty-body] def delta(self, *args: Any, **kwargs: Any) -> DataFrame: ... # type: ignore[empty-body] def gamma(self, *args: Any, **kwargs: Any) -> DataFrame: ... # type: ignore[empty-body] def cashflows(self, *args: Any, **kwargs: Any) -> DataFrame: ... # type: ignore[empty-body] diff --git a/python/tests/instruments/test_instruments_bonds_legacy.py b/python/tests/instruments/test_instruments_bonds_legacy.py index 7bca4f4a..1658a518 100644 --- a/python/tests/instruments/test_instruments_bonds_legacy.py +++ b/python/tests/instruments/test_instruments_bonds_legacy.py @@ -1321,7 +1321,7 @@ def test_fixed_rate_bond_rate_raises(self) -> None: gilt.rate(curves=curve, metric="bad_metric") def test_fixed_rate_bond_no_amortization(self) -> None: - with pytest.raises(NotImplementedError, match="`amortization` for"): + with pytest.raises(TypeError, match="got an unexpected keyword argument 'amortization"): FixedRateBond( effective=dt(1998, 12, 7), termination=dt(2015, 12, 7), @@ -1835,7 +1835,7 @@ def test_fixed_rate_bond_zero_frequency_raises(self) -> None: ) def test_fixed_rate_bond_no_amortization(self) -> None: - with pytest.raises(NotImplementedError, match="`amortization` for"): + with pytest.raises(TypeError, match="got an unexpected keyword argument 'amortization"): IndexFixedRateBond( effective=dt(1998, 12, 7), termination=dt(2015, 12, 7), @@ -1865,7 +1865,13 @@ def test_fixed_rate_bond_rate_raises(self) -> None: ) curve = Curve({dt(1998, 12, 7): 1.0, dt(2015, 12, 7): 0.50}) with pytest.raises(ValueError, match="`metric` must be in"): - gilt.rate(curves=curve, metric="bad_metric") + gilt.rate( + curves=[ + Curve({dt(1992, 1, 1): 1.0, dt(2070, 1, 1): 0.13}, index_base=100.0), + curve, + ], + metric="bad_metric", + ) def test_initialisation_rate_metric(self) -> None: gilt = IndexFixedRateBond( @@ -2364,7 +2370,7 @@ def test_spec_kwargs(self) -> None: spec="us_gb_tsy", fixed_rate=0.75, notional=-100e6, - curves="sofr", + curves=["sofr", "sofr"], index_lag=3, index_method="monthly", index_base=251.01658, diff --git a/python/tests/instruments/test_instruments_legacy.py b/python/tests/instruments/test_instruments_legacy.py index 174b567a..7896371f 100644 --- a/python/tests/instruments/test_instruments_legacy.py +++ b/python/tests/instruments/test_instruments_legacy.py @@ -650,37 +650,73 @@ class TestNullPricing: curves=["usdusd", "usdusd", "eureur", "eureur"], notional=-1e6, ), - # FXSwap( - # dt(2022, 7, 1), - # "3M", - # "A", - # currency="eur", - # leg2_currency="usd", - # curves=["eureur", "eureur", "usdusd", "usdusd"], - # notional=1e6, - # fx_fixing=0.999851, - # split_notional=1003052.812, - # points=2.523505, - # ), + ], + ) + def test_null_priced_delta(self, inst) -> None: + c1 = Curve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.99}, id="usdusd") + c2 = Curve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.98}, id="eureur") + c3 = Curve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.982}, id="eurusd") + c4 = Curve( + {dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.995}, + id="eu_cpi", + index_base=100.0, + interpolation="linear_index", + index_lag=3, + ) + fxf = FXForwards( + FXRates({"eurusd": 1.0}, settlement=dt(2022, 1, 1)), + {"usdusd": c1, "eureur": c2, "eurusd": c3}, + ) + ins = [ + IRS(dt(2022, 1, 1), "1y", "A", curves="eureur"), + IRS(dt(2022, 1, 1), "1y", "A", curves="usdusd"), + IRS(dt(2022, 1, 1), "1y", "A", curves="eurusd"), + ZCIS(dt(2022, 1, 1), "1y", "A", curves=["eureur", "eureur", "eu_cpi", "eureur"]), + ] + solver = Solver( + curves=[c1, c2, c3, c4, fxf.curve("usd", "eur", "usdeur")], + instruments=ins, + s=[1.2, 1.3, 1.33, 0.5], + id="solver", + instrument_labels=["eur 1y", "usd 1y", "eur 1y xcs adj.", "1y cpi"], + fx=fxf, + ) + result = inst.delta(solver=solver) + assert abs(result.iloc[0, 0] - 25.0) < 1.0 + result2 = inst.npv(solver=solver) + assert abs(result2) < 1e-3 + + # test that instruments have not been set by the previous pricing action + solver.s = [1.3, 1.4, 1.36, 0.55] + solver.iterate() + result3 = inst.npv(solver=solver) + assert abs(result3) < 1e-3 + + @pytest.mark.parametrize( + "inst", + [ FXSwap( dt(2022, 7, 1), "3M", - pair="usdeur", - curves=["usdusd", "eurusd"], - notional=1e6, - # fx_fixing=0.999851, - # split_notional=1003052.812, - # points=2.523505, + pair="eurusd", + curves=["eureur", "usdeur"], + notional=-1e6, + leg2_fx_fixings=0.999851, + split_notional=-1003052.812, + points=-0.756443, ), FXForward( settlement=dt(2022, 10, 1), pair="eurusd", - curves=[None, "eureur", None, "usdusd"], + curves=["eureur", "usdeur"], notional=-1e6 * 25 / 74.27, ), ], ) - def test_null_priced_delta(self, inst) -> None: + def test_instruments_that_cannot_be_set_to_mid_market_if_null_priced(self, inst) -> None: + # These instruments behave differently when they have no pricing parameters with regards + # to risk becuase they cannot have FXFixings set, otherwise it breaks the fixings + # calculation (or each call manually requires a reset fixing process) c1 = Curve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.99}, id="usdusd") c2 = Curve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.98}, id="eureur") c3 = Curve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.982}, id="eurusd") @@ -702,7 +738,7 @@ def test_null_priced_delta(self, inst) -> None: ZCIS(dt(2022, 1, 1), "1y", "A", curves=["eureur", "eureur", "eu_cpi", "eureur"]), ] solver = Solver( - curves=[c1, c2, c3, c4], + curves=[c1, c2, c3, c4, fxf.curve("usd", "eur", "usdeur")], instruments=ins, s=[1.2, 1.3, 1.33, 0.5], id="solver", @@ -714,11 +750,11 @@ def test_null_priced_delta(self, inst) -> None: result2 = inst.npv(solver=solver) assert abs(result2) < 1e-3 - # test that instruments have not been set by the previous pricing action - solver.s = [1.3, 1.4, 1.36, 0.55] - solver.iterate() - result3 = inst.npv(solver=solver) - assert abs(result3) < 1e-3 + # # test that instruments have not been set by the previous pricing action + # solver.s = [1.3, 1.4, 1.36, 0.55] + # solver.iterate() + # result3 = inst.npv(solver=solver) + # assert abs(result3) < 1e-3 @pytest.mark.parametrize( "inst", @@ -1114,10 +1150,10 @@ def test_null_priced_delta_round_trip_one_pricing_param_fx_fix(self, inst, param pair="usdeur", notional=-1e6 * 25 / 74.27, ), - NDF( - pair="eurusd", # settlement currency defaults to right hand side: usd - settlement=dt(2022, 10, 1), - ), + # NDF( + # pair="eurusd", # settlement currency defaults to right hand side: usd + # settlement=dt(2022, 10, 1), + # ), ], ) def test_set_pricing_does_not_overwrite_unpriced_status(self, inst): @@ -5118,10 +5154,11 @@ def test_spec_overwrites(self) -> None: leg2_fixing_series=NoInput(0), leg2_fixing_frequency=NoInput(0), curves=_Curves(disc_curve="test", leg2_rate_curve="test", leg2_disc_curve="test"), + vol=_Vol(), ) kwargs = _KWArgs( user_args=expected, - meta_args=["curves"], + meta_args=["curves", "vol"], ) assert irs.kwargs.meta == kwargs.meta assert irs.kwargs.leg1 == kwargs.leg1 @@ -5712,7 +5749,7 @@ def test_fx_call_cashflows_table(self, fxfo) -> None: strike=1.101, ) curves = [fxfo.curve("eur", "usd"), fxfo.curve("usd", "usd")] - result = fxo.cashflows_table(curves, fx=fxfo, vol=8.9) + result = fxo.cashflows_table(curves=curves, fx=fxfo, vol=8.9) expected = DataFrame( data=[[0.0]], index=Index([dt(2023, 6, 20)], name="payment"), @@ -7605,6 +7642,25 @@ def test_repr(self): expected = f"" assert v.__repr__() == expected + def test_sabr_surface(self): + fxss = FXSabrSurface( + expiries=[dt(2000, 6, 1), dt(2000, 9, 1)], + node_values=[[0.1, 1.0, 0.01, 0.01], [0.11, 1.0, 0.01, 0.01]], + eval_date=dt(2000, 1, 1), + pair="eurusd", + ) + fxvv = FXVolValue(index_value=1.1, expiry=dt(2000, 8, 1)) + fxf = FXForwards( + fx_rates=FXRates({"eurusd": 1.15}, settlement=dt(2000, 1, 4)), + fx_curves={ + "eureur": Curve({dt(2000, 1, 1): 1.0, dt(2001, 1, 1): 0.95}), + "eurusd": Curve({dt(2000, 1, 1): 1.0, dt(2001, 1, 1): 0.951}), + "usdusd": Curve({dt(2000, 1, 1): 1.0, dt(2001, 1, 1): 0.94}), + }, + ) + result = fxvv.rate(vol=fxss, fx=fxf) + assert abs(result - 10.767884) < 1e-5 + @pytest.mark.parametrize( "inst", diff --git a/python/tests/periods/test_periods_legacy.py b/python/tests/periods/test_periods_legacy.py index 042271d3..124792d6 100644 --- a/python/tests/periods/test_periods_legacy.py +++ b/python/tests/periods/test_periods_legacy.py @@ -1273,6 +1273,7 @@ def test_ibor_fixing_table_substitute(self, line_curve, curve) -> None: result = float_period.local_analytic_rate_fixings(rate_curve=line_curve, disc_curve=curve) assert abs(result.iloc[0, 0] + 24.402790080357686) < 1e-10 + @pytest.mark.skip(reason="`right` removed by v2.5") def test_ibor_fixing_table_right(self, line_curve, curve) -> None: float_period = FloatPeriod( start=dt(2022, 1, 4), @@ -1878,6 +1879,7 @@ def test_rfr_fixings_table(self, curve, meth, exp) -> None: fixings.pop(f"{name}_1B") assert_frame_equal(result, exp) + @pytest.mark.skip(reason="`right` removed by v2.5") @pytest.mark.parametrize( ("right", "exp"), [ @@ -1907,6 +1909,7 @@ def test_rfr_fixings_table_right(self, curve, right, exp) -> None: assert isinstance(result, DataFrame) assert len(result.index) == exp + @pytest.mark.skip(reason="`right` removed by v2.5") def test_rfr_fixings_table_right_non_bus_day(self) -> None: curve = Curve({dt(2022, 1, 1): 1.0, dt(2022, 11, 19): 0.98}, calendar="tgt") float_period = FloatPeriod( @@ -2499,6 +2502,7 @@ def test_ibor_stub_fixings_rfr_in_dict_ignored_substitute(self) -> None: assert abs(result.iloc[0, 0] + 8.0601) < 1e-4 assert abs(result.iloc[0, 1] + 8.32877) < 1e-4 + @pytest.mark.skip(reason="`right` removed by v2.5") def test_ibor_stub_fixings_table_right(self) -> None: period = FloatPeriod( start=dt(2023, 2, 1), diff --git a/python/tests/test_solver.py b/python/tests/test_solver.py index e171522e..0f135d87 100644 --- a/python/tests/test_solver.py +++ b/python/tests/test_solver.py @@ -1209,7 +1209,7 @@ def test_delta_irs_guide() -> None: fixed_rate=6.0, curves="sofr", ) - result = irs.delta(solver=usd_solver, base="usd") # local overrides base to USD + result = irs.delta(solver=usd_solver) # local overrides base to USD # result = irs.delta(solver=usd_solver, base="eur", local=True) # local overrides base to USD expected = DataFrame( [[0], [16.77263], [32.60487]], @@ -1533,7 +1533,7 @@ def test_solver_gamma_pnl_explain() -> None: ) assert_frame_equal(delta_base, expected_delta, atol=1e-2, rtol=1e-4) - gamma_base = pf.gamma(solver=solver, base="usd", local=True) # local overrrides base to EUR + gamma_base = pf.gamma(solver=solver, base="eur") expected_gamma = DataFrame( data=[ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], @@ -1569,7 +1569,9 @@ def test_solver_gamma_pnl_explain() -> None: names=["type", "solver", "label"], ), ) - assert_frame_equal(gamma_base, expected_gamma, atol=1e-2, rtol=1e-4) + assert_frame_equal( + gamma_base.loc[("all", "eur")], expected_gamma.loc[("eur", "eur")], atol=1e-2, rtol=1e-4 + ) def test_gamma_with_fxrates_ad_order_1_raises() -> None: