From c2e0e9d5a82942b57e323e3ac03dc443a02fa8e5 Mon Sep 17 00:00:00 2001 From: JHM Darbyshire <24256554+attack68@users.noreply.github.com> Date: Wed, 2 Oct 2024 09:38:50 +0100 Subject: [PATCH] REF: move instruments to folders (#423) --- python/rateslib/calendars/__init__.py | 3 +- python/rateslib/instruments/__init__.py | 3732 +---------------- python/rateslib/instruments/generics.py | 254 +- .../rateslib/instruments/rates_derivatives.py | 2420 +++++++++++ .../rateslib/instruments/rates_multi_ccy.py | 1131 +++++ 5 files changed, 3824 insertions(+), 3716 deletions(-) create mode 100644 python/rateslib/instruments/rates_derivatives.py create mode 100644 python/rateslib/instruments/rates_multi_ccy.py diff --git a/python/rateslib/calendars/__init__.py b/python/rateslib/calendars/__init__.py index 5f5579fe..c6b90157 100644 --- a/python/rateslib/calendars/__init__.py +++ b/python/rateslib/calendars/__init__.py @@ -216,7 +216,8 @@ def add_tenor( from rateslib.dual import Dual, Dual2 from rateslib.periods import FixedPeriod, FloatPeriod, Cashflow, IndexFixedPeriod, IndexCashflow from rateslib.legs import FixedLeg, FloatLeg, CustomLeg, FloatLegMtm, FixedLegMtm, IndexFixedLeg, ZeroFixedLeg, ZeroFloatLeg, ZeroIndexLeg - from rateslib.instruments import FixedRateBond, FloatRateNote, Value, IRS, SBS, FRA, forward_fx, Spread, Fly, BondFuture, Bill, ZCS, FXSwap, ZCIS, IIRS, STIRFuture + from rateslib.instruments import FixedRateBond, FloatRateNote, Value, IRS, SBS, FRA, Spread, Fly, BondFuture, Bill, ZCS, FXSwap, ZCIS, IIRS, STIRFuture + from rateslib.fx import forward_fx from rateslib.solver import Solver from rateslib.splines import bspldnev_single, PPSpline from datetime import datetime as dt diff --git a/python/rateslib/instruments/__init__.py b/python/rateslib/instruments/__init__.py index bc848056..856e1617 100644 --- a/python/rateslib/instruments/__init__.py +++ b/python/rateslib/instruments/__init__.py @@ -9,15 +9,12 @@ from __future__ import annotations -import abc import warnings -from abc import ABCMeta, abstractmethod from datetime import datetime, timedelta from functools import partial -from typing import NoReturn import numpy as np -from pandas import DataFrame, MultiIndex, Series +from pandas import DataFrame, Series # from scipy.optimize import brentq from pandas.tseries.offsets import CustomBusinessDay @@ -31,9 +28,8 @@ ) from rateslib.curves import Curve, IndexCurve, LineCurve, average_rate, index_left from rateslib.default import NoInput, _drb -from rateslib.dual import Dual, Dual2, DualTypes, dual_log, gradient -from rateslib.fx import FXForwards, FXRates, forward_fx -from rateslib.fx_volatility import FXDeltaVolSmile +from rateslib.dual import Dual, Dual2, DualTypes, gradient +from rateslib.fx import FXForwards, FXRates from rateslib.instruments.bonds import ( BILL_MODE_MAP, BOND_MODE_MAP, @@ -46,13 +42,8 @@ Sensitivities, _get, _get_curves_fx_and_base_maybe_from_solver, - _get_vol_maybe_from_solver, - _inherit_or_negate, - _lower, _push, - _update_not_noinput, _update_with_defaults, - _upper, ) from rateslib.instruments.fx_volatility import ( FXBrokerFly, @@ -64,22 +55,30 @@ FXStraddle, FXStrangle, ) -from rateslib.instruments.generics import Fly, Portfolio, Spread +from rateslib.instruments.generics import Fly, Portfolio, Spread, Value, VolValue +from rateslib.instruments.rates_derivatives import ( + FRA, + IIRS, + IRS, + SBS, + ZCIS, + ZCS, + BaseDerivative, + STIRFuture, +) +from rateslib.instruments.rates_multi_ccy import ( + XCS, + FXExchange, + FXSwap, +) from rateslib.legs import ( FixedLeg, - FixedLegMtm, FloatLeg, - FloatLegMtm, IndexFixedLeg, - ZeroFixedLeg, - ZeroFloatLeg, - ZeroIndexLeg, ) from rateslib.periods import ( - Cashflow, FloatPeriod, IndexMixin, - _disc_from_curve, _disc_maybe_from_curve, _get_fx_and_base, _maybe_local, @@ -94,441 +93,6 @@ # Contact rateslib at gmail.com if this code is observed outside its intended sphere. -class Value(BaseMixin): - """ - A null *Instrument* which can be used within a :class:`~rateslib.solver.Solver` - to directly parametrise a *Curve* node, via some calculated value. - - Parameters - ---------- - effective : datetime - The datetime index for which the `rate`, which is just the curve value, is - returned. - curves : Curve, LineCurve, str or list of such, optional - A single :class:`~rateslib.curves.Curve`, - :class:`~rateslib.curves.LineCurve` or id or a - list of such. Only uses the first *Curve* in a list. - convention : str, optional, - Day count convention used with certain ``metric``. - metric : str in {"curve_value", "index_value", "cc_zero_rate"}, optional - Configures which value to extract from the *Curve*. - - Examples - -------- - The below :class:`~rateslib.curves.Curve` is solved directly - from a calibrating DF value on 1st Nov 2022. - - .. ipython:: python - - curve = Curve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 1.0}, id="v") - instruments = [(Value(dt(2022, 11, 1)), (curve,), {})] - solver = Solver([curve], [], instruments, [0.99]) - curve[dt(2022, 1, 1)] - curve[dt(2022, 11, 1)] - curve[dt(2023, 1, 1)] - """ - - def __init__( - self, - effective: datetime, - convention: str | NoInput = NoInput(0), - metric: str = "curve_value", - curves: list | str | Curve | None = None, - ): - self.effective = effective - self.curves = curves - self.convention = defaults.convention if convention is NoInput.blank else convention - self.metric = metric.lower() - - def rate( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - metric: str | NoInput = NoInput(0), - ): - """ - Return a value derived from a *Curve*. - - Parameters - ---------- - curves : Curve, LineCurve, str or list of such - Uses only one *Curve*, the one given or the first in the list. - solver : Solver, optional - The numerical :class:`~rateslib.solver.Solver` that constructs - ``Curves`` from calibrating instruments. - fx : float, FXRates, FXForwards, optional - Not used. - base : str, optional - Not used. - metric: str in {"curve_value", "index_value", "cc_zero_rate"}, optional - Configures which type of value to return from the applicable *Curve*. - - Returns - ------- - float, Dual, Dual2 - - """ - curves, _, _ = _get_curves_fx_and_base_maybe_from_solver( - self.curves, - solver, - curves, - NoInput(0), - NoInput(0), - "_", - ) - metric = self.metric if metric is NoInput.blank else metric.lower() - if metric == "curve_value": - return curves[0][self.effective] - elif metric == "cc_zero_rate": - if curves[0]._base_type != "dfs": - raise TypeError( - "`curve` used with `metric`='cc_zero_rate' must be discount factor based.", - ) - dcf_ = dcf(curves[0].node_dates[0], self.effective, self.convention) - _ = (dual_log(curves[0][self.effective]) / -dcf_) * 100 - return _ - elif metric == "index_value": - if not isinstance(curves[0], IndexCurve): - raise TypeError("`curve` used with `metric`='index_value' must be type IndexCurve.") - _ = curves[0].index_value(self.effective) - return _ - raise ValueError("`metric`must be in {'curve_value', 'cc_zero_rate', 'index_value'}.") - - def npv(self, *args, **kwargs) -> NoReturn: - raise NotImplementedError("`Value` instrument has no concept of NPV.") - - def cashflows(self, *args, **kwargs) -> NoReturn: - raise NotImplementedError("`Value` instrument has no concept of cashflows.") - - def analytic_delta(self, *args, **kwargs) -> NoReturn: - raise NotImplementedError("`Value` instrument has no concept of analytic delta.") - - -class VolValue(BaseMixin): - """ - A null *Instrument* which can be used within a :class:`~rateslib.solver.Solver` - to directly parametrise a *Vol* node, via some calculated metric. - - Parameters - ---------- - index_value : float, Dual, Dual2 - The value of some index to the *VolSmile* or *VolSurface*. - metric: str, optional - The default metric to return from the ``rate`` method. - vol: str, FXDeltaVolSmile, optional - The associated object from which to determine the ``rate``. - - Examples - -------- - The below :class:`~rateslib.fx_volatility.FXDeltaVolSmile` is solved directly - from calibrating volatility values. - - .. ipython:: python - :suppress: - - from rateslib.fx_volatility import FXDeltaVolSmile - from rateslib.instruments import VolValue - from rateslib.solver import Solver - - .. ipython:: python - - smile = FXDeltaVolSmile( - nodes={0.25: 10.0, 0.5: 10.0, 0.75: 10.0}, - eval_date=dt(2023, 3, 16), - expiry=dt(2023, 6, 16), - delta_type="forward", - id="VolSmile", - ) - instruments = [ - VolValue(0.25, vol="VolSmile"), - VolValue(0.5, vol="VolSmile"), - VolValue(0.75, vol=smile) - ] - solver = Solver(curves=[smile], instruments=instruments, s=[8.9, 7.8, 9.9]) - smile[0.25] - smile[0.5] - smile[0.75] - """ - - def __init__( - self, - index_value: DualTypes, - # index_type: str = "delta", - # delta_type: str = NoInput(0), - metric: str = "vol", - vol: NoInput | str | FXDeltaVolSmile = NoInput(0), - ): - self.index_value = index_value - # self.index_type = index_type - # self.delta_type = delta_type - self.vol = vol - self.curves = NoInput(0) - self.metric = metric.lower() - - def rate( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - vol: DualTypes | FXDeltaVolSmile = NoInput(0), - metric: str = "vol", - ): - """ - Return a value derived from a *Curve*. - - Parameters - ---------- - curves : Curve, LineCurve, str or list of such - Uses only one *Curve*, the one given or the first in the list. - solver : Solver, optional - The numerical :class:`~rateslib.solver.Solver` that constructs - ``Curves`` from calibrating instruments. - fx : float, FXRates, FXForwards, optional - Not used. - base : str, optional - Not used. - metric: str in {"curve_value", "index_value", "cc_zero_rate"}, optional - Configures which type of value to return from the applicable *Curve*. - - Returns - ------- - float, Dual, Dual2 - - """ - curves, fx, base = _get_curves_fx_and_base_maybe_from_solver( - self.curves, - solver, - curves, - fx, - base, - "_", - ) - vol = _get_vol_maybe_from_solver(self.vol, vol, solver) - metric = self.metric if metric is NoInput.blank else metric.lower() - - if metric == "vol": - return vol[self.index_value] - - raise ValueError("`metric` must be in {'vol'}.") - - def npv(self, *args, **kwargs) -> NoReturn: - raise NotImplementedError("`VolValue` instrument has no concept of NPV.") - - def cashflows(self, *args, **kwargs) -> NoReturn: - raise NotImplementedError("`VolValue` instrument has no concept of cashflows.") - - def analytic_delta(self, *args, **kwargs) -> NoReturn: - raise NotImplementedError("`VolValue` instrument has no concept of analytic delta.") - - -class FXExchange(Sensitivities, BaseMixin): - """ - Create a simple exchange of two currencies. - - Parameters - ---------- - settlement : datetime - The date of the currency exchange. - pair: str - The curreny pair of the exchange, e.g. "eurusd", using 3-digit iso codes. - fx_rate : float, optional - The FX rate used to derive the notional exchange on *Leg2*. - notional : float - The cashflow amount of the LHS currency. - curves : Curve, LineCurve, str or list of such, optional - For *FXExchange* only discounting curves are required in each currency and not rate - forecasting curves. - The signature should be: `[None, eur_curve, None, usd_curve]` for a "eurusd" pair. - """ - - def __init__( - self, - settlement: datetime, - pair: str, - fx_rate: float | NoInput = NoInput(0), - notional: float | NoInput = NoInput(0), - curves: list | str | Curve | NoInput = NoInput(0), - ): - self.curves = curves - self.settlement = settlement - self.pair = pair.lower() - self.leg1 = Cashflow( - notional=-defaults.notional if notional is NoInput.blank else -notional, - currency=self.pair[0:3], - payment=settlement, - stub_type="Exchange", - rate=NoInput(0), - ) - self.leg2 = Cashflow( - notional=1.0, # will be determined by setting fx_rate - currency=self.pair[3:6], - payment=settlement, - stub_type="Exchange", - rate=fx_rate, - ) - self.fx_rate = fx_rate - - @property - def fx_rate(self): - return self._fx_rate - - @fx_rate.setter - def fx_rate(self, value): - self._fx_rate = value - self.leg2.notional = 0.0 if value is NoInput.blank else value * -self.leg1.notional - self.leg2._rate = value - - def _set_pricing_mid( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - ): - if self.fx_rate is NoInput.blank: - mid_market_rate = self.rate(curves, solver, fx) - self.fx_rate = float(mid_market_rate) - self._fx_rate = NoInput(0) - - def npv( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - local: bool = False, - ): - """ - Return the NPV of the *FXExchange* by summing legs. - - For arguments see :meth:`BaseMixin.npv` - """ - self._set_pricing_mid(curves, solver, fx) - - curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( - self.curves, - solver, - curves, - fx, - base, - self.leg1.currency, - ) - - if fx_ is NoInput.blank: - raise ValueError( - "Must have some FX information to price FXExchange, either `fx` or " - "`solver` containing an FX object.", - ) - if not isinstance(fx_, (FXRates, FXForwards)): - # force base_ leg1 currency to be converted consistent. - leg1_npv = self.leg1.npv(curves[0], curves[1], fx_, base_, local) - leg2_npv = self.leg2.npv(curves[2], curves[3], 1.0, base_, local) - warnings.warn( - "When valuing multi-currency derivatives it not best practice to " - "supply `fx` as numeric.\nYour input:\n" - f"`npv(solver={'None' if solver is NoInput.blank else ''}, " - f"fx={fx}, base='{base if base is not NoInput.blank else 'None'}')\n" - "has been implicitly converted into the following by this operation:\n" - f"`npv(solver={'None' if solver is NoInput.blank else ''}, " - f"fx=FXRates({{'{self.leg2.currency}{self.leg1.currency}: {fx}}}), " - f"base='{self.leg2.currency}')\n.", - UserWarning, - ) - else: - leg1_npv = self.leg1.npv(curves[0], curves[1], fx_, base_, local) - leg2_npv = self.leg2.npv(curves[2], curves[3], fx_, base_, local) - - if local: - return { - k: leg1_npv.get(k, 0) + leg2_npv.get(k, 0) for k in set(leg1_npv) | set(leg2_npv) - } - else: - return leg1_npv + leg2_npv - - def cashflows( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - ): - """ - Return the cashflows of the *FXExchange* by aggregating legs. - - For arguments see :meth:`BaseMixin.npv` - """ - self._set_pricing_mid(curves, solver, fx) - curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( - self.curves, - solver, - curves, - fx, - base, - NoInput(0), - ) - seq = [ - self.leg1.cashflows(curves[0], curves[1], fx_, base_), - self.leg2.cashflows(curves[2], curves[3], fx_, base_), - ] - _ = DataFrame.from_records(seq) - _.index = MultiIndex.from_tuples([("leg1", 0), ("leg2", 0)]) - return _ - - def rate( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - ): - """ - Return the mid-market rate of the instrument. - - For arguments see :meth:`BaseMixin.rate` - """ - curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( - self.curves, - solver, - curves, - fx, - base, - self.leg1.currency, - ) - if isinstance(fx_, (FXRates, FXForwards)): - imm_fx = fx_.rate(self.pair) - else: - imm_fx = fx_ - - if imm_fx is NoInput.blank: - raise ValueError( - "`fx` must be supplied to price FXExchange object.\n" - "Note: it can be attached to and then gotten from a Solver.", - ) - _ = forward_fx(self.settlement, curves[1], curves[3], imm_fx) - return _ - - def delta(self, *args, **kwargs): - """ - Calculate the delta of the *Instrument*. - - For arguments see :meth:`Sensitivities.delta()`. - """ - return super().delta(*args, **kwargs) - - def gamma(self, *args, **kwargs): - """ - Calculate the gamma of the *Instrument*. - - For arguments see :meth:`Sensitivities.gamma()`. - """ - return super().gamma(*args, **kwargs) - - def analytic_delta(self, *args, **kwargs) -> NoReturn: - raise NotImplementedError("`analytic_delta` for FXExchange not defined.") - - # Securities @@ -4141,3263 +3705,7 @@ def gamma(self, *args, **kwargs): return super().gamma(*args, **kwargs) -class BaseDerivative(Sensitivities, BaseMixin, metaclass=ABCMeta): - """ - Abstract base class with common parameters for many *Derivative* subclasses. - - Parameters - ---------- - effective : datetime - The adjusted or unadjusted effective date. - termination : datetime or str - The adjusted or unadjusted termination date. If a string, then a tenor must be - given expressed in days (`"D"`), months (`"M"`) or years (`"Y"`), e.g. `"48M"`. - frequency : str in {"M", "B", "Q", "T", "S", "A", "Z"}, optional - The frequency of the schedule. - stub : str combining {"SHORT", "LONG"} with {"FRONT", "BACK"}, optional - The stub type to enact on the swap. Can provide two types, for - example "SHORTFRONTLONGBACK". - front_stub : datetime, optional - An adjusted or unadjusted date for the first stub period. - back_stub : datetime, optional - An adjusted or unadjusted date for the back stub period. - See notes for combining ``stub``, ``front_stub`` and ``back_stub`` - and any automatic stub inference. - roll : int in [1, 31] or str in {"eom", "imm", "som"}, optional - The roll day of the schedule. Inferred if not given. - eom : bool, optional - Use an end of month preference rather than regular rolls for inference. Set by - default. Not required if ``roll`` is specified. - modifier : str, optional - The modification rule, in {"F", "MF", "P", "MP"} - calendar : calendar or str, optional - The holiday calendar object to use. If str, looks up named calendar from - static data. - payment_lag : int, optional - The number of business days to lag payments by. - notional : float, optional - The leg notional, which is applied to each period. - amortization: float, optional - The amount by which to adjust the notional each successive period. Should have - sign equal to that of notional if the notional is to reduce towards zero. - convention: str, optional - The day count convention applied to calculations of period accrual dates. - See :meth:`~rateslib.calendars.dcf`. - leg2_kwargs: Any - All ``leg2`` arguments can be similarly input as above, e.g. ``leg2_frequency``. - If **not** given, any ``leg2`` - argument inherits its value from the ``leg1`` arguments, except in the case of - ``notional`` and ``amortization`` where ``leg2`` inherits the negated value. - curves : Curve, LineCurve, str or list of such, optional - A single :class:`~rateslib.curves.Curve`, - :class:`~rateslib.curves.LineCurve` or id or a - list of such. A list defines the following curves in the order: - - - Forecasting :class:`~rateslib.curves.Curve` or - :class:`~rateslib.curves.LineCurve` for ``leg1``. - - Discounting :class:`~rateslib.curves.Curve` for ``leg1``. - - Forecasting :class:`~rateslib.curves.Curve` or - :class:`~rateslib.curves.LineCurve` for ``leg2``. - - Discounting :class:`~rateslib.curves.Curve` for ``leg2``. - spec : str, optional - An identifier to pre-populate many field with conventional values. See - :ref:`here` for more info and available values. - - Attributes - ---------- - effective : datetime - termination : datetime - frequency : str - stub : str - front_stub : datetime - back_stub : datetime - roll : str, int - eom : bool - modifier : str - calendar : Calendar - payment_lag : int - notional : float - amortization : float - convention : str - leg2_effective : datetime - leg2_termination : datetime - leg2_frequency : str - leg2_stub : str - leg2_front_stub : datetime - leg2_back_stub : datetime - leg2_roll : str, int - leg2_eom : bool - leg2_modifier : str - leg2_calendar : Calendar - leg2_payment_lag : int - leg2_notional : float - leg2_amortization : float - leg2_convention : str - """ - - @abc.abstractmethod - def __init__( - self, - effective: datetime | NoInput = NoInput(0), - termination: datetime | str | NoInput = NoInput(0), - frequency: int | NoInput = NoInput(0), - stub: str | NoInput = NoInput(0), - front_stub: datetime | NoInput = NoInput(0), - back_stub: datetime | NoInput = NoInput(0), - roll: str | int | NoInput = NoInput(0), - eom: bool | NoInput = NoInput(0), - modifier: str | NoInput = NoInput(0), - calendar: CustomBusinessDay | str | NoInput = NoInput(0), - payment_lag: int | NoInput = NoInput(0), - notional: float | NoInput = NoInput(0), - currency: str | NoInput = NoInput(0), - amortization: float | NoInput = NoInput(0), - convention: str | NoInput = NoInput(0), - leg2_effective: datetime | NoInput = NoInput(1), - leg2_termination: datetime | str | NoInput = NoInput(1), - leg2_frequency: int | NoInput = NoInput(1), - leg2_stub: str | NoInput = NoInput(1), - leg2_front_stub: datetime | NoInput = NoInput(1), - leg2_back_stub: datetime | NoInput = NoInput(1), - leg2_roll: str | int | NoInput = NoInput(1), - leg2_eom: bool | NoInput = NoInput(1), - leg2_modifier: str | NoInput = NoInput(1), - leg2_calendar: CustomBusinessDay | str | NoInput = NoInput(1), - leg2_payment_lag: int | NoInput = NoInput(1), - leg2_notional: float | NoInput = NoInput(-1), - leg2_currency: str | NoInput = NoInput(1), - leg2_amortization: float | NoInput = NoInput(-1), - leg2_convention: str | NoInput = NoInput(1), - curves: list | str | Curve | NoInput = NoInput(0), - spec: str | NoInput = NoInput(0), - ): - self.kwargs = dict( - effective=effective, - termination=termination, - frequency=frequency, - stub=stub, - front_stub=front_stub, - back_stub=back_stub, - roll=roll, - eom=eom, - modifier=modifier, - calendar=calendar, - payment_lag=payment_lag, - notional=notional, - currency=currency, - amortization=amortization, - convention=convention, - leg2_effective=leg2_effective, - leg2_termination=leg2_termination, - leg2_frequency=leg2_frequency, - leg2_stub=leg2_stub, - leg2_front_stub=leg2_front_stub, - leg2_back_stub=leg2_back_stub, - leg2_roll=leg2_roll, - leg2_eom=leg2_eom, - leg2_modifier=leg2_modifier, - leg2_calendar=leg2_calendar, - leg2_payment_lag=leg2_payment_lag, - leg2_notional=leg2_notional, - leg2_currency=leg2_currency, - leg2_amortization=leg2_amortization, - leg2_convention=leg2_convention, - ) - self.kwargs = _push(spec, self.kwargs) - # set some defaults if missing - self.kwargs["notional"] = ( - defaults.notional - if self.kwargs["notional"] is NoInput.blank - else self.kwargs["notional"] - ) - if self.kwargs["payment_lag"] is NoInput.blank: - self.kwargs["payment_lag"] = defaults.payment_lag_specific[type(self).__name__] - self.kwargs = _inherit_or_negate(self.kwargs) # inherit or negate the complete arg list - - self.curves = curves - self.spec = spec - - # - # for attribute in [ - # "effective", - # "termination", - # "frequency", - # "stub", - # "front_stub", - # "back_stub", - # "roll", - # "eom", - # "modifier", - # "calendar", - # "payment_lag", - # "convention", - # "notional", - # "amortization", - # "currency", - # ]: - # leg2_val, val = self.kwargs[f"leg2_{attribute}"], self.kwargs[attribute] - # if leg2_val is NoInput.inherit: - # _ = val - # elif leg2_val == NoInput.negate: - # _ = NoInput(0) if val is NoInput(0) else val * -1 - # else: - # _ = leg2_val - # self.kwargs[attribute] = val - # self.kwargs[f"leg2_{attribute}"] = _ - # # setattr(self, attribute, val) - # # setattr(self, f"leg2_{attribute}", _) - - @abstractmethod - def _set_pricing_mid(self, *args, **kwargs): # pragma: no cover - pass - - def delta(self, *args, **kwargs): - """ - Calculate the delta of the *Instrument*. - - For arguments see :meth:`Sensitivities.delta()`. - """ - return super().delta(*args, **kwargs) - - def gamma(self, *args, **kwargs): - """ - Calculate the gamma of the *Instrument*. - - For arguments see :meth:`Sensitivities.gamma()`. - """ - return super().gamma(*args, **kwargs) - - -class IRS(BaseDerivative): - """ - Create an interest rate swap composing a :class:`~rateslib.legs.FixedLeg` - and a :class:`~rateslib.legs.FloatLeg`. - - Parameters - ---------- - args : dict - Required positional args to :class:`BaseDerivative`. - fixed_rate : float or None - The fixed rate applied to the :class:`~rateslib.legs.FixedLeg`. If `None` - will be set to mid-market when curves are provided. - leg2_float_spread : float, optional - The spread applied to the :class:`~rateslib.legs.FloatLeg`. Can be set to - `None` and designated - later, perhaps after a mid-market spread for all periods has been calculated. - leg2_spread_compound_method : str, optional - The method to use for adding a floating spread to compounded rates. Available - options are `{"none_simple", "isda_compounding", "isda_flat_compounding"}`. - leg2_fixings : float, list, or Series optional - If a float scalar, will be applied as the determined fixing for the first - period. If a list of *n* fixings will be used as the fixings for the first *n* - periods. If any sublist of length *m* is given, is used as the first *m* RFR - fixings for that :class:`~rateslib.periods.FloatPeriod`. If a datetime - indexed ``Series`` will use the fixings that are available in that object, - and derive the rest from the ``curve``. - leg2_fixing_method : str, optional - The method by which floating rates are determined, set by default. See notes. - leg2_method_param : int, optional - A parameter that is used for the various ``fixing_method`` s. See notes. - kwargs : dict - Required keyword arguments to :class:`BaseDerivative`. - - Notes - ------ - The various different ``leg2_fixing_methods``, which describe how an - individual *FloatPeriod* calculates its *rate*, are - fully documented in the notes for the :class:`~rateslib.periods.FloatPeriod`. - These configurations provide the mechanics to differentiate between IBOR swaps, and - OISs with different mechanisms such as *payment delay*, *observation shift*, - *lockout*, and/or *averaging*. - Similarly some information is provided in that same link regarding - ``leg2_fixings``, but a cookbook article is also produced for - :ref:`working with fixings `. - - Examples - -------- - Construct a curve to price the example. - - .. ipython:: python - - usd = Curve( - nodes={ - dt(2022, 1, 1): 1.0, - dt(2023, 1, 1): 0.965, - dt(2024, 1, 1): 0.94 - }, - id="usd" - ) - - Create the IRS, and demonstrate the :meth:`~rateslib.instruments.IRS.rate`, - :meth:`~rateslib.instruments.IRS.npv`, - :meth:`~rateslib.instruments.IRS.analytic_delta`, and - :meth:`~rateslib.instruments.IRS.spread`. - - .. ipython:: python - - irs = IRS( - effective=dt(2022, 1, 1), - termination="18M", - frequency="A", - calendar="nyc", - currency="usd", - fixed_rate=3.269, - convention="Act360", - notional=100e6, - curves=["usd"], - ) - irs.rate(curves=usd) - irs.npv(curves=usd) - irs.analytic_delta(curve=usd) - irs.spread(curves=usd) - - A DataFrame of :meth:`~rateslib.instruments.IRS.cashflows`. - - .. ipython:: python - - irs.cashflows(curves=usd) - - For accurate sensitivity calculations; :meth:`~rateslib.instruments.IRS.delta` - and :meth:`~rateslib.instruments.IRS.gamma`, construct a curve model. - - .. ipython:: python - - sofr_kws = dict( - effective=dt(2022, 1, 1), - frequency="A", - convention="Act360", - calendar="nyc", - currency="usd", - curves=["usd"] - ) - instruments = [ - IRS(termination="1Y", **sofr_kws), - IRS(termination="2Y", **sofr_kws), - ] - solver = Solver( - curves=[usd], - instruments=instruments, - s=[3.65, 3.20], - instrument_labels=["1Y", "2Y"], - id="sofr", - ) - irs.delta(solver=solver) - irs.gamma(solver=solver) - """ - - _fixed_rate_mixin = True - _leg2_float_spread_mixin = True - - def __init__( - self, - *args, - fixed_rate: float | NoInput = NoInput(0), - leg2_float_spread: float | NoInput = NoInput(0), - leg2_spread_compound_method: str | NoInput = NoInput(0), - leg2_fixings: float | list | Series | NoInput = NoInput(0), - leg2_fixing_method: str | NoInput = NoInput(0), - leg2_method_param: int | NoInput = NoInput(0), - **kwargs, - ): - super().__init__(*args, **kwargs) - user_kwargs = dict( - fixed_rate=fixed_rate, - leg2_float_spread=leg2_float_spread, - leg2_spread_compound_method=leg2_spread_compound_method, - leg2_fixings=leg2_fixings, - leg2_fixing_method=leg2_fixing_method, - leg2_method_param=leg2_method_param, - ) - self.kwargs = _update_not_noinput(self.kwargs, user_kwargs) - - self._fixed_rate = fixed_rate - self._leg2_float_spread = leg2_float_spread - self.leg1 = FixedLeg(**_get(self.kwargs, leg=1)) - self.leg2 = FloatLeg(**_get(self.kwargs, leg=2)) - - def _set_pricing_mid( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - ): - # the test for an unpriced IRS is that its fixed rate is not set. - if self.fixed_rate is NoInput.blank: - # set a fixed rate for the purpose of generic methods NPV will be zero. - mid_market_rate = self.rate(curves, solver) - self.leg1.fixed_rate = float(mid_market_rate) - - def analytic_delta(self, *args, **kwargs): - """ - Return the analytic delta of a leg of the derivative object. - - See :meth:`BaseDerivative.analytic_delta`. - """ - return super().analytic_delta(*args, **kwargs) - - def npv( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - local: bool = False, - ): - """ - Return the NPV of the derivative by summing legs. - - See :meth:`BaseDerivative.npv`. - """ - self._set_pricing_mid(curves, solver) - return super().npv(curves, solver, fx, base, local) - - def rate( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - ): - """ - Return the mid-market rate of the IRS. - - Parameters - ---------- - curves : Curve, str or list of such - A single :class:`~rateslib.curves.Curve` or id or a list of such. - A list defines the following curves in the order: - - - Forecasting :class:`~rateslib.curves.Curve` for floating leg. - - Discounting :class:`~rateslib.curves.Curve` for both legs. - solver : Solver, optional - The numerical :class:`~rateslib.solver.Solver` that - constructs :class:`~rateslib.curves.Curve` from calibrating instruments. - - .. note:: - - The arguments ``fx`` and ``base`` are unused by single currency - derivatives rates calculations. - - Returns - ------- - float, Dual or Dual2 - - Notes - ----- - The arguments ``fx`` and ``base`` are unused by single currency derivatives - rates calculations. - """ - curves, _, _ = _get_curves_fx_and_base_maybe_from_solver( - self.curves, - solver, - curves, - fx, - base, - self.leg1.currency, - ) - leg2_npv = self.leg2.npv(curves[2], curves[3]) - return self.leg1._spread(-leg2_npv, curves[0], curves[1]) / 100 - # leg1_analytic_delta = self.leg1.analytic_delta(curves[0], curves[1]) - # return leg2_npv / (leg1_analytic_delta * 100) - - def cashflows( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - ): - """ - Return the properties of all legs used in calculating cashflows. - - See :meth:`BaseDerivative.cashflows`. - """ - self._set_pricing_mid(curves, solver) - return super().cashflows(curves, solver, fx, base) - - # Licence: Creative Commons - Attribution-NonCommercial-NoDerivatives 4.0 International - # Commercial use of this code, and/or copying and redistribution is prohibited. - # Contact rateslib at gmail.com if this code is observed outside its intended sphere. - - def spread( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - ): - """ - Return the mid-market float spread (bps) required to equate to the fixed rate. - - Parameters - ---------- - curves : Curve, str or list of such - A single :class:`~rateslib.curves.Curve` or id or a list of such. - A list defines the following curves in the order: - - - Forecasting :class:`~rateslib.curves.Curve` for floating leg. - - Discounting :class:`~rateslib.curves.Curve` for both legs. - solver : Solver, optional - The numerical :class:`~rateslib.solver.Solver` that constructs - :class:`~rateslib.curves.Curve` from calibrating instruments. - - .. note:: - - The arguments ``fx`` and ``base`` are unused by single currency - derivatives rates calculations. - - Returns - ------- - float, Dual or Dual2 - - Notes - ----- - If the :class:`IRS` is specified without a ``fixed_rate`` this should always - return the current ``leg2_float_spread`` value or zero since the fixed rate used - for calculation is the implied rate including the current ``leg2_float_spread`` - parameter. - - Examples - -------- - For the most common parameters this method will be exact. - - .. ipython:: python - - irs.spread(curves=usd) - irs.leg2_float_spread = -6.948753 - irs.npv(curves=usd) - - When a non-linear spread compound method is used for float RFR legs this is - an approximation, via second order Taylor expansion. - - .. ipython:: python - - irs = IRS( - effective=dt(2022, 2, 15), - termination=dt(2022, 8, 15), - frequency="Q", - convention="30e360", - leg2_convention="Act360", - leg2_fixing_method="rfr_payment_delay", - leg2_spread_compound_method="isda_compounding", - payment_lag=2, - fixed_rate=2.50, - leg2_float_spread=0, - notional=50000000, - currency="usd", - ) - irs.spread(curves=usd) - irs.leg2_float_spread = -111.060143 - irs.npv(curves=usd) - irs.spread(curves=usd) - - The ``leg2_float_spread`` is determined through NPV differences. If the difference - is small since the defined spread is already quite close to the solution the - approximation is much more accurate. This is shown above where the second call - to ``irs.spread`` is different to the previous call, albeit the difference - is 1/10000th of a basis point. - """ - irs_npv = self.npv(curves, solver) - specified_spd = 0 if self.leg2.float_spread is NoInput(0) else self.leg2.float_spread - curves, _, _ = _get_curves_fx_and_base_maybe_from_solver( - self.curves, - solver, - curves, - fx, - base, - self.leg1.currency, - ) - return self.leg2._spread(-irs_npv, curves[2], curves[3]) + specified_spd - # leg2_analytic_delta = self.leg2.analytic_delta(curves[2], curves[3]) - # return irs_npv / leg2_analytic_delta + specified_spd - - -class STIRFuture(IRS): - """ - Create a short term interest rate (STIR) future. - - Parameters - ---------- - args : dict - Required positional args to :class:`BaseDerivative`. - price : float - The traded price of the future. Defined as 100 minus the fixed rate. - contracts : int - The number of traded contracts. - bp_value : float. - The value of 1bp on the contract as specified by the exchange, e.g. SOFR 3M futures are - $25 per bp. This is not the same as tick value where the tick size can be different across - different futures. - nominal : float - The nominal value of the contract. E.g. SOFR 3M futures are $1mm. If not given will use the - default notional. - fixed_rate : float or None - The fixed rate applied to the :class:`~rateslib.legs.FixedLeg`. If `None` - will be set to mid-market when curves are provided. - leg2_float_spread : float, optional - The spread applied to the :class:`~rateslib.legs.FloatLeg`. Can be set to - `None` and designated - later, perhaps after a mid-market spread for all periods has been calculated. - leg2_spread_compound_method : str, optional - The method to use for adding a floating spread to compounded rates. Available - options are `{"none_simple", "isda_compounding", "isda_flat_compounding"}`. - leg2_fixings : float, list, or Series optional - If a float scalar, will be applied as the determined fixing for the first - period. If a list of *n* fixings will be used as the fixings for the first *n* - periods. If any sublist of length *m* is given, is used as the first *m* RFR - fixings for that :class:`~rateslib.periods.FloatPeriod`. If a datetime - indexed ``Series`` will use the fixings that are available in that object, - and derive the rest from the ``curve``. - leg2_fixing_method : str, optional - The method by which floating rates are determined, set by default. See notes. - leg2_method_param : int, optional - A parameter that is used for the various ``fixing_method`` s. See notes. - kwargs : dict - Required keyword arguments to :class:`BaseDerivative`. - - Examples - -------- - Construct a curve to price the example. - - .. ipython:: python - - usd = Curve( - nodes={ - dt(2022, 1, 1): 1.0, - dt(2023, 1, 1): 0.965, - dt(2024, 1, 1): 0.94 - }, - id="usd_stir" - ) - - Create the *STIRFuture*, and demonstrate the :meth:`~rateslib.instruments.STIRFuture.rate`, - :meth:`~rateslib.instruments.STIRFuture.npv`, - - .. ipython:: python - - stir = STIRFuture( - effective=dt(2022, 3, 16), - termination=dt(2022, 6, 15), - spec="usd_stir", - curves=usd, - price=99.50, - contracts=10, - ) - stir.rate(metric="price") - stir.npv() - - """ - - _fixed_rate_mixin = True - _leg2_float_spread_mixin = True - - def __init__( - self, - *args, - price: float | NoInput = NoInput(0), - contracts: int = 1, - bp_value: float | NoInput = NoInput(0), - nominal: float | NoInput = NoInput(0), - leg2_float_spread: float | NoInput = NoInput(0), - leg2_spread_compound_method: str | NoInput = NoInput(0), - leg2_fixings: float | list | Series | NoInput = NoInput(0), - leg2_fixing_method: str | NoInput = NoInput(0), - leg2_method_param: int | NoInput = NoInput(0), - **kwargs, - ): - nominal = defaults.notional if nominal is NoInput.blank else nominal - # TODO this overwrite breaks positional arguments - kwargs["notional"] = nominal * contracts * -1.0 - super(IRS, self).__init__(*args, **kwargs) # call BaseDerivative.__init__() - user_kwargs = dict( - price=price, - fixed_rate=NoInput(0) if price is NoInput.blank else (100 - price), - leg2_float_spread=leg2_float_spread, - leg2_spread_compound_method=leg2_spread_compound_method, - leg2_fixings=leg2_fixings, - leg2_fixing_method=leg2_fixing_method, - leg2_method_param=leg2_method_param, - nominal=nominal, - bp_value=bp_value, - contracts=contracts, - ) - self.kwargs = _update_not_noinput(self.kwargs, user_kwargs) - - self._fixed_rate = self.kwargs["fixed_rate"] - self._leg2_float_spread = leg2_float_spread - self.leg1 = FixedLeg( - **_get(self.kwargs, leg=1, filter=["price", "nominal", "bp_value", "contracts"]), - ) - self.leg2 = FloatLeg(**_get(self.kwargs, leg=2)) - - def npv( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - local: bool = False, - ): - """ - Return the NPV of the derivative by summing legs. - - See :meth:`BaseDerivative.npv`. - """ - # the test for an unpriced IRS is that its fixed rate is not set. - mid_price = self.rate(curves, solver, fx, base, metric="price") - if self.fixed_rate is NoInput.blank: - # set a fixed rate for the purpose of generic methods NPV will be zero. - self.leg1.fixed_rate = float(100 - mid_price) - - traded_price = 100 - self.leg1.fixed_rate - _ = (mid_price - traded_price) * 100 * self.kwargs["contracts"] * self.kwargs["bp_value"] - if local: - return {self.leg1.currency: _} - else: - return _ - - def rate( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - metric: str = "rate", - ): - """ - Return the mid-market rate of the IRS. - - Parameters - ---------- - curves : Curve, str or list of such - A single :class:`~rateslib.curves.Curve` or id or a list of such. - A list defines the following curves in the order: - - - Forecasting :class:`~rateslib.curves.Curve` for floating leg. - - Discounting :class:`~rateslib.curves.Curve` for both legs. - solver : Solver, optional - The numerical :class:`~rateslib.solver.Solver` that - constructs :class:`~rateslib.curves.Curve` from calibrating instruments. - - .. note:: - - The arguments ``fx`` and ``base`` are unused by single currency - derivatives rates calculations. - metric : str in {"rate", "price"} - The calculation metric that will be returned. - - Returns - ------- - float, Dual or Dual2 - - Notes - ----- - The arguments ``fx`` and ``base`` are unused by single currency derivatives - rates calculations. - """ - curves, _, _ = _get_curves_fx_and_base_maybe_from_solver( - self.curves, - solver, - curves, - fx, - base, - self.leg1.currency, - ) - leg2_npv = self.leg2.npv(curves[2], curves[3]) - - _ = self.leg1._spread(-leg2_npv, curves[0], curves[1]) / 100 - if metric.lower() == "rate": - return _ - elif metric.lower() == "price": - return 100 - _ - else: - raise ValueError("`metric` must be in {'price', 'rate'}.") - - def analytic_delta(self, *args, **kwargs): - """ - Return the analytic delta of the *STIRFuture*. - - See :meth:`BasePeriod.analytic_delta()`. - For *STIRFuture* this method requires no arguments. - """ - return -1.0 * self.kwargs["contracts"] * self.kwargs["bp_value"] - - def cashflows( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - ): - return DataFrame.from_records( - [ - { - defaults.headers["type"]: type(self).__name__, - defaults.headers["stub_type"]: "Regular", - defaults.headers["currency"]: self.leg1.currency.upper(), - defaults.headers["a_acc_start"]: self.leg1.schedule.effective, - defaults.headers["a_acc_end"]: self.leg1.schedule.termination, - defaults.headers["payment"]: None, - defaults.headers["convention"]: "Exchange", - defaults.headers["dcf"]: float(self.leg1.notional) - / self.kwargs["nominal"] - * self.kwargs["bp_value"] - / 100.0, - defaults.headers["notional"]: float(self.leg1.notional), - defaults.headers["df"]: 1.0, - defaults.headers["collateral"]: self.leg1.currency.lower(), - }, - ], - ) - - def spread(self): - """ - Not implemented for *STIRFuture*. - """ - return NotImplementedError() - - -# class Swap(IRS): -# """ -# Alias for :class:`~rateslib.instruments.IRS`. -# """ - - -class IIRS(BaseDerivative): - """ - Create an indexed interest rate swap (IIRS) composing an - :class:`~rateslib.legs.IndexFixedLeg` and a :class:`~rateslib.legs.FloatLeg`. - - Parameters - ---------- - args : dict - Required positional args to :class:`BaseDerivative`. - fixed_rate : float or None - The fixed rate applied to the :class:`~rateslib.legs.ZeroFixedLeg`. If `None` - will be set to mid-market when curves are provided. - index_base : float or None, optional - The base index applied to all periods. - index_fixings : float, or Series, optional - If a float scalar, will be applied as the index fixing for the first - period. - If a list of *n* fixings will be used as the index fixings for the first *n* - periods. - If a datetime indexed ``Series`` will use the fixings that are available in - that object, and derive the rest from the ``curve``. - index_method : str - Whether the indexing uses a daily measure for settlement or the most recently - monthly data taken from the first day of month. - index_lag : int, optional - The number of months by which the index value is lagged. Used to ensure - consistency between curves and forecast values. Defined by default. - notional_exchange : bool, optional - Whether the legs include final notional exchanges and interim - amortization notional exchanges. - kwargs : dict - Required keyword arguments to :class:`BaseDerivative`. - - Examples - -------- - Construct a curve to price the example. - - .. ipython:: python - - usd = Curve( - nodes={ - dt(2022, 1, 1): 1.0, - dt(2027, 1, 1): 0.85, - dt(2032, 1, 1): 0.65, - }, - id="usd", - ) - us_cpi = IndexCurve( - nodes={ - dt(2022, 1, 1): 1.0, - dt(2027, 1, 1): 0.85, - dt(2032, 1, 1): 0.70, - }, - id="us_cpi", - index_base=100, - index_lag=3, - ) - - Create the IIRS, and demonstrate the :meth:`~rateslib.instruments.IIRS.rate`, and - :meth:`~rateslib.instruments.IIRS.npv`. - - .. ipython:: python - - iirs = IIRS( - effective=dt(2022, 1, 1), - termination="4Y", - frequency="A", - calendar="nyc", - currency="usd", - fixed_rate=2.05, - convention="1+", - notional=100e6, - index_base=100.0, - index_method="monthly", - index_lag=3, - notional_exchange=True, - leg2_convention="Act360", - curves=["us_cpi", "usd", "usd", "usd"], - ) - iirs.rate(curves=[us_cpi, usd, usd, usd]) - iirs.npv(curves=[us_cpi, usd, usd, usd]) - - A DataFrame of :meth:`~rateslib.instruments.IIRS.cashflows`. - - .. ipython:: python - - iirs.cashflows(curves=[us_cpi, usd, usd, usd]) - - For accurate sensitivity calculations; :meth:`~rateslib.instruments.IIRS.delta` - and :meth:`~rateslib.instruments.IIRS.gamma`, construct a curve model. - - .. ipython:: python - - sofr_kws = dict( - effective=dt(2022, 1, 1), - frequency="A", - convention="Act360", - calendar="nyc", - currency="usd", - curves=["usd"] - ) - cpi_kws = dict( - effective=dt(2022, 1, 1), - frequency="A", - convention="1+", - calendar="nyc", - leg2_index_method="monthly", - currency="usd", - curves=["usd", "usd", "us_cpi", "usd"] - ) - instruments = [ - IRS(termination="5Y", **sofr_kws), - IRS(termination="10Y", **sofr_kws), - ZCIS(termination="5Y", **cpi_kws), - ZCIS(termination="10Y", **cpi_kws), - ] - solver = Solver( - curves=[usd, us_cpi], - instruments=instruments, - s=[3.40, 3.60, 2.2, 2.05], - instrument_labels=["5Y", "10Y", "5Yi", "10Yi"], - id="us", - ) - iirs.delta(solver=solver) - iirs.gamma(solver=solver) - """ - - _fixed_rate_mixin = True - _index_base_mixin = True - _leg2_float_spread_mixin = True - - def __init__( - self, - *args, - fixed_rate: float | NoInput = NoInput(0), - index_base: float | Series | NoInput = NoInput(0), - index_fixings: float | Series | NoInput = NoInput(0), - index_method: str | NoInput = NoInput(0), - index_lag: int | NoInput = NoInput(0), - notional_exchange: bool | NoInput = False, - payment_lag_exchange: int | NoInput = NoInput(0), - leg2_float_spread: float | NoInput = NoInput(0), - leg2_fixings: float | list | NoInput = NoInput(0), - leg2_fixing_method: str | NoInput = NoInput(0), - leg2_method_param: int | NoInput = NoInput(0), - leg2_spread_compound_method: str | NoInput = NoInput(0), - leg2_payment_lag_exchange: int | NoInput = NoInput(1), - **kwargs, - ): - super().__init__(*args, **kwargs) - if leg2_payment_lag_exchange is NoInput.inherit: - leg2_payment_lag_exchange = payment_lag_exchange - user_kwargs = dict( - fixed_rate=fixed_rate, - index_base=index_base, - index_fixings=index_fixings, - index_method=index_method, - index_lag=index_lag, - initial_exchange=False, - final_exchange=notional_exchange, - payment_lag_exchange=payment_lag_exchange, - leg2_float_spread=leg2_float_spread, - leg2_spread_compound_method=leg2_spread_compound_method, - leg2_fixings=leg2_fixings, - leg2_fixing_method=leg2_fixing_method, - leg2_method_param=leg2_method_param, - leg2_payment_lag_exchange=leg2_payment_lag_exchange, - leg2_initial_exchange=False, - leg2_final_exchange=notional_exchange, - ) - self.kwargs = _update_not_noinput(self.kwargs, user_kwargs) - - self._index_base = self.kwargs["index_base"] - self._fixed_rate = self.kwargs["fixed_rate"] - self.leg1 = IndexFixedLeg(**_get(self.kwargs, leg=1)) - self.leg2 = FloatLeg(**_get(self.kwargs, leg=2)) - - def _set_pricing_mid( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - ): - mid_market_rate = self.rate(curves, solver) - self.leg1.fixed_rate = float(mid_market_rate) - - def npv( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - local: bool = False, - ): - curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( - self.curves, - solver, - curves, - fx, - base, - self.leg1.currency, - ) - if self.index_base is NoInput.blank: - # must forecast for the leg - self.leg1.index_base = curves[0].index_value( - self.leg1.schedule.effective, - self.leg1.index_method, - ) - if self.fixed_rate is NoInput.blank: - # set a fixed rate for the purpose of pricing NPV, which should be zero. - self._set_pricing_mid(curves, solver) - return super().npv(curves, solver, fx_, base_, local) - - def cashflows( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - ): - curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( - self.curves, - solver, - curves, - fx, - base, - self.leg1.currency, - ) - if self.index_base is NoInput.blank: - # must forecast for the leg - self.leg1.index_base = curves[0].index_value( - self.leg1.schedule.effective, - self.leg1.index_method, - ) - if self.fixed_rate is NoInput.blank: - # set a fixed rate for the purpose of pricing NPV, which should be zero. - self._set_pricing_mid(curves, solver) - return super().cashflows(curves, solver, fx_, base_) - - def rate( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - ): - """ - Return the mid-market rate of the IRS. - - Parameters - ---------- - curves : Curve, str or list of such - A single :class:`~rateslib.curves.Curve` or id or a list of such. - A list defines the following curves in the order: - - - Forecasting :class:`~rateslib.curves.Curve` for floating leg. - - Discounting :class:`~rateslib.curves.Curve` for both legs. - solver : Solver, optional - The numerical :class:`~rateslib.solver.Solver` that - constructs :class:`~rateslib.curves.Curve` from calibrating instruments. - - .. note:: - - The arguments ``fx`` and ``base`` are unused by single currency - derivatives rates calculations. - - Returns - ------- - float, Dual or Dual2 - - Notes - ----- - The arguments ``fx`` and ``base`` are unused by single currency derivatives - rates calculations. - """ - curves, _, _ = _get_curves_fx_and_base_maybe_from_solver( - self.curves, - solver, - curves, - fx, - base, - self.leg1.currency, - ) - if self.index_base is NoInput.blank: - # must forecast for the leg - self.leg1.index_base = curves[0].index_value( - self.leg1.schedule.effective, - self.leg1.index_method, - ) - leg2_npv = self.leg2.npv(curves[2], curves[3]) - - if self.fixed_rate is NoInput.blank: - self.leg1.fixed_rate = 0.0 - _existing = self.leg1.fixed_rate - leg1_npv = self.leg1.npv(curves[0], curves[1]) - - _ = self.leg1._spread(-leg2_npv - leg1_npv, curves[0], curves[1]) / 100 - return _ + _existing - - def spread( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - ): - """ - Return the mid-market float spread (bps) required to equate to the fixed rate. - - Parameters - ---------- - curves : Curve, str or list of such - A single :class:`~rateslib.curves.Curve` or id or a list of such. - A list defines the following curves in the order: - - - Forecasting :class:`~rateslib.curves.Curve` for floating leg. - - Discounting :class:`~rateslib.curves.Curve` for both legs. - solver : Solver, optional - The numerical :class:`~rateslib.solver.Solver` that constructs - :class:`~rateslib.curves.Curve` from calibrating instruments. - - .. note:: - - The arguments ``fx`` and ``base`` are unused by single currency - derivatives rates calculations. - - Returns - ------- - float, Dual or Dual2 - - Notes - ----- - If the :class:`IRS` is specified without a ``fixed_rate`` this should always - return the current ``leg2_float_spread`` value or zero since the fixed rate used - for calculation is the implied rate including the current ``leg2_float_spread`` - parameter. - - Examples - -------- - For the most common parameters this method will be exact. - - .. ipython:: python - - irs.spread(curves=usd) - irs.leg2_float_spread = -6.948753 - irs.npv(curves=usd) - - When a non-linear spread compound method is used for float RFR legs this is - an approximation, via second order Taylor expansion. - - .. ipython:: python - - irs = IRS( - effective=dt(2022, 2, 15), - termination=dt(2022, 8, 15), - frequency="Q", - convention="30e360", - leg2_convention="Act360", - leg2_fixing_method="rfr_payment_delay", - leg2_spread_compound_method="isda_compounding", - payment_lag=2, - fixed_rate=2.50, - leg2_float_spread=0, - notional=50000000, - currency="usd", - ) - irs.spread(curves=usd) - irs.leg2_float_spread = -111.060143 - irs.npv(curves=usd) - irs.spread(curves=usd) - - The ``leg2_float_spread`` is determined through NPV differences. If the difference - is small since the defined spread is already quite close to the solution the - approximation is much more accurate. This is shown above where the second call - to ``irs.spread`` is different to the previous call, albeit the difference - is 1/10000th of a basis point. - """ - irs_npv = self.npv(curves, solver) - specified_spd = 0 if self.leg2.float_spread is NoInput.blank else self.leg2.float_spread - curves, _, _ = _get_curves_fx_and_base_maybe_from_solver( - self.curves, - solver, - curves, - fx, - base, - self.leg1.currency, - ) - return self.leg2._spread(-irs_npv, curves[2], curves[3]) + specified_spd - - -class ZCS(BaseDerivative): - """ - Create a zero coupon swap (ZCS) composing a :class:`~rateslib.legs.ZeroFixedLeg` - and a :class:`~rateslib.legs.ZeroFloatLeg`. - - Parameters - ---------- - args : dict - Required positional args to :class:`BaseDerivative`. - fixed_rate : float or None - The fixed rate applied to the :class:`~rateslib.legs.ZeroFixedLeg`. If `None` - will be set to mid-market when curves are provided. - leg2_float_spread : float, optional - The spread applied to the :class:`~rateslib.legs.FloatLeg`. Can be set to - `None` and designated - later, perhaps after a mid-market spread for all periods has been calculated. - leg2_spread_compound_method : str, optional - The method to use for adding a floating spread to compounded rates. Available - options are `{"none_simple", "isda_compounding", "isda_flat_compounding"}`. - leg2_fixings : float, list, or Series optional - If a float scalar, will be applied as the determined fixing for the first - period. If a list of *n* fixings will be used as the fixings for the first *n* - periods. If any sublist of length *m* is given, is used as the first *m* RFR - fixings for that :class:`~rateslib.periods.FloatPeriod`. If a datetime - indexed ``Series`` will use the fixings that are available in that object, - and derive the rest from the ``curve``. - leg2_fixing_method : str, optional - The method by which floating rates are determined, set by default. See notes. - leg2_method_param : int, optional - A parameter that is used for the various ``fixing_method`` s. See notes. - kwargs : dict - Required keyword arguments to :class:`BaseDerivative`. - - Examples - -------- - Construct a curve to price the example. - - .. ipython:: python - - usd = Curve( - nodes={ - dt(2022, 1, 1): 1.0, - dt(2027, 1, 1): 0.85, - dt(2032, 1, 1): 0.70, - }, - id="usd" - ) - - Create the ZCS, and demonstrate the :meth:`~rateslib.instruments.ZCS.rate`, - :meth:`~rateslib.instruments.ZCS.npv`, - :meth:`~rateslib.instruments.ZCS.analytic_delta`, and - - .. ipython:: python - - zcs = ZCS( - effective=dt(2022, 1, 1), - termination="10Y", - frequency="Q", - calendar="nyc", - currency="usd", - fixed_rate=4.0, - convention="Act360", - notional=100e6, - curves=["usd"], - ) - zcs.rate(curves=usd) - zcs.npv(curves=usd) - zcs.analytic_delta(curve=usd) - - A DataFrame of :meth:`~rateslib.instruments.ZCS.cashflows`. - - .. ipython:: python - - zcs.cashflows(curves=usd) - - For accurate sensitivity calculations; :meth:`~rateslib.instruments.ZCS.delta` - and :meth:`~rateslib.instruments.ZCS.gamma`, construct a curve model. - - .. ipython:: python - - sofr_kws = dict( - effective=dt(2022, 1, 1), - frequency="A", - convention="Act360", - calendar="nyc", - currency="usd", - curves=["usd"] - ) - instruments = [ - IRS(termination="5Y", **sofr_kws), - IRS(termination="10Y", **sofr_kws), - ] - solver = Solver( - curves=[usd], - instruments=instruments, - s=[3.40, 3.60], - instrument_labels=["5Y", "10Y"], - id="sofr", - ) - zcs.delta(solver=solver) - zcs.gamma(solver=solver) - """ - - _fixed_rate_mixin = True - _leg2_float_spread_mixin = True - - def __init__( - self, - *args, - fixed_rate: float | NoInput = NoInput(0), - leg2_float_spread: float | NoInput = NoInput(0), - leg2_spread_compound_method: str | NoInput = NoInput(0), - leg2_fixings: float | list | Series | NoInput = NoInput(0), - leg2_fixing_method: str | NoInput = NoInput(0), - leg2_method_param: int | NoInput = NoInput(0), - **kwargs, - ): - super().__init__(*args, **kwargs) - user_kwargs = dict( - fixed_rate=fixed_rate, - leg2_float_spread=leg2_float_spread, - leg2_spread_compound_method=leg2_spread_compound_method, - leg2_fixings=leg2_fixings, - leg2_fixing_method=leg2_fixing_method, - leg2_method_param=leg2_method_param, - ) - self.kwargs = _update_not_noinput(self.kwargs, user_kwargs) - self._fixed_rate = fixed_rate - self._leg2_float_spread = leg2_float_spread - self.leg1 = ZeroFixedLeg(**_get(self.kwargs, leg=1)) - self.leg2 = ZeroFloatLeg(**_get(self.kwargs, leg=2)) - - def analytic_delta(self, *args, **kwargs): - """ - Return the analytic delta of a leg of the derivative object. - - See - :meth:`BaseDerivative.analytic_delta`. - """ - return super().analytic_delta(*args, **kwargs) - - def _set_pricing_mid(self, curves, solver): - if self.fixed_rate is NoInput.blank: - # set a fixed rate for the purpose of pricing NPV, which should be zero. - mid_market_rate = self.rate(curves, solver) - self.leg1.fixed_rate = float(mid_market_rate) - - def npv( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - local: bool = False, - ): - """ - Return the NPV of the derivative by summing legs. - - See :meth:`BaseDerivative.npv`. - """ - self._set_pricing_mid(curves, solver) - return super().npv(curves, solver, fx, base, local) - - def rate( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - ): - """ - Return the mid-market rate of the ZCS. - - Parameters - ---------- - curves : Curve, str or list of such - A single :class:`~rateslib.curves.Curve` or id or a list of such. - A list defines the following curves in the order: - - - Forecasting :class:`~rateslib.curves.Curve` for floating leg. - - Discounting :class:`~rateslib.curves.Curve` for both legs. - solver : Solver, optional - The numerical :class:`~rateslib.solver.Solver` that - constructs :class:`~rateslib.curves.Curve` from calibrating instruments. - - .. note:: - - The arguments ``fx`` and ``base`` are unused by single currency - derivatives rates calculations. - - Returns - ------- - float, Dual or Dual2 - - Notes - ----- - The arguments ``fx`` and ``base`` are unused by single currency derivatives - rates calculations. - - The *'irr'* ``fixed_rate`` defines a cashflow by: - - .. math:: - - -notional * ((1 + irr / f)^{f \\times dcf} - 1) - - where :math:`f` is associated with the compounding frequency. - """ - curves, _, _ = _get_curves_fx_and_base_maybe_from_solver( - self.curves, - solver, - curves, - fx, - base, - self.leg1.currency, - ) - leg2_npv = self.leg2.npv(curves[2], curves[3]) - _ = self.leg1._spread(-leg2_npv, curves[0], curves[1]) / 100 - return _ - - def cashflows( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - ): - """ - Return the properties of all legs used in calculating cashflows. - - See :meth:`BaseDerivative.cashflows`. - """ - self._set_pricing_mid(curves, solver) - return super().cashflows(curves, solver, fx, base) - - -class ZCIS(BaseDerivative): - """ - Create a zero coupon index swap (ZCIS) composing an - :class:`~rateslib.legs.ZeroFixedLeg` - and a :class:`~rateslib.legs.ZeroIndexLeg`. - - Parameters - ---------- - args : dict - Required positional args to :class:`BaseDerivative`. - fixed_rate : float or None - The fixed rate applied to the :class:`~rateslib.legs.ZeroFixedLeg`. If `None` - will be set to mid-market when curves are provided. - index_base : float or None, optional - The base index applied to all periods. - index_fixings : float, or Series, optional - If a float scalar, will be applied as the index fixing for the first - period. - If a list of *n* fixings will be used as the index fixings for the first *n* - periods. - If a datetime indexed ``Series`` will use the fixings that are available in - that object, and derive the rest from the ``curve``. - index_method : str - Whether the indexing uses a daily measure for settlement or the most recently - monthly data taken from the first day of month. - index_lag : int, optional - The number of months by which the index value is lagged. Used to ensure - consistency between curves and forecast values. Defined by default. - kwargs : dict - Required keyword arguments to :class:`BaseDerivative`. - - Examples - -------- - Construct a curve to price the example. - - .. ipython:: python - - usd = Curve( - nodes={ - dt(2022, 1, 1): 1.0, - dt(2027, 1, 1): 0.85, - dt(2032, 1, 1): 0.65, - }, - id="usd", - ) - us_cpi = IndexCurve( - nodes={ - dt(2022, 1, 1): 1.0, - dt(2027, 1, 1): 0.85, - dt(2032, 1, 1): 0.70, - }, - id="us_cpi", - index_base=100, - index_lag=3, - ) - - Create the ZCIS, and demonstrate the :meth:`~rateslib.instruments.ZCIS.rate`, - :meth:`~rateslib.instruments.ZCIS.npv`, - :meth:`~rateslib.instruments.ZCIS.analytic_delta`, and - - .. ipython:: python - - zcis = ZCIS( - effective=dt(2022, 1, 1), - termination="10Y", - frequency="A", - calendar="nyc", - currency="usd", - fixed_rate=2.05, - convention="1+", - notional=100e6, - leg2_index_base=100.0, - leg2_index_method="monthly", - leg2_index_lag=3, - curves=["usd", "usd", "us_cpi", "usd"], - ) - zcis.rate(curves=[usd, usd, us_cpi, usd]) - zcis.npv(curves=[usd, usd, us_cpi, usd]) - zcis.analytic_delta(usd, usd) - - A DataFrame of :meth:`~rateslib.instruments.ZCIS.cashflows`. - - .. ipython:: python - - zcis.cashflows(curves=[usd, usd, us_cpi, usd]) - - For accurate sensitivity calculations; :meth:`~rateslib.instruments.ZCIS.delta` - and :meth:`~rateslib.instruments.ZCIS.gamma`, construct a curve model. - - .. ipython:: python - - sofr_kws = dict( - effective=dt(2022, 1, 1), - frequency="A", - convention="Act360", - calendar="nyc", - currency="usd", - curves=["usd"] - ) - cpi_kws = dict( - effective=dt(2022, 1, 1), - frequency="A", - convention="1+", - calendar="nyc", - leg2_index_method="monthly", - currency="usd", - curves=["usd", "usd", "us_cpi", "usd"] - ) - instruments = [ - IRS(termination="5Y", **sofr_kws), - IRS(termination="10Y", **sofr_kws), - ZCIS(termination="5Y", **cpi_kws), - ZCIS(termination="10Y", **cpi_kws), - ] - solver = Solver( - curves=[usd, us_cpi], - instruments=instruments, - s=[3.40, 3.60, 2.2, 2.05], - instrument_labels=["5Y", "10Y", "5Yi", "10Yi"], - id="us", - ) - zcis.delta(solver=solver) - zcis.gamma(solver=solver) - """ - - _fixed_rate_mixin = True - _leg2_index_base_mixin = True - - def __init__( - self, - *args, - fixed_rate: float | NoInput = NoInput(0), - leg2_index_base: float | Series | NoInput = NoInput(0), - leg2_index_fixings: float | Series | NoInput = NoInput(0), - leg2_index_method: str | NoInput = NoInput(0), - leg2_index_lag: int | NoInput = NoInput(0), - **kwargs, - ): - super().__init__(*args, **kwargs) - user_kwargs = dict( - fixed_rate=fixed_rate, - leg2_index_base=leg2_index_base, - leg2_index_fixings=leg2_index_fixings, - leg2_index_lag=leg2_index_lag, - leg2_index_method=leg2_index_method, - ) - self.kwargs = _update_not_noinput(self.kwargs, user_kwargs) - self._fixed_rate = fixed_rate - self._leg2_index_base = leg2_index_base - self.leg1 = ZeroFixedLeg(**_get(self.kwargs, leg=1)) - self.leg2 = ZeroIndexLeg(**_get(self.kwargs, leg=2)) - - def _set_pricing_mid(self, curves, solver): - if self.fixed_rate is NoInput.blank: - # set a fixed rate for the purpose of pricing NPV, which should be zero. - mid_market_rate = self.rate(curves, solver) - self.leg1.fixed_rate = float(mid_market_rate) - - def cashflows( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - ): - self._set_pricing_mid(curves, solver) - return super().cashflows(curves, solver, fx, base) - - def npv( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - local: bool = False, - ): - self._set_pricing_mid(curves, solver) - return super().npv(curves, solver, fx, base, local) - - def rate( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - ): - """ - Return the mid-market IRR rate of the ZCIS. - - Parameters - ---------- - curves : Curve, str or list of such - A single :class:`~rateslib.curves.Curve` or id or a list of such. - A list defines the following curves in the order: - - - Forecasting :class:`~rateslib.curves.Curve` for floating leg. - - Discounting :class:`~rateslib.curves.Curve` for both legs. - solver : Solver, optional - The numerical :class:`~rateslib.solver.Solver` that - constructs :class:`~rateslib.curves.Curve` from calibrating instruments. - - .. note:: - - The arguments ``fx`` and ``base`` are unused by single currency - derivatives rates calculations. - - Returns - ------- - float, Dual or Dual2 - - Notes - ----- - The arguments ``fx`` and ``base`` are unused by single currency derivatives - rates calculations. - """ - curves, _, _ = _get_curves_fx_and_base_maybe_from_solver( - self.curves, - solver, - curves, - fx, - base, - self.leg1.currency, - ) - if self.leg2_index_base is NoInput.blank: - # must forecast for the leg - forecast_value = curves[2].index_value( - self.leg2.schedule.effective, - self.leg2.index_method, - ) - if abs(forecast_value) < 1e-13: - raise ValueError( - "Forecasting the `index_base` for the ZCIS yielded 0.0, which is infeasible.\n" - "This might occur if the ZCIS starts in the past, or has a 'monthly' " - "`index_method` which uses the 1st day of the effective month, which is in the " - "past.\nA known `index_base` value should be input with the ZCIS " - "specification.", - ) - self.leg2.index_base = forecast_value - leg2_npv = self.leg2.npv(curves[2], curves[3]) - - return self.leg1._spread(-leg2_npv, curves[0], curves[1]) / 100 - - -class SBS(BaseDerivative): - """ - Create a single currency basis swap composing two - :class:`~rateslib.legs.FloatLeg` s. - - Parameters - ---------- - args : tuple - Required positional args to :class:`BaseDerivative`. - float_spread : float, optional - The spread applied to the :class:`~rateslib.legs.FloatLeg`. Can be set to - `None` and designated - later, perhaps after a mid-market spread for all periods has been calculated. - spread_compound_method : str, optional - The method to use for adding a floating spread to compounded rates. Available - options are `{"none_simple", "isda_compounding", "isda_flat_compounding"}`. - fixings : float, list, or Series optional - If a float scalar, will be applied as the determined fixing for the first - period. If a list of *n* fixings will be used as the fixings for the first *n* - periods. If any sublist of length *m* is given, is used as the first *m* RFR - fixings for that :class:`~rateslib.periods.FloatPeriod`. If a datetime - indexed ``Series`` will use the fixings that are available in that object, - and derive the rest from the ``curve``. - fixing_method : str, optional - The method by which floating rates are determined, set by default. See notes. - method_param : int, optional - A parameter that is used for the various ``fixing_method`` s. See notes. - leg2_float_spread : float or None - The floating spread applied in a simple way (after daily compounding) to the - second :class:`~rateslib.legs.FloatLeg`. If `None` will be set to zero. - float_spread : float, optional - The spread applied to the :class:`~rateslib.legs.FloatLeg`. Can be set to - `None` and designated - later, perhaps after a mid-market spread for all periods has been calculated. - leg2_spread_compound_method : str, optional - The method to use for adding a floating spread to compounded rates. Available - options are `{"none_simple", "isda_compounding", "isda_flat_compounding"}`. - leg2_fixings : float, list, or Series optional - If a float scalar, will be applied as the determined fixing for the first - period. If a list of *n* fixings will be used as the fixings for the first *n* - periods. If any sublist of length *m* is given, is used as the first *m* RFR - fixings for that :class:`~rateslib.periods.FloatPeriod`. If a datetime - indexed ``Series`` will use the fixings that are available in that object, - and derive the rest from the ``curve``. - leg2_fixing_method : str, optional - The method by which floating rates are determined, set by default. See notes. - leg2_method_param : int, optional - A parameter that is used for the various ``fixing_method`` s. See notes. - kwargs : dict - Required keyword arguments to :class:`BaseDerivative`. - - Examples - -------- - Construct curves to price the example. - - .. ipython:: python - - eur3m = Curve( - nodes={ - dt(2022, 1, 1): 1.0, - dt(2023, 1, 1): 0.965, - dt(2024, 1, 1): 0.94 - }, - id="eur3m", - ) - eur6m = Curve( - nodes={ - dt(2022, 1, 1): 1.0, - dt(2023, 1, 1): 0.962, - dt(2024, 1, 1): 0.936 - }, - id="eur6m", - ) - - Create the SBS, and demonstrate the :meth:`~rateslib.instruments.SBS.rate`, - :meth:`~rateslib.instruments.SBS.npv`, - :meth:`~rateslib.instruments.SBS.analytic_delta`, and - :meth:`~rateslib.instruments.SBS.spread`. - - .. ipython:: python - - sbs = SBS( - effective=dt(2022, 1, 1), - termination="18M", - frequency="Q", - leg2_frequency="S", - calendar="tgt", - currency="eur", - fixing_method="ibor", - method_param=2, - convention="Act360", - leg2_float_spread=-22.9, - notional=100e6, - curves=["eur3m", "eur3m", "eur6m", "eur3m"], - ) - sbs.rate(curves=[eur3m, eur3m, eur6m, eur3m]) - sbs.npv(curves=[eur3m, eur3m, eur6m, eur3m]) - sbs.analytic_delta(curve=eur6m, disc_curve=eur3m, leg=2) - sbs.spread(curves=[eur3m, eur3m, eur6m, eur3m], leg=2) - - A DataFrame of :meth:`~rateslib.instruments.SBS.cashflows`. - - .. ipython:: python - - sbs.cashflows(curves=[eur3m, eur3m, eur6m, eur3m]) - - For accurate sensitivity calculations; :meth:`~rateslib.instruments.SBS.delta` - and :meth:`~rateslib.instruments.SBS.gamma`, construct a curve model. - - .. ipython:: python - - irs_kws = dict( - effective=dt(2022, 1, 1), - frequency="A", - leg2_frequency="Q", - convention="30E360", - leg2_convention="Act360", - leg2_fixing_method="ibor", - leg2_method_param=2, - calendar="tgt", - currency="eur", - curves=["eur3m", "eur3m"], - ) - sbs_kws = dict( - effective=dt(2022, 1, 1), - frequency="Q", - leg2_frequency="S", - convention="Act360", - fixing_method="ibor", - method_param=2, - leg2_convention="Act360", - calendar="tgt", - currency="eur", - curves=["eur3m", "eur3m", "eur6m", "eur3m"] - ) - instruments = [ - IRS(termination="1Y", **irs_kws), - IRS(termination="2Y", **irs_kws), - SBS(termination="1Y", **sbs_kws), - SBS(termination="2Y", **sbs_kws), - ] - solver = Solver( - curves=[eur3m, eur6m], - instruments=instruments, - s=[1.55, 1.6, 5.5, 6.5], - instrument_labels=["1Y", "2Y", "1Y 3s6s", "2Y 3s6s"], - id="eur", - ) - sbs.delta(solver=solver) - sbs.gamma(solver=solver) - - """ - - _float_spread_mixin = True - _leg2_float_spread_mixin = True - _rate_scalar = 100.0 - - def __init__( - self, - *args, - float_spread: float | NoInput = NoInput(0), - spread_compound_method: str | NoInput = NoInput(0), - fixings: float | list | Series | NoInput = NoInput(0), - fixing_method: str | NoInput = NoInput(0), - method_param: int | NoInput = NoInput(0), - leg2_float_spread: float | NoInput = NoInput(0), - leg2_spread_compound_method: str | NoInput = NoInput(0), - leg2_fixings: float | list | Series | NoInput = NoInput(0), - leg2_fixing_method: str | NoInput = NoInput(0), - leg2_method_param: int | NoInput = NoInput(0), - **kwargs, - ): - super().__init__(*args, **kwargs) - user_kwargs = dict( - float_spread=float_spread, - spread_compound_method=spread_compound_method, - fixings=fixings, - fixing_method=fixing_method, - method_param=method_param, - leg2_float_spread=leg2_float_spread, - leg2_spread_compound_method=leg2_spread_compound_method, - leg2_fixings=leg2_fixings, - leg2_fixing_method=leg2_fixing_method, - leg2_method_param=leg2_method_param, - ) - self.kwargs = _update_not_noinput(self.kwargs, user_kwargs) - self._float_spread = float_spread - self._leg2_float_spread = leg2_float_spread - self.leg1 = FloatLeg(**_get(self.kwargs, leg=1)) - self.leg2 = FloatLeg(**_get(self.kwargs, leg=2)) - - def _set_pricing_mid(self, curves, solver): - if self.float_spread is NoInput.blank and self.leg2_float_spread is NoInput.blank: - # set a pricing parameter for the purpose of pricing NPV at zero. - rate = self.rate(curves, solver) - self.leg1.float_spread = float(rate) - - def analytic_delta(self, *args, **kwargs): - """ - Return the analytic delta of a leg of the derivative object. - - See :meth:`BaseDerivative.analytic_delta`. - """ - return super().analytic_delta(*args, **kwargs) - - def cashflows( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - ): - """ - Return the properties of all legs used in calculating cashflows. - - See :meth:`BaseDerivative.cashflows`. - """ - self._set_pricing_mid(curves, solver) - return super().cashflows(curves, solver, fx, base) - - def npv( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - local: bool = False, - ): - """ - Return the NPV of the derivative object by summing legs. - - See :meth:`BaseDerivative.npv`. - """ - self._set_pricing_mid(curves, solver) - return super().npv(curves, solver, fx, base, local) - - def rate( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - leg: int = 1, - ): - """ - Return the mid-market float spread on the specified leg of the SBS. - - Parameters - ---------- - curves : Curve, str or list of such - A list defines the following curves in the order: - - - Forecasting :class:`~rateslib.curves.Curve` for floating leg1. - - Discounting :class:`~rateslib.curves.Curve` for both legs. - - Forecasting :class:`~rateslib.curves.Curve` for floating leg2. - solver : Solver, optional - The numerical :class:`~rateslib.solver.Solver` that constructs - :class:`~rateslib.curves.Curve` from calibrating - instruments. - leg: int in [1, 2] - Specify which leg the spread calculation is applied to. - - Returns - ------- - float, Dual or Dual2 - """ - core_npv = super().npv(curves, solver) - curves, _, _ = _get_curves_fx_and_base_maybe_from_solver( - self.curves, - solver, - curves, - fx, - base, - self.leg1.currency, - ) - if leg == 1: - leg_obj, args = self.leg1, (curves[0], curves[1]) - else: - leg_obj, args = self.leg2, (curves[2], curves[3]) - - specified_spd = 0 if leg_obj.float_spread is NoInput.blank else leg_obj.float_spread - return leg_obj._spread(-core_npv, *args) + specified_spd - - # irs_npv = self.npv(curves, solver) - # curves, _ = self._get_curves_and_fx_maybe_from_solver(solver, curves, None) - # if leg == 1: - # args = (curves[0], curves[1]) - # else: - # args = (curves[2], curves[3]) - # leg_analytic_delta = getattr(self, f"leg{leg}").analytic_delta(*args) - # adjust = getattr(self, f"leg{leg}").float_spread - # adjust = 0 if adjust is NoInput.blank else adjust - # _ = irs_npv / leg_analytic_delta + adjust - # return _ - - def spread(self, *args, **kwargs): - """ - Return the mid-market float spread on the specified leg of the SBS. - - Alias for :meth:`~rateslib.instruments.SBS.rate`. - """ - return self.rate(*args, **kwargs) - - -class FRA(Sensitivities, BaseMixin): - """ - Create a forward rate agreement composing single period :class:`~rateslib.legs.FixedLeg` - and :class:`~rateslib.legs.FloatLeg` valued in a customised manner. - - Parameters - ---------- - args : dict - Required positional args to :class:`BaseDerivative`. - fixed_rate : float or None - The fixed rate applied to the :class:`~rateslib.legs.FixedLeg`. If `None` - will be set to mid-market when curves are provided. - fixings : float or list, optional - If a float scalar, will be applied as the determined fixing for the first - period. If a list of *n* fixings will be used as the fixings for the first *n* - periods. If any sublist of length *m* is given as the first *m* RFR fixings - within individual curve and composed into the overall rate. - method_param : int, optional - A parameter that is used for the various ``fixing_method`` s. See notes. - kwargs : dict - Required keyword arguments to :class:`BaseDerivative`. - - Notes - ----- - FRAs are a legacy derivative whose *FloatLeg* ``fixing_method`` is set to *"ibor"*. - - Examples - -------- - Construct curves to price the example. - - .. ipython:: python - - eur3m = Curve( - nodes={ - dt(2022, 1, 1): 1.0, - dt(2023, 1, 1): 0.965, - dt(2024, 1, 1): 0.94 - }, - id="eur3m", - ) - - Create the FRA, and demonstrate the :meth:`~rateslib.instruments.FRA.rate`, - :meth:`~rateslib.instruments.FRA.npv`, - :meth:`~rateslib.instruments.FRA.analytic_delta`. - - .. ipython:: python - - fra = FRA( - effective=dt(2023, 2, 15), - termination="3M", - frequency="Q", - calendar="tgt", - currency="eur", - method_param=2, - convention="Act360", - notional=100e6, - fixed_rate=2.617, - curves=["eur3m"], - ) - fra.rate(curves=eur3m) - fra.npv(curves=eur3m) - fra.analytic_delta(curve=eur3m) - - A DataFrame of :meth:`~rateslib.instruments.FRA.cashflows`. - - .. ipython:: python - - fra.cashflows(curves=eur3m) - - For accurate sensitivity calculations; :meth:`~rateslib.instruments.FRA.delta` - and :meth:`~rateslib.instruments.FRA.gamma`, construct a curve model. - - .. ipython:: python - - irs_kws = dict( - effective=dt(2022, 1, 1), - frequency="A", - leg2_frequency="Q", - convention="30E360", - leg2_convention="Act360", - leg2_fixing_method="ibor", - leg2_method_param=2, - calendar="tgt", - currency="eur", - curves=["eur3m", "eur3m"], - ) - instruments = [ - IRS(termination="1Y", **irs_kws), - IRS(termination="2Y", **irs_kws), - ] - solver = Solver( - curves=[eur3m], - instruments=instruments, - s=[1.55, 1.6], - instrument_labels=["1Y", "2Y"], - id="eur", - ) - fra.delta(solver=solver) - fra.gamma(solver=solver) - - """ - - _fixed_rate_mixin = True - - def __init__( - self, - effective: datetime | NoInput = NoInput(0), - termination: datetime | str | NoInput = NoInput(0), - frequency: int | NoInput = NoInput(0), - roll: str | int | NoInput = NoInput(0), - eom: bool | NoInput = NoInput(0), - modifier: str | None | NoInput = NoInput(0), - calendar: CustomBusinessDay | str | NoInput = NoInput(0), - payment_lag: int | NoInput = NoInput(0), - notional: float | NoInput = NoInput(0), - currency: str | NoInput = NoInput(0), - convention: str | NoInput = NoInput(0), - method_param: int | NoInput = NoInput(0), - fixed_rate: float | NoInput = NoInput(0), - fixings: float | Series | NoInput = NoInput(0), - curves: str | list | Curve | NoInput = NoInput(0), - spec: str | NoInput = NoInput(0), - ) -> None: - self.kwargs = dict( - effective=effective, - termination=termination, - frequency=_upper(frequency), - roll=roll, - eom=eom, - modifier=_upper(modifier), - calendar=calendar, - payment_lag=payment_lag, - notional=notional, - currency=_lower(currency), - convention=_upper(convention), - fixed_rate=fixed_rate, - leg2_effective=NoInput(1), - leg2_termination=NoInput(1), - leg2_convention=NoInput(1), - leg2_frequency=NoInput(1), - leg2_notional=NoInput(-1), - leg2_modifier=NoInput(1), - leg2_currency=NoInput(1), - leg2_calendar=NoInput(1), - leg2_roll=NoInput(1), - leg2_eom=NoInput(1), - leg2_payment_lag=NoInput(1), - leg2_fixing_method="ibor", - leg2_method_param=method_param, - leg2_spread_compound_method="none_simple", - leg2_fixings=fixings, - ) - self.kwargs = _push(spec, self.kwargs) - - # set defaults for missing values - default_kwargs = dict( - notional=defaults.notional, - payment_lag=defaults.payment_lag_specific[type(self).__name__], - currency=defaults.base_currency, - modifier=defaults.modifier, - eom=defaults.eom, - convention=defaults.convention, - ) - self.kwargs = _update_with_defaults(self.kwargs, default_kwargs) - self.kwargs = _inherit_or_negate(self.kwargs) - - # Build - self.curves = curves - - self._fixed_rate = self.kwargs["fixed_rate"] - self.leg1 = FixedLeg(**_get(self.kwargs, leg=1)) - self.leg2 = FloatLeg(**_get(self.kwargs, leg=2)) - - if self.leg1.schedule.n_periods != 1 or self.leg2.schedule.n_periods != 1: - raise ValueError("FRA scheduling inputs did not define a single period.") - - def _set_pricing_mid( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - ) -> None: - if self.fixed_rate is NoInput.blank: - mid_market_rate = self.rate(curves, solver) - self.leg1.fixed_rate = mid_market_rate.real - - def analytic_delta( - self, - curve: Curve, - disc_curve: Curve | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - ) -> DualTypes: - """ - Return the analytic delta of the FRA. - - For arguments see :meth:`~rateslib.periods.BasePeriod.analytic_delta`. - """ - disc_curve_: Curve = _disc_from_curve(curve, disc_curve) - fx, base = _get_fx_and_base(self.leg1.currency, fx, base) - rate = self.rate([curve]) - _ = ( - self.leg1.notional - * self.leg1.periods[0].dcf - * disc_curve_[self.leg1.schedule.pschedule[0]] - / 10000 - ) - return fx * _ / (1 + self.leg1.periods[0].dcf * rate / 100) - - def npv( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - local: bool = False, - ) -> DualTypes: - """ - Return the NPV of the derivative. - - See :meth:`BaseDerivative.npv`. - """ - - self._set_pricing_mid(curves, solver) - curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( - self.curves, - solver, - curves, - fx, - base, - self.leg1.currency, - ) - fx, base = _get_fx_and_base(self.leg1.currency, fx_, base_) - value = self.cashflow(curves[0]) * curves[1][self.leg1.schedule.pschedule[0]] - if local: - return {self.leg1.currency: value} - else: - return fx * value - - def rate( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - ) -> DualTypes: - """ - Return the mid-market rate of the FRA. - - Only the forecasting curve is required to price an FRA. - - Parameters - ---------- - curves : Curve, str or list of such - A single :class:`~rateslib.curves.Curve` or id or a list of such. - A list defines the following curves in the order: - - - Forecasting :class:`~rateslib.curves.Curve` for floating leg. - - Discounting :class:`~rateslib.curves.Curve` for floating leg. - solver : Solver, optional - The numerical :class:`~rateslib.solver.Solver` that - constructs :class:`~rateslib.curves.Curve` from calibrating instruments. - fx : unused - base : unused - - Returns - ------- - float, Dual or Dual2 - """ - curves, _, _ = _get_curves_fx_and_base_maybe_from_solver( - self.curves, - solver, - curves, - fx, - base, - self.leg1.currency, - ) - return self.leg2.periods[0].rate(curves[0]) - - def cashflow(self, curve: Curve | LineCurve): - """ - Calculate the local currency cashflow on the FRA from current floating rate - and fixed rate. - - Parameters - ---------- - curve : Curve or LineCurve, - The forecasting curve for determining the floating rate. - - Returns - ------- - float, Dual or Dual2 - """ - cf1 = self.leg1.periods[0].cashflow - cf2 = self.leg2.periods[0].cashflow(curve) - if cf1 is not NoInput.blank and cf2 is not NoInput.blank: - cf = cf1 + cf2 - else: - return None - rate = ( - None - if curve is NoInput.blank - else 100 * cf2 / (-self.leg2.notional * self.leg2.periods[0].dcf) - ) - cf /= 1 + self.leg1.periods[0].dcf * rate / 100 - - # if self.fixed_rate is NoInput.blank: - # return 0 # set the fixed rate = to floating rate netting to zero - # rate = self.leg2.rate(curve) - # cf = self.notional * self.leg1.dcf * (rate - self.fixed_rate) / 100 - # cf /= 1 + self.leg1.dcf * rate / 100 - return cf - - def cashflows( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: float | FXRates | FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - ) -> DataFrame: - """ - Return the properties of the leg used in calculating cashflows. - - Parameters - ---------- - args : - Positional arguments supplied to :meth:`~rateslib.periods.BasePeriod.cashflows`. - kwargs : - Keyword arguments supplied to :meth:`~rateslib.periods.BasePeriod.cashflows`. - - Returns - ------- - DataFrame - """ - self._set_pricing_mid(curves, solver) - curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( - self.curves, - solver, - curves, - fx, - base, - self.leg1.currency, - ) - fx_, base_ = _get_fx_and_base(self.leg1.currency, fx_, base_) - - cf = float(self.cashflow(curves[0])) - df = float(curves[1][self.leg1.schedule.pschedule[0]]) - npv_local = cf * df - - _fix = None if self.fixed_rate is NoInput.blank else -float(self.fixed_rate) - _spd = None if curves[1] is NoInput.blank else -float(self.rate(curves[1])) * 100 - cfs = self.leg1.periods[0].cashflows(curves[0], curves[1], fx_, base_) - cfs[defaults.headers["type"]] = "FRA" - cfs[defaults.headers["payment"]] = self.leg1.schedule.pschedule[0] - cfs[defaults.headers["cashflow"]] = cf - cfs[defaults.headers["rate"]] = _fix - cfs[defaults.headers["spread"]] = _spd - cfs[defaults.headers["npv"]] = npv_local - cfs[defaults.headers["df"]] = df - cfs[defaults.headers["fx"]] = float(fx_) - cfs[defaults.headers["npv_fx"]] = npv_local * float(fx_) - return DataFrame.from_records([cfs]) - - def delta(self, *args, **kwargs): - """ - Calculate the delta of the *Instrument*. - - For arguments see :meth:`Sensitivities.delta()`. - """ - return super().delta(*args, **kwargs) - - def gamma(self, *args, **kwargs): - """ - Calculate the gamma of the *Instrument*. - - For arguments see :meth:`Sensitivities.gamma()`. - """ - return super().gamma(*args, **kwargs) - - -# Multi-currency derivatives - - -class XCS(BaseDerivative): - """ - Create a cross-currency swap (XCS) composing relevant fixed or floating *Legs*. - - MTM-XCSs will introduce a MTM *Leg* as *Leg2*. - - .. warning:: - - ``leg2_notional`` is unused by *XCS*. That notional is always dynamically determined by - ``fx_fixings``, i.e. an initial FX fixing and/or forecast forward FX rates if ``leg2_mtm`` - is set to *True*. See also the parameter definition for ``fx_fixings``. - - Parameters - ---------- - args : tuple - Required positional arguments for :class:`~rateslib.instruments.BaseDerivative`. - fixed : bool, optional - Whether *leg1* is fixed or floating rate. Defaults to *False*. - payment_lag_exchange : int - The number of business days by which to delay notional exchanges, aligned with - the accrual schedule. - fixed_rate : float, optional - If ``fixed``, the fixed rate of *leg1*. - float_spread : float, optional - If not ``fixed``, the spread applied to the :class:`~rateslib.legs.FloatLeg`. Can be set to - `None` and designated - later, perhaps after a mid-market spread for all periods has been calculated. - spread_compound_method : str, optional - If not ``fixed``, the method to use for adding a floating spread to compounded rates. - Available options are `{"none_simple", "isda_compounding", "isda_flat_compounding"}`. - fixings : float, list, or Series optional - If not ``fixed``, then if a float scalar, will be applied as the determined fixing for - the first period. If a list of *n* fixings will be used as the fixings for the first *n* - periods. If any sublist of length *m* is given, is used as the first *m* RFR - fixings for that :class:`~rateslib.periods.FloatPeriod`. If a datetime - indexed ``Series`` will use the fixings that are available in that object, - and derive the rest from the ``curve``. - fixing_method : str, optional - If not ``fixed``, the method by which floating rates are determined, set by default. - See notes. - method_param : int, optional - If not ``fixed`` A parameter that is used for the various ``fixing_method`` s. See notes. - leg2_fixed : bool, optional - Whether *leg2* is fixed or floating rate. Defaults to *False* - leg2_mtm : bool optional - Whether *leg2* is a mark-to-market leg. Defaults to *True* - leg2_payment_lag_exchange : int - The number of business days by which to delay notional exchanges, aligned with - the accrual schedule. - leg2_fixed_rate : float, optional - If ``leg2_fixed``, the fixed rate of *leg2*. - leg2_float_spread : float, optional - If not ``leg2_fixed``, the spread applied to the :class:`~rateslib.legs.FloatLeg`. - Can be set to `None` and designated - later, perhaps after a mid-market spread for all periods has been calculated. - leg2_spread_compound_method : str, optional - If not ``leg2_fixed``, the method to use for adding a floating spread to compounded rates. - Available options are `{"none_simple", "isda_compounding", "isda_flat_compounding"}`. - leg2_fixings : float, list, or Series optional - If not ``leg2_fixed``, then if a float scalar, will be applied as the determined fixing for - the first period. If a list of *n* fixings will be used as the fixings for the first *n* - periods. If any sublist of length *m* is given, is used as the first *m* RFR - fixings for that :class:`~rateslib.periods.FloatPeriod`. If a datetime - indexed ``Series`` will use the fixings that are available in that object, - and derive the rest from the ``curve``. - leg2_fixing_method : str, optional - If not ``leg2_fixed``, the method by which floating rates are determined, set by default. - See notes. - leg2_method_param : int, optional - If not ``leg2_fixed`` A parameter that is used for the various ``fixing_method`` s. - See notes. - fx_fixings : float, Dual, Dual2, list of such, optional - Specify a known initial FX fixing or a list of such for ``mtm`` legs, where leg 1 is - considered the domestic currency. For example for an ESTR/SOFR XCS in 100mm EUR notional - a value of 1.10 EURUSD for fx_fixings implies the notional on leg 2 is 110m USD. Fixings - that are not specified will be forecast at pricing time with an - :class:`~rateslib.fx.FXForwards` object. - kwargs : dict - Required keyword arguments for :class:`~rateslib.instruments.BaseDerivative`. - """ - - def __init__( - self, - *args, - fixed: bool | NoInput = NoInput(0), - payment_lag_exchange: int | NoInput = NoInput(0), - fixed_rate: float | NoInput = NoInput(0), - float_spread: float | NoInput = NoInput(0), - spread_compound_method: str | NoInput = NoInput(0), - fixings: float | list | Series | NoInput = NoInput(0), - fixing_method: str | NoInput = NoInput(0), - method_param: int | NoInput = NoInput(0), - leg2_fixed: bool | NoInput = NoInput(0), - leg2_mtm: bool | NoInput = NoInput(0), - leg2_payment_lag_exchange: int | NoInput = NoInput(1), - leg2_fixed_rate: float | NoInput = NoInput(0), - leg2_float_spread: float | NoInput = NoInput(0), - leg2_fixings: float | list | NoInput = NoInput(0), - leg2_fixing_method: str | NoInput = NoInput(0), - leg2_method_param: int | NoInput = NoInput(0), - leg2_spread_compound_method: str | NoInput = NoInput(0), - fx_fixings: list | DualTypes | FXRates | FXForwards | NoInput = NoInput(0), - **kwargs, - ): - super().__init__(*args, **kwargs) - # set defaults for missing values - default_kwargs = dict( - fixed=False if fixed is NoInput.blank else fixed, - leg2_fixed=False if leg2_fixed is NoInput.blank else leg2_fixed, - leg2_mtm=True if leg2_mtm is NoInput.blank else leg2_mtm, - ) - self.kwargs = _update_not_noinput(self.kwargs, default_kwargs) - - if self.kwargs["fixed"]: - self._fixed_rate_mixin = True - self._fixed_rate = fixed_rate - leg1_user_kwargs = dict(fixed_rate=fixed_rate) - Leg1 = FixedLeg - else: - self._rate_scalar = 100.0 - self._float_spread_mixin = True - self._float_spread = float_spread - leg1_user_kwargs = dict( - float_spread=float_spread, - spread_compound_method=spread_compound_method, - fixings=fixings, - fixing_method=fixing_method, - method_param=method_param, - ) - Leg1 = FloatLeg - leg1_user_kwargs.update( - dict( - payment_lag_exchange=payment_lag_exchange, - initial_exchange=True, - final_exchange=True, - ), - ) - - if leg2_payment_lag_exchange is NoInput.inherit: - leg2_payment_lag_exchange = payment_lag_exchange - if self.kwargs["leg2_fixed"]: - self._leg2_fixed_rate_mixin = True - self._leg2_fixed_rate = leg2_fixed_rate - leg2_user_kwargs = dict(leg2_fixed_rate=leg2_fixed_rate) - Leg2 = FixedLeg if not leg2_mtm else FixedLegMtm - else: - self._leg2_float_spread_mixin = True - self._leg2_float_spread = leg2_float_spread - leg2_user_kwargs = dict( - leg2_float_spread=leg2_float_spread, - leg2_spread_compound_method=leg2_spread_compound_method, - leg2_fixings=leg2_fixings, - leg2_fixing_method=leg2_fixing_method, - leg2_method_param=leg2_method_param, - ) - Leg2 = FloatLeg if not leg2_mtm else FloatLegMtm - leg2_user_kwargs.update( - dict( - leg2_payment_lag_exchange=leg2_payment_lag_exchange, - leg2_initial_exchange=True, - leg2_final_exchange=True, - ), - ) - - if self.kwargs["leg2_mtm"]: - self._is_mtm = True - leg2_user_kwargs.update( - dict( - leg2_alt_currency=self.kwargs["currency"], - leg2_alt_notional=-self.kwargs["notional"], - leg2_fx_fixings=fx_fixings, - ), - ) - else: - self._is_mtm = False - - self.kwargs = _update_not_noinput(self.kwargs, {**leg1_user_kwargs, **leg2_user_kwargs}) - - self.leg1 = Leg1(**_get(self.kwargs, leg=1, filter=["fixed"])) - self.leg2 = Leg2(**_get(self.kwargs, leg=2, filter=["leg2_fixed", "leg2_mtm"])) - self._initialise_fx_fixings(fx_fixings) - - @property - def fx_fixings(self): - return self._fx_fixings - - @fx_fixings.setter - def fx_fixings(self, value): - self._fx_fixings = value - self._set_leg2_notional(value) - - def _initialise_fx_fixings(self, fx_fixings): - """ - Sets the `fx_fixing` for non-mtm XCS instruments, which require only a single - value. - """ - if not self._is_mtm: - self.pair = self.leg1.currency + self.leg2.currency - # if self.fx_fixing is NoInput.blank this indicates the swap is unfixed and will be set - # later. If a fixing is given this means the notional is fixed without any - # further sensitivity, hence the downcast to a float below. - if isinstance(fx_fixings, FXForwards): - self.fx_fixings = float(fx_fixings.rate(self.pair, self.leg2.periods[0].payment)) - elif isinstance(fx_fixings, FXRates): - self.fx_fixings = float(fx_fixings.rate(self.pair)) - elif isinstance(fx_fixings, (float, Dual, Dual2)): - self.fx_fixings = float(fx_fixings) - else: - self._fx_fixings = NoInput(0) - else: - self._fx_fixings = fx_fixings - - def _set_fx_fixings(self, fx): - """ - Checks the `fx_fixings` and sets them according to given object if null. - - Used by ``rate`` and ``npv`` methods when ``fx_fixings`` are not - initialised but required for pricing and can be inferred from an FX object. - """ - if not self._is_mtm: # then we manage the initial FX from the pricing object. - if self.fx_fixings is NoInput.blank: - if fx is NoInput.blank: - if defaults.no_fx_fixings_for_xcs.lower() == "raise": - raise ValueError( - "`fx` is required when `fx_fixings` is not pre-set and " - "if rateslib option `no_fx_fixings_for_xcs` is set to " - "'raise'.", - ) - else: - fx_fixing = 1.0 - if defaults.no_fx_fixings_for_xcs.lower() == "warn": - warnings.warn( - "Using 1.0 for FX, no `fx` or `fx_fixings` given and " - "rateslib option `no_fx_fixings_for_xcs` is set to " - "'warn'.", - UserWarning, - ) - else: - if isinstance(fx, FXForwards): - # this is the correct pricing path - fx_fixing = fx.rate(self.pair, self.leg2.periods[0].payment) - elif isinstance(fx, FXRates): - # maybe used in debugging - fx_fixing = fx.rate(self.pair) - else: - # possible float used in debugging also - fx_fixing = fx - self._set_leg2_notional(fx_fixing) - else: - self._set_leg2_notional(fx) - - def _set_leg2_notional(self, fx_arg: float | FXForwards): - """ - Update the notional on leg2 (foreign leg) if the initial fx rate is unfixed. - - ---------- - fx_arg : float or FXForwards - For non-MTM XCSs this input must be a float. - The FX rate to use as the initial notional fixing. - Will only update the leg if ``NonMtmXCS.fx_fixings`` has been initially - set to `None`. - - For MTM XCSs this input must be ``FXForwards``. - The FX object from which to determine FX rates used as the initial - notional fixing, and to determine MTM cashflow exchanges. - """ - if self._is_mtm: - self.leg2._set_periods(fx_arg) - self.leg2_notional = self.leg2.notional - else: - self.leg2_notional = self.leg1.notional * -fx_arg - self.leg2.notional = self.leg2_notional - if self.kwargs["amortization"] is not NoInput.blank: - self.leg2_amortization = self.leg1.amortization * -fx_arg - self.leg2.amortization = self.leg2_amortization - - @property - def _is_unpriced(self): - if getattr(self, "_unpriced", None) is True: - return True - if self._fixed_rate_mixin and self._leg2_fixed_rate_mixin: - # Fixed/Fixed where one leg is unpriced. - if self.fixed_rate is NoInput.blank or self.leg2_fixed_rate is NoInput.blank: # noqa: SIM103 - return True # noqa: SIM103 - return False # noqa: SIM103 - elif self._fixed_rate_mixin and self.fixed_rate is NoInput.blank: - # Fixed/Float where fixed leg is unpriced - return True - elif self._float_spread_mixin and self.float_spread is NoInput.blank: - # Float leg1 where leg1 is - pass # goto 2) - else: - return False - - # 2) leg1 is Float - if self._leg2_fixed_rate_mixin and self.leg2_fixed_rate is NoInput.blank: # noqa: SIM114, SIM103 - return True # noqa: SIM114, SIM103 - elif self._leg2_float_spread_mixin and self.leg2_float_spread is NoInput.blank: # noqa: SIM114, SIM103 - return True # noqa: SIM114, SIM103 - else: # noqa: SIM114, SIM103 - return False # noqa: SIM114, SIM103 - - def _set_pricing_mid( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: FXForwards | NoInput = NoInput(0), - ): - leg: int = 1 - lookup = { - 1: ["_fixed_rate_mixin", "_float_spread_mixin"], - 2: ["_leg2_fixed_rate_mixin", "_leg2_float_spread_mixin"], - } - if self._leg2_fixed_rate_mixin and self.leg2_fixed_rate is NoInput.blank: - # Fixed/Fixed or Float/Fixed - leg = 2 - - rate = self.rate(curves, solver, fx, leg=leg) - if getattr(self, lookup[leg][0]): - getattr(self, f"leg{leg}").fixed_rate = float(rate) - elif getattr(self, lookup[leg][1]): - getattr(self, f"leg{leg}").float_spread = float(rate) - else: - # this line should not be hit: internal code check - raise AttributeError("BaseXCS leg1 must be defined fixed or float.") # pragma: no cover - - def npv( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - local: bool = False, - ): - """ - Return the NPV of the derivative by summing legs. - - .. warning:: - - If ``fx_fixing`` has not been set for the instrument requires - ``fx`` as an FXForwards object to dynamically determine this. - - See :meth:`BaseDerivative.npv`. - """ - curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( - self.curves, - solver, - curves, - fx, - base, - self.leg1.currency, - ) - - if self._is_unpriced: - self._set_pricing_mid(curves, solver, fx_) - - self._set_fx_fixings(fx_) - if self._is_mtm: - self.leg2._do_not_repeat_set_periods = True - - ret = super().npv(curves, solver, fx_, base_, local) - if self._is_mtm: - self.leg2._do_not_repeat_set_periods = False # reset for next calculation - return ret - - def rate( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: FXForwards | NoInput = NoInput(0), - leg: int = 1, - ): - """ - Return the mid-market pricing parameter of the XCS. - - Parameters - ---------- - curves : list of Curves - A list defines the following curves in the order: - - - Forecasting :class:`~rateslib.curves.Curve` for leg1 (if floating). - - Discounting :class:`~rateslib.curves.Curve` for leg1. - - Forecasting :class:`~rateslib.curves.Curve` for leg2 (if floating). - - Discounting :class:`~rateslib.curves.Curve` for leg2. - solver : Solver, optional - The numerical :class:`~rateslib.solver.Solver` that - constructs :class:`~rateslib.curves.Curve` from calibrating instruments. - fx : FXForwards, optional - The FX forwards object that is used to determine the initial FX fixing for - determining ``leg2_notional``, if not specified at initialisation, and for - determining mark-to-market exchanges on mtm XCSs. - leg : int in [1, 2] - The leg whose pricing parameter is to be determined. - - Returns - ------- - float, Dual or Dual2 - - Notes - ----- - Fixed legs have pricing parameter returned in percentage terms, and - float legs have pricing parameter returned in basis point (bp) terms. - - If the ``XCS`` type is specified without a ``fixed_rate`` on any leg then an - implied ``float_spread`` will return as its originaly value or zero since - the fixed rate used - for calculation is the implied mid-market rate including the - current ``float_spread`` parameter. - - Examples - -------- - """ - curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( - self.curves, - solver, - curves, - fx, - NoInput(0), - self.leg1.currency, - ) - - if leg == 1: - tgt_fore_curve, tgt_disc_curve = curves[0], curves[1] - alt_fore_curve, alt_disc_curve = curves[2], curves[3] - else: - tgt_fore_curve, tgt_disc_curve = curves[2], curves[3] - alt_fore_curve, alt_disc_curve = curves[0], curves[1] - - leg2 = 1 if leg == 2 else 2 - # tgt_str, alt_str = "" if leg == 1 else "leg2_", "" if leg2 == 1 else "leg2_" - tgt_leg, alt_leg = getattr(self, f"leg{leg}"), getattr(self, f"leg{leg2}") - base_ = tgt_leg.currency - - _is_float_tgt_leg = "Float" in type(tgt_leg).__name__ - _is_float_alt_leg = "Float" in type(alt_leg).__name__ - if not _is_float_alt_leg and alt_leg.fixed_rate is NoInput.blank: - raise ValueError( - "Cannot solve for a `fixed_rate` or `float_spread` where the " - "`fixed_rate` on the non-solvable leg is NoInput.blank.", - ) - - # Licence: Creative Commons - Attribution-NonCommercial-NoDerivatives 4.0 International - # Commercial use of this code, and/or copying and redistribution is prohibited. - # Contact rateslib at gmail.com if this code is observed outside its intended sphere. - - if not _is_float_tgt_leg: - tgt_leg_fixed_rate = tgt_leg.fixed_rate - if tgt_leg_fixed_rate is NoInput.blank: - # set the target fixed leg to a null fixed rate for calculation - tgt_leg.fixed_rate = 0.0 - else: - # set the fixed rate to a float for calculation and no Dual Type crossing PR: XXX - tgt_leg.fixed_rate = float(tgt_leg_fixed_rate) - - self._set_fx_fixings(fx_) - if self._is_mtm: - self.leg2._do_not_repeat_set_periods = True - - tgt_leg_npv = tgt_leg.npv(tgt_fore_curve, tgt_disc_curve, fx_, base_) - alt_leg_npv = alt_leg.npv(alt_fore_curve, alt_disc_curve, fx_, base_) - fx_a_delta = 1.0 if not tgt_leg._is_mtm else fx_ - _ = tgt_leg._spread( - -(tgt_leg_npv + alt_leg_npv), - tgt_fore_curve, - tgt_disc_curve, - fx_a_delta, - ) - - specified_spd = 0.0 - if _is_float_tgt_leg and tgt_leg.float_spread is not NoInput.blank: - specified_spd = tgt_leg.float_spread - elif not _is_float_tgt_leg: - specified_spd = tgt_leg.fixed_rate * 100 - - _ += specified_spd - - if self._is_mtm: - self.leg2._do_not_repeat_set_periods = False # reset the mtm calc - - return _ if _is_float_tgt_leg else _ * 0.01 - - def spread(self, *args, **kwargs): - """ - Alias for :meth:`~rateslib.instruments.BaseXCS.rate` - """ - return self.rate(*args, **kwargs) - - def cashflows( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - ): - curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( - self.curves, - solver, - curves, - fx, - base, - self.leg1.currency, - ) - - if self._is_unpriced: - self._set_pricing_mid(curves, solver, fx_) - - self._set_fx_fixings(fx_) - if self._is_mtm: - self.leg2._do_not_repeat_set_periods = True - - ret = super().cashflows(curves, solver, fx_, base_) - if self._is_mtm: - self.leg2._do_not_repeat_set_periods = False # reset the mtm calc - return ret - - -class FXSwap(XCS): - """ - Create an FX swap simulated via a *Fixed-Fixed* :class:`XCS`. - - Parameters - ---------- - args : dict - Required positional args to :class:`XCS`. - pair : str, optional - The FX pair, e.g. "eurusd" as 3-digit ISO codes. If not given, fallsback to the base - implementation of *XCS* which defines separate inputs as ``currency`` and ``leg2_currency``. - If overspecified, ``pair`` will dominate. - fx_fixings : float, FXForwards or None - The initial FX fixing where leg 1 is considered the domestic currency. For - example for an ESTR/SOFR XCS in 100mm EUR notional a value of 1.10 for `fx0` - implies the notional on leg 2 is 110m USD. If `None` determines this - dynamically. - points : float, optional - The pricing parameter for the FX Swap, which will determine the implicit - fixed rate on leg2. - split_notional : float, optional - The accrued notional at termination of the domestic leg accounting for interest - payable at domestic interest rates. - kwargs : dict - Required keyword arguments to :class:`XCS`. - - Notes - ----- - .. warning:: - - ``leg2_notional`` is determined by the ``fx_fixings`` either initialised or at price - time and the value of ``notional``. The argument value of ``leg2_notional`` does - not impact calculations. - - *FXSwaps* are technically complicated instruments. To define a fully **priced** *Instrument* - they require at least two pricing parameters; ``fx_fixings`` and ``points``. If a - ``split_notional`` is also given at initialisation it will be assumed to be a split notional - *FXSwap*. If not, then it will not be assumed to be. - - If ``fx_fixings`` is given then the market pricing parameter ``points`` can be calculated. - This is an unusual partially *priced* parametrisation, however, and a warning will be emitted. - As before, if ``split_notional`` is given, or not, at initialisation the *FXSwap* will be - assumed to be split notional or not. - - If the *FXSwap* is not initialised with any parameters this defines an **unpriced** - *Instrument* and it will be assumed to be split notional, inline with interbank - market standards. The mid-market rate of an unpriced FXSwap is the same regardless of whether - it is split notional or not, albeit split notional FXSwaps result in smaller FX rate - sensitivity. - - Other combinations of arguments, just providing ``points`` or ``split_notional`` or both of - those will raise an error. An *FXSwap* cannot be parametrised by these in isolation. This is - summarised in the below table. - - .. list-table:: Resultant initialisation dependent upon given pricing parameters. - :widths: 10 10 10 70 - :header-rows: 1 - - * - fx_fixings - - points - - split_notional - - Result - * - X - - X - - X - - A fully *priced* instrument defined with split notionals. - * - X - - X - - - - A fully *priced* instruments without split notionals. - * - - - - - - - An *unpriced* instrument with assumed split notionals. - * - X - - - - X - - A partially priced instrument with split notionals. Warns about unconventionality. - * - X - - - - - - A partially priced instrument without split notionals. Warns about unconventionality. - * - - - X - - X - - Raises ValueError. Not allowable partially priced instrument. - * - - - X - - - - Raises ValueError. Not allowable partially priced instrument. - * - - - - - X - - Raises ValueError. Not allowable partially priced instrument. - - Examples - -------- - To value the *FXSwap* we create *Curves* and :class:`~rateslib.fx.FXForwards` - objects. - - .. ipython:: python - - usd = Curve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.95}, id="usd") - eur = Curve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.97}, id="eur") - eurusd = Curve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.971}, id="eurusd") - fxr = FXRates({"eurusd": 1.10}, settlement=dt(2022, 1, 3)) - fxf = FXForwards( - fx_rates=fxr, - fx_curves={"usdusd": usd, "eureur": eur, "eurusd": eurusd}, - ) - - Then we define the *FXSwap*. This in an unpriced instrument. - - .. ipython:: python - - fxs = FXSwap( - effective=dt(2022, 1, 18), - termination=dt(2022, 4, 19), - pair="usdeur", - calendar="nyc", - notional=1000000, - curves=["usd", "usd", "eur", "eurusd"], - ) - - Now demonstrate the :meth:`~rateslib.instruments.FXSwap.npv` and - :meth:`~rateslib.instruments.FXSwap.rate` methods: - - .. ipython:: python - - fxs.npv(curves=[None, usd, None, eurusd], fx=fxf) - fxs.rate(curves=[None, usd, None, eurusd], fx=fxf) - - In the case of *FXSwaps*, whose mid-market price is the difference between two - forward FX rates we can also derive this quantity using the independent - :meth:`FXForwards.swap` method. - - .. ipython:: python - - fxf.swap("usdeur", [dt(2022, 1, 18), dt(2022, 4, 19)]) - - The following is an example of a fully priced *FXSwap* with split notionals. - - .. ipython:: python - - fxs = FXSwap( - effective=dt(2022, 1, 18), - termination=dt(2022, 4, 19), - pair="usdeur", - calendar="nyc", - notional=1000000, - curves=["usd", "usd", "eur", "eurusd"], - fx_fixings=0.90, - split_notional=1001500, - points=-49.0 - ) - fxs.npv(curves=[None, usd, None, eurusd], fx=fxf) - fxs.cashflows(curves=[None, usd, None, eurusd], fx=fxf) - fxs.cashflows_table(curves=[None, usd, None, eurusd], fx=fxf) - - """ - - _unpriced = True - - def _parse_split_flag(self, fx_fixings, points, split_notional): - """ - Determine the rules for a priced, unpriced or partially priced derivative and whether - it is inferred as split notional or not. - """ - is_none = [_ is NoInput.blank for _ in [fx_fixings, points, split_notional]] - if all(is_none) or not any(is_none): - self._is_split = True - elif split_notional is NoInput.blank and not any( - _ is NoInput.blank for _ in [fx_fixings, points] - ): - self._is_split = False - elif fx_fixings is not NoInput.blank: - warnings.warn( - "Initialising FXSwap with `fx_fixings` but without `points` is unconventional.\n" - "Pricing can still be performed to determine `points`.", - UserWarning, - ) - if split_notional is not NoInput.blank: - self._is_split = True - else: - self._is_split = False - else: - if points is not NoInput.blank: - raise ValueError("Cannot initialise FXSwap with `points` but without `fx_fixings`.") - else: - raise ValueError( - "Cannot initialise FXSwap with `split_notional` but without `fx_fixings`", - ) - - def _set_split_notional(self, curve: Curve | NoInput = NoInput(0), at_init: bool = False): - """ - Will set the fixed rate, if not zero, for leg1, given provided split not or forecast splnot. - - self._split_notional is used as a temporary storage when mid market price is determined. - """ - if not self._is_split: - self._split_notional = self.kwargs["notional"] - # fixed rate at zero remains - - # a split notional is given by a user and then this is set and never updated. - elif self.kwargs["split_notional"] is not NoInput.blank: - if at_init: # this will be run for one time only at initialisation - self._split_notional = self.kwargs["split_notional"] - self._set_leg1_fixed_rate() - else: - return None - - # else new pricing parameters will affect and unpriced split notional - else: - if at_init: - self._split_notional = None - else: - dt1, dt2 = self.leg1.periods[0].payment, self.leg1.periods[2].payment - self._split_notional = self.kwargs["notional"] * curve[dt1] / curve[dt2] - self._set_leg1_fixed_rate() - - def _set_leg1_fixed_rate(self): - fixed_rate = (self.leg1.notional - self._split_notional) / ( - -self.leg1.notional * self.leg1.periods[1].dcf - ) - self.leg1.fixed_rate = fixed_rate * 100 - - def __init__( - self, - *args, - pair: str | NoInput = NoInput(0), - fx_fixings: float | FXRates | FXForwards | NoInput = NoInput(0), - points: float | NoInput = NoInput(0), - split_notional: float | NoInput = NoInput(0), - **kwargs, - ): - self._parse_split_flag(fx_fixings, points, split_notional) - currencies = {} - if isinstance(pair, str): - # TODO for version 2.0 should look to deprecate 'currency' and 'leg2_currency' as - # allowable inputs. - currencies = {"currency": pair.lower()[0:3], "leg2_currency": pair.lower()[3:6]} - - kwargs_overrides = dict( # specific args for FXSwap passed to the Base XCS - fixed=True, - leg2_fixed=True, - leg2_mtm=False, - fixed_rate=0.0, - frequency="Z", - leg2_frequency="Z", - leg2_fixed_rate=NoInput(0), - fx_fixings=fx_fixings, - ) - super().__init__(*args, **{**kwargs, **kwargs_overrides, **currencies}) - - self.kwargs["split_notional"] = split_notional - self._set_split_notional(curve=None, at_init=True) - # self._initialise_fx_fixings(fx_fixings) - self.points = points - - @property - def points(self): - return self._points - - @points.setter - def points(self, value): - self._unpriced = False - self._points = value - self._leg2_fixed_rate = NoInput(0) - - # setting points requires leg1.notional leg1.split_notional, fx_fixing and points value - - if value is not NoInput.blank: - # leg2 should have been properly set as part of fx_fixings and set_leg2_notional - fx_fixing = self.leg2.notional / -self.leg1.notional - - _ = self._split_notional * (fx_fixing + value / 10000) + self.leg2.notional - fixed_rate = _ / (self.leg2.periods[1].dcf * -self.leg2.notional) - - self.leg2_fixed_rate = fixed_rate * 100 - - # Licence: Creative Commons - Attribution-NonCommercial-NoDerivatives 4.0 International - - # Commercial use of this code, and/or copying and redistribution is prohibited. - # Contact rateslib at gmail.com if this code is observed outside its intended sphere. - - def _set_pricing_mid( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: FXForwards | NoInput = NoInput(0), - ): - # This function ASSUMES that the instrument is unpriced, i.e. all of - # split_notional, fx_fixing and points have been initialised as None. - - # first we set the split notional which is defined by interest rates on leg1. - points = self.rate(curves, solver, fx) - self.points = float(points) - self._unpriced = True # setting pricing mid does not define a priced instrument - - def rate( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: FXForwards | NoInput = NoInput(0), - fixed_rate: bool = False, - ): - """ - Return the mid-market pricing parameter of the FXSwapS. - - Parameters - ---------- - curves : list of Curves - A list defines the following curves in the order: - - - Forecasting :class:`~rateslib.curves.Curve` for leg1 (if floating). - - Discounting :class:`~rateslib.curves.Curve` for leg1. - - Forecasting :class:`~rateslib.curves.Curve` for leg2 (if floating). - - Discounting :class:`~rateslib.curves.Curve` for leg2. - solver : Solver, optional - The numerical :class:`~rateslib.solver.Solver` that - constructs :class:`~rateslib.curves.Curve` from calibrating instruments. - fx : FXForwards, optional - The FX forwards object that is used to determine the initial FX fixing for - determining ``leg2_notional``, if not specified at initialisation, and for - determining mark-to-market exchanges on mtm XCSs. - fixed_rate : bool - Whether to return the fixed rate for the leg or the FX swap points price. - - Returns - ------- - float, Dual or Dual2 - """ - curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( - self.curves, - solver, - curves, - fx, - NoInput(0), - self.leg1.currency, - ) - # set the split notional from the curve if not available - self._set_split_notional(curve=curves[1]) - # then we will set the fx_fixing and leg2 initial notional. - - # self._set_fx_fixings(fx) # this will be done by super().rate() - leg2_fixed_rate = super().rate(curves, solver, fx_, leg=2) - - if fixed_rate: - return leg2_fixed_rate - else: - points = -self.leg2.notional * ( - (1 + leg2_fixed_rate * self.leg2.periods[1].dcf / 100) / self._split_notional - - 1 / self.kwargs["notional"] - ) - return points * 10000 - - def cashflows( - self, - curves: Curve | str | list | NoInput = NoInput(0), - solver: Solver | NoInput = NoInput(0), - fx: FXForwards | NoInput = NoInput(0), - base: str | NoInput = NoInput(0), - ): - if self._is_unpriced: - self._set_pricing_mid(curves, solver, fx) - ret = super().cashflows(curves, solver, fx, base) - return ret +# Multi-currency derivatives # Licence: Creative Commons - Attribution-NonCommercial-NoDerivatives 4.0 International diff --git a/python/rateslib/instruments/generics.py b/python/rateslib/instruments/generics.py index fac0b309..d3416036 100644 --- a/python/rateslib/instruments/generics.py +++ b/python/rateslib/instruments/generics.py @@ -1,17 +1,259 @@ from __future__ import annotations import warnings +from datetime import datetime from pandas import DataFrame, concat from rateslib import defaults -from rateslib.curves import Curve +from rateslib.calendars import dcf +from rateslib.curves import Curve, IndexCurve from rateslib.default import NoInput +from rateslib.dual import DualTypes, dual_log from rateslib.fx import FXForwards, FXRates -from rateslib.instruments.core import Sensitivities +from rateslib.fx_volatility import FXVols +from rateslib.instruments.core import ( + BaseMixin, + Sensitivities, + _get_curves_fx_and_base_maybe_from_solver, + _get_vol_maybe_from_solver, +) from rateslib.solver import Solver -# Generic Instruments +# Licence: Creative Commons - Attribution-NonCommercial-NoDerivatives 4.0 International +# Commercial use of this code, and/or copying and redistribution is prohibited. +# This code cannot be installed or executed on a corporate computer without a paid licence extension +# Contact info at rateslib.com if this code is observed outside its intended sphere of use. + + +class Value(BaseMixin): + """ + A null *Instrument* which can be used within a :class:`~rateslib.solver.Solver` + to directly parametrise a *Curve* node, via some calculated value. + + Parameters + ---------- + effective : datetime + The datetime index for which the `rate`, which is just the curve value, is + returned. + curves : Curve, LineCurve, str or list of such, optional + A single :class:`~rateslib.curves.Curve`, + :class:`~rateslib.curves.LineCurve` or id or a + list of such. Only uses the first *Curve* in a list. + convention : str, optional, + Day count convention used with certain ``metric``. + metric : str in {"curve_value", "index_value", "cc_zero_rate"}, optional + Configures which value to extract from the *Curve*. + + Examples + -------- + The below :class:`~rateslib.curves.Curve` is solved directly + from a calibrating DF value on 1st Nov 2022. + + .. ipython:: python + + curve = Curve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 1.0}, id="v") + instruments = [(Value(dt(2022, 11, 1)), (curve,), {})] + solver = Solver([curve], [], instruments, [0.99]) + curve[dt(2022, 1, 1)] + curve[dt(2022, 11, 1)] + curve[dt(2023, 1, 1)] + """ + + def __init__( + self, + effective: datetime, + convention: str | NoInput = NoInput(0), + metric: str = "curve_value", + curves: list | str | Curve | None = None, + ): + self.effective = effective + self.curves = curves + self.convention = defaults.convention if convention is NoInput.blank else convention + self.metric = metric.lower() + + def rate( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + metric: str | NoInput = NoInput(0), + ): + """ + Return a value derived from a *Curve*. + + Parameters + ---------- + curves : Curve, LineCurve, str or list of such + Uses only one *Curve*, the one given or the first in the list. + solver : Solver, optional + The numerical :class:`~rateslib.solver.Solver` that constructs + ``Curves`` from calibrating instruments. + fx : float, FXRates, FXForwards, optional + Not used. + base : str, optional + Not used. + metric: str in {"curve_value", "index_value", "cc_zero_rate"}, optional + Configures which type of value to return from the applicable *Curve*. + + Returns + ------- + float, Dual, Dual2 + + """ + curves, _, _ = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + NoInput(0), + NoInput(0), + "_", + ) + metric = self.metric if metric is NoInput.blank else metric.lower() + if metric == "curve_value": + return curves[0][self.effective] + elif metric == "cc_zero_rate": + if curves[0]._base_type != "dfs": + raise TypeError( + "`curve` used with `metric`='cc_zero_rate' must be discount factor based.", + ) + dcf_ = dcf(curves[0].node_dates[0], self.effective, self.convention) + _ = (dual_log(curves[0][self.effective]) / -dcf_) * 100 + return _ + elif metric == "index_value": + if not isinstance(curves[0], IndexCurve): + raise TypeError("`curve` used with `metric`='index_value' must be type IndexCurve.") + _ = curves[0].index_value(self.effective) + return _ + raise ValueError("`metric`must be in {'curve_value', 'cc_zero_rate', 'index_value'}.") + + def npv(self, *args, **kwargs): + raise NotImplementedError("`Value` instrument has no concept of NPV.") + + def cashflows(self, *args, **kwargs): + raise NotImplementedError("`Value` instrument has no concept of cashflows.") + + def analytic_delta(self, *args, **kwargs): + raise NotImplementedError("`Value` instrument has no concept of analytic delta.") + + +class VolValue(BaseMixin): + """ + A null *Instrument* which can be used within a :class:`~rateslib.solver.Solver` + to directly parametrise a *Vol* node, via some calculated metric. + + Parameters + ---------- + index_value : float, Dual, Dual2 + The value of some index to the *VolSmile* or *VolSurface*. + metric: str, optional + The default metric to return from the ``rate`` method. + vol: str, FXDeltaVolSmile, optional + The associated object from which to determine the ``rate``. + + Examples + -------- + The below :class:`~rateslib.fx_volatility.FXDeltaVolSmile` is solved directly + from calibrating volatility values. + + .. ipython:: python + :suppress: + + from rateslib.fx_volatility import FXDeltaVolSmile + from rateslib.instruments import VolValue + from rateslib.solver import Solver + + .. ipython:: python + + smile = FXDeltaVolSmile( + nodes={0.25: 10.0, 0.5: 10.0, 0.75: 10.0}, + eval_date=dt(2023, 3, 16), + expiry=dt(2023, 6, 16), + delta_type="forward", + id="VolSmile", + ) + instruments = [ + VolValue(0.25, vol="VolSmile"), + VolValue(0.5, vol="VolSmile"), + VolValue(0.75, vol=smile) + ] + solver = Solver(curves=[smile], instruments=instruments, s=[8.9, 7.8, 9.9]) + smile[0.25] + smile[0.5] + smile[0.75] + """ + + def __init__( + self, + index_value: DualTypes, + # index_type: str = "delta", + # delta_type: str = NoInput(0), + metric: str = "vol", + vol: NoInput | str | FXVols = NoInput(0), + ): + self.index_value = index_value + # self.index_type = index_type + # self.delta_type = delta_type + self.vol = vol + self.curves = NoInput(0) + self.metric = metric.lower() + + def rate( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + vol: DualTypes | FXVols = NoInput(0), + metric: str = "vol", + ): + """ + Return a value derived from a *Curve*. + + Parameters + ---------- + curves : Curve, LineCurve, str or list of such + Uses only one *Curve*, the one given or the first in the list. + solver : Solver, optional + The numerical :class:`~rateslib.solver.Solver` that constructs + ``Curves`` from calibrating instruments. + fx : float, FXRates, FXForwards, optional + Not used. + base : str, optional + Not used. + metric: str in {"curve_value", "index_value", "cc_zero_rate"}, optional + Configures which type of value to return from the applicable *Curve*. + + Returns + ------- + float, Dual, Dual2 + + """ + curves, fx, base = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + fx, + base, + "_", + ) + vol = _get_vol_maybe_from_solver(self.vol, vol, solver) + metric = self.metric if metric is NoInput.blank else metric.lower() + + if metric == "vol": + return vol[self.index_value] + + raise ValueError("`metric` must be in {'vol'}.") + + def npv(self, *args, **kwargs): + raise NotImplementedError("`VolValue` instrument has no concept of NPV.") + + def cashflows(self, *args, **kwargs): + raise NotImplementedError("`VolValue` instrument has no concept of cashflows.") + + def analytic_delta(self, *args, **kwargs): + raise NotImplementedError("`VolValue` instrument has no concept of analytic delta.") class Spread(Sensitivities): @@ -281,6 +523,12 @@ def gamma(self, *args, **kwargs): return super().gamma(*args, **kwargs) +# Licence: Creative Commons - Attribution-NonCommercial-NoDerivatives 4.0 International +# Commercial use of this code, and/or copying and redistribution is prohibited. +# This code cannot be installed or executed on a corporate computer without a paid licence extension +# Contact info at rateslib.com if this code is observed outside its intended sphere of use. + + def _instrument_npv(instrument, *args, **kwargs): # pragma: no cover # this function is captured by TestPortfolio pooling but is not registered as a parallel process # used for parallel processing with Portfolio.npv diff --git a/python/rateslib/instruments/rates_derivatives.py b/python/rateslib/instruments/rates_derivatives.py new file mode 100644 index 00000000..8afd62cf --- /dev/null +++ b/python/rateslib/instruments/rates_derivatives.py @@ -0,0 +1,2420 @@ +from __future__ import annotations + +from abc import ABCMeta, abstractmethod +from datetime import datetime + +from pandas import DataFrame, Series + +from rateslib import defaults +from rateslib.calendars import CalInput +from rateslib.curves import Curve, LineCurve +from rateslib.default import NoInput +from rateslib.dual import DualTypes +from rateslib.fx import FXForwards, FXRates +from rateslib.instruments.core import ( + BaseMixin, + Sensitivities, + _get, + _get_curves_fx_and_base_maybe_from_solver, + _inherit_or_negate, + _lower, + _push, + _update_not_noinput, + _update_with_defaults, + _upper, +) +from rateslib.legs import ( + FixedLeg, + FloatLeg, + IndexFixedLeg, + ZeroFixedLeg, + ZeroFloatLeg, + ZeroIndexLeg, +) +from rateslib.periods import ( + _disc_from_curve, + _get_fx_and_base, +) +from rateslib.solver import Solver + +# Licence: Creative Commons - Attribution-NonCommercial-NoDerivatives 4.0 International +# Commercial use of this code, and/or copying and redistribution is prohibited. +# This code cannot be installed or executed on a corporate computer without a paid licence extension +# Contact info at rateslib.com if this code is observed outside its intended sphere of use. + + +class BaseDerivative(Sensitivities, BaseMixin, metaclass=ABCMeta): + """ + Abstract base class with common parameters for many *Derivative* subclasses. + + Parameters + ---------- + effective : datetime + The adjusted or unadjusted effective date. + termination : datetime or str + The adjusted or unadjusted termination date. If a string, then a tenor must be + given expressed in days (`"D"`), months (`"M"`) or years (`"Y"`), e.g. `"48M"`. + frequency : str in {"M", "B", "Q", "T", "S", "A", "Z"}, optional + The frequency of the schedule. + stub : str combining {"SHORT", "LONG"} with {"FRONT", "BACK"}, optional + The stub type to enact on the swap. Can provide two types, for + example "SHORTFRONTLONGBACK". + front_stub : datetime, optional + An adjusted or unadjusted date for the first stub period. + back_stub : datetime, optional + An adjusted or unadjusted date for the back stub period. + See notes for combining ``stub``, ``front_stub`` and ``back_stub`` + and any automatic stub inference. + roll : int in [1, 31] or str in {"eom", "imm", "som"}, optional + The roll day of the schedule. Inferred if not given. + eom : bool, optional + Use an end of month preference rather than regular rolls for inference. Set by + default. Not required if ``roll`` is specified. + modifier : str, optional + The modification rule, in {"F", "MF", "P", "MP"} + calendar : calendar or str, optional + The holiday calendar object to use. If str, looks up named calendar from + static data. + payment_lag : int, optional + The number of business days to lag payments by. + notional : float, optional + The leg notional, which is applied to each period. + amortization: float, optional + The amount by which to adjust the notional each successive period. Should have + sign equal to that of notional if the notional is to reduce towards zero. + convention: str, optional + The day count convention applied to calculations of period accrual dates. + See :meth:`~rateslib.calendars.dcf`. + leg2_kwargs: Any + All ``leg2`` arguments can be similarly input as above, e.g. ``leg2_frequency``. + If **not** given, any ``leg2`` + argument inherits its value from the ``leg1`` arguments, except in the case of + ``notional`` and ``amortization`` where ``leg2`` inherits the negated value. + curves : Curve, LineCurve, str or list of such, optional + A single :class:`~rateslib.curves.Curve`, + :class:`~rateslib.curves.LineCurve` or id or a + list of such. A list defines the following curves in the order: + + - Forecasting :class:`~rateslib.curves.Curve` or + :class:`~rateslib.curves.LineCurve` for ``leg1``. + - Discounting :class:`~rateslib.curves.Curve` for ``leg1``. + - Forecasting :class:`~rateslib.curves.Curve` or + :class:`~rateslib.curves.LineCurve` for ``leg2``. + - Discounting :class:`~rateslib.curves.Curve` for ``leg2``. + spec : str, optional + An identifier to pre-populate many field with conventional values. See + :ref:`here` for more info and available values. + + Attributes + ---------- + effective : datetime + termination : datetime + frequency : str + stub : str + front_stub : datetime + back_stub : datetime + roll : str, int + eom : bool + modifier : str + calendar : Calendar + payment_lag : int + notional : float + amortization : float + convention : str + leg2_effective : datetime + leg2_termination : datetime + leg2_frequency : str + leg2_stub : str + leg2_front_stub : datetime + leg2_back_stub : datetime + leg2_roll : str, int + leg2_eom : bool + leg2_modifier : str + leg2_calendar : Calendar + leg2_payment_lag : int + leg2_notional : float + leg2_amortization : float + leg2_convention : str + """ + + @abstractmethod + def __init__( + self, + effective: datetime | NoInput = NoInput(0), + termination: datetime | str | NoInput = NoInput(0), + frequency: int | NoInput = NoInput(0), + stub: str | NoInput = NoInput(0), + front_stub: datetime | NoInput = NoInput(0), + back_stub: datetime | NoInput = NoInput(0), + roll: str | int | NoInput = NoInput(0), + eom: bool | NoInput = NoInput(0), + modifier: str | NoInput = NoInput(0), + calendar: CalInput = NoInput(0), + payment_lag: int | NoInput = NoInput(0), + notional: float | NoInput = NoInput(0), + currency: str | NoInput = NoInput(0), + amortization: float | NoInput = NoInput(0), + convention: str | NoInput = NoInput(0), + leg2_effective: datetime | NoInput = NoInput(1), + leg2_termination: datetime | str | NoInput = NoInput(1), + leg2_frequency: int | NoInput = NoInput(1), + leg2_stub: str | NoInput = NoInput(1), + leg2_front_stub: datetime | NoInput = NoInput(1), + leg2_back_stub: datetime | NoInput = NoInput(1), + leg2_roll: str | int | NoInput = NoInput(1), + leg2_eom: bool | NoInput = NoInput(1), + leg2_modifier: str | NoInput = NoInput(1), + leg2_calendar: CalInput = NoInput(1), + leg2_payment_lag: int | NoInput = NoInput(1), + leg2_notional: float | NoInput = NoInput(-1), + leg2_currency: str | NoInput = NoInput(1), + leg2_amortization: float | NoInput = NoInput(-1), + leg2_convention: str | NoInput = NoInput(1), + curves: list | str | Curve | NoInput = NoInput(0), + spec: str | NoInput = NoInput(0), + ): + self.kwargs = dict( + effective=effective, + termination=termination, + frequency=frequency, + stub=stub, + front_stub=front_stub, + back_stub=back_stub, + roll=roll, + eom=eom, + modifier=modifier, + calendar=calendar, + payment_lag=payment_lag, + notional=notional, + currency=currency, + amortization=amortization, + convention=convention, + leg2_effective=leg2_effective, + leg2_termination=leg2_termination, + leg2_frequency=leg2_frequency, + leg2_stub=leg2_stub, + leg2_front_stub=leg2_front_stub, + leg2_back_stub=leg2_back_stub, + leg2_roll=leg2_roll, + leg2_eom=leg2_eom, + leg2_modifier=leg2_modifier, + leg2_calendar=leg2_calendar, + leg2_payment_lag=leg2_payment_lag, + leg2_notional=leg2_notional, + leg2_currency=leg2_currency, + leg2_amortization=leg2_amortization, + leg2_convention=leg2_convention, + ) + self.kwargs = _push(spec, self.kwargs) + # set some defaults if missing + self.kwargs["notional"] = ( + defaults.notional + if self.kwargs["notional"] is NoInput.blank + else self.kwargs["notional"] + ) + if self.kwargs["payment_lag"] is NoInput.blank: + self.kwargs["payment_lag"] = defaults.payment_lag_specific[type(self).__name__] + self.kwargs = _inherit_or_negate(self.kwargs) # inherit or negate the complete arg list + + self.curves = curves + self.spec = spec + + # + # for attribute in [ + # "effective", + # "termination", + # "frequency", + # "stub", + # "front_stub", + # "back_stub", + # "roll", + # "eom", + # "modifier", + # "calendar", + # "payment_lag", + # "convention", + # "notional", + # "amortization", + # "currency", + # ]: + # leg2_val, val = self.kwargs[f"leg2_{attribute}"], self.kwargs[attribute] + # if leg2_val is NoInput.inherit: + # _ = val + # elif leg2_val == NoInput.negate: + # _ = NoInput(0) if val is NoInput(0) else val * -1 + # else: + # _ = leg2_val + # self.kwargs[attribute] = val + # self.kwargs[f"leg2_{attribute}"] = _ + # # setattr(self, attribute, val) + # # setattr(self, f"leg2_{attribute}", _) + + @abstractmethod + def _set_pricing_mid(self, *args, **kwargs): # pragma: no cover + pass + + def delta(self, *args, **kwargs): + """ + Calculate the delta of the *Instrument*. + + For arguments see :meth:`Sensitivities.delta()`. + """ + return super().delta(*args, **kwargs) + + def gamma(self, *args, **kwargs): + """ + Calculate the gamma of the *Instrument*. + + For arguments see :meth:`Sensitivities.gamma()`. + """ + return super().gamma(*args, **kwargs) + + +class IRS(BaseDerivative): + """ + Create an interest rate swap composing a :class:`~rateslib.legs.FixedLeg` + and a :class:`~rateslib.legs.FloatLeg`. + + Parameters + ---------- + args : dict + Required positional args to :class:`BaseDerivative`. + fixed_rate : float or None + The fixed rate applied to the :class:`~rateslib.legs.FixedLeg`. If `None` + will be set to mid-market when curves are provided. + leg2_float_spread : float, optional + The spread applied to the :class:`~rateslib.legs.FloatLeg`. Can be set to + `None` and designated + later, perhaps after a mid-market spread for all periods has been calculated. + leg2_spread_compound_method : str, optional + The method to use for adding a floating spread to compounded rates. Available + options are `{"none_simple", "isda_compounding", "isda_flat_compounding"}`. + leg2_fixings : float, list, or Series optional + If a float scalar, will be applied as the determined fixing for the first + period. If a list of *n* fixings will be used as the fixings for the first *n* + periods. If any sublist of length *m* is given, is used as the first *m* RFR + fixings for that :class:`~rateslib.periods.FloatPeriod`. If a datetime + indexed ``Series`` will use the fixings that are available in that object, + and derive the rest from the ``curve``. + leg2_fixing_method : str, optional + The method by which floating rates are determined, set by default. See notes. + leg2_method_param : int, optional + A parameter that is used for the various ``fixing_method`` s. See notes. + kwargs : dict + Required keyword arguments to :class:`BaseDerivative`. + + Notes + ------ + The various different ``leg2_fixing_methods``, which describe how an + individual *FloatPeriod* calculates its *rate*, are + fully documented in the notes for the :class:`~rateslib.periods.FloatPeriod`. + These configurations provide the mechanics to differentiate between IBOR swaps, and + OISs with different mechanisms such as *payment delay*, *observation shift*, + *lockout*, and/or *averaging*. + Similarly some information is provided in that same link regarding + ``leg2_fixings``, but a cookbook article is also produced for + :ref:`working with fixings `. + + Examples + -------- + Construct a curve to price the example. + + .. ipython:: python + + usd = Curve( + nodes={ + dt(2022, 1, 1): 1.0, + dt(2023, 1, 1): 0.965, + dt(2024, 1, 1): 0.94 + }, + id="usd" + ) + + Create the IRS, and demonstrate the :meth:`~rateslib.instruments.IRS.rate`, + :meth:`~rateslib.instruments.IRS.npv`, + :meth:`~rateslib.instruments.IRS.analytic_delta`, and + :meth:`~rateslib.instruments.IRS.spread`. + + .. ipython:: python + + irs = IRS( + effective=dt(2022, 1, 1), + termination="18M", + frequency="A", + calendar="nyc", + currency="usd", + fixed_rate=3.269, + convention="Act360", + notional=100e6, + curves=["usd"], + ) + irs.rate(curves=usd) + irs.npv(curves=usd) + irs.analytic_delta(curve=usd) + irs.spread(curves=usd) + + A DataFrame of :meth:`~rateslib.instruments.IRS.cashflows`. + + .. ipython:: python + + irs.cashflows(curves=usd) + + For accurate sensitivity calculations; :meth:`~rateslib.instruments.IRS.delta` + and :meth:`~rateslib.instruments.IRS.gamma`, construct a curve model. + + .. ipython:: python + + sofr_kws = dict( + effective=dt(2022, 1, 1), + frequency="A", + convention="Act360", + calendar="nyc", + currency="usd", + curves=["usd"] + ) + instruments = [ + IRS(termination="1Y", **sofr_kws), + IRS(termination="2Y", **sofr_kws), + ] + solver = Solver( + curves=[usd], + instruments=instruments, + s=[3.65, 3.20], + instrument_labels=["1Y", "2Y"], + id="sofr", + ) + irs.delta(solver=solver) + irs.gamma(solver=solver) + """ + + _fixed_rate_mixin = True + _leg2_float_spread_mixin = True + + def __init__( + self, + *args, + fixed_rate: float | NoInput = NoInput(0), + leg2_float_spread: float | NoInput = NoInput(0), + leg2_spread_compound_method: str | NoInput = NoInput(0), + leg2_fixings: float | list | Series | NoInput = NoInput(0), + leg2_fixing_method: str | NoInput = NoInput(0), + leg2_method_param: int | NoInput = NoInput(0), + **kwargs, + ): + super().__init__(*args, **kwargs) + user_kwargs = dict( + fixed_rate=fixed_rate, + leg2_float_spread=leg2_float_spread, + leg2_spread_compound_method=leg2_spread_compound_method, + leg2_fixings=leg2_fixings, + leg2_fixing_method=leg2_fixing_method, + leg2_method_param=leg2_method_param, + ) + self.kwargs = _update_not_noinput(self.kwargs, user_kwargs) + + self._fixed_rate = fixed_rate + self._leg2_float_spread = leg2_float_spread + self.leg1 = FixedLeg(**_get(self.kwargs, leg=1)) + self.leg2 = FloatLeg(**_get(self.kwargs, leg=2)) + + def _set_pricing_mid( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + ): + # the test for an unpriced IRS is that its fixed rate is not set. + if self.fixed_rate is NoInput.blank: + # set a fixed rate for the purpose of generic methods NPV will be zero. + mid_market_rate = self.rate(curves, solver) + self.leg1.fixed_rate = float(mid_market_rate) + + def analytic_delta(self, *args, **kwargs): + """ + Return the analytic delta of a leg of the derivative object. + + See :meth:`BaseDerivative.analytic_delta`. + """ + return super().analytic_delta(*args, **kwargs) + + def npv( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + local: bool = False, + ): + """ + Return the NPV of the derivative by summing legs. + + See :meth:`BaseDerivative.npv`. + """ + self._set_pricing_mid(curves, solver) + return super().npv(curves, solver, fx, base, local) + + def rate( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + ): + """ + Return the mid-market rate of the IRS. + + Parameters + ---------- + curves : Curve, str or list of such + A single :class:`~rateslib.curves.Curve` or id or a list of such. + A list defines the following curves in the order: + + - Forecasting :class:`~rateslib.curves.Curve` for floating leg. + - Discounting :class:`~rateslib.curves.Curve` for both legs. + solver : Solver, optional + The numerical :class:`~rateslib.solver.Solver` that + constructs :class:`~rateslib.curves.Curve` from calibrating instruments. + + .. note:: + + The arguments ``fx`` and ``base`` are unused by single currency + derivatives rates calculations. + + Returns + ------- + float, Dual or Dual2 + + Notes + ----- + The arguments ``fx`` and ``base`` are unused by single currency derivatives + rates calculations. + """ + curves, _, _ = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + fx, + base, + self.leg1.currency, + ) + leg2_npv = self.leg2.npv(curves[2], curves[3]) + return self.leg1._spread(-leg2_npv, curves[0], curves[1]) / 100 + # leg1_analytic_delta = self.leg1.analytic_delta(curves[0], curves[1]) + # return leg2_npv / (leg1_analytic_delta * 100) + + def cashflows( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + ): + """ + Return the properties of all legs used in calculating cashflows. + + See :meth:`BaseDerivative.cashflows`. + """ + self._set_pricing_mid(curves, solver) + return super().cashflows(curves, solver, fx, base) + + # Licence: Creative Commons - Attribution-NonCommercial-NoDerivatives 4.0 International + # Commercial use of this code, and/or copying and redistribution is prohibited. + # Contact rateslib at gmail.com if this code is observed outside its intended sphere. + + def spread( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + ): + """ + Return the mid-market float spread (bps) required to equate to the fixed rate. + + Parameters + ---------- + curves : Curve, str or list of such + A single :class:`~rateslib.curves.Curve` or id or a list of such. + A list defines the following curves in the order: + + - Forecasting :class:`~rateslib.curves.Curve` for floating leg. + - Discounting :class:`~rateslib.curves.Curve` for both legs. + solver : Solver, optional + The numerical :class:`~rateslib.solver.Solver` that constructs + :class:`~rateslib.curves.Curve` from calibrating instruments. + + .. note:: + + The arguments ``fx`` and ``base`` are unused by single currency + derivatives rates calculations. + + Returns + ------- + float, Dual or Dual2 + + Notes + ----- + If the :class:`IRS` is specified without a ``fixed_rate`` this should always + return the current ``leg2_float_spread`` value or zero since the fixed rate used + for calculation is the implied rate including the current ``leg2_float_spread`` + parameter. + + Examples + -------- + For the most common parameters this method will be exact. + + .. ipython:: python + + irs.spread(curves=usd) + irs.leg2_float_spread = -6.948753 + irs.npv(curves=usd) + + When a non-linear spread compound method is used for float RFR legs this is + an approximation, via second order Taylor expansion. + + .. ipython:: python + + irs = IRS( + effective=dt(2022, 2, 15), + termination=dt(2022, 8, 15), + frequency="Q", + convention="30e360", + leg2_convention="Act360", + leg2_fixing_method="rfr_payment_delay", + leg2_spread_compound_method="isda_compounding", + payment_lag=2, + fixed_rate=2.50, + leg2_float_spread=0, + notional=50000000, + currency="usd", + ) + irs.spread(curves=usd) + irs.leg2_float_spread = -111.060143 + irs.npv(curves=usd) + irs.spread(curves=usd) + + The ``leg2_float_spread`` is determined through NPV differences. If the difference + is small since the defined spread is already quite close to the solution the + approximation is much more accurate. This is shown above where the second call + to ``irs.spread`` is different to the previous call, albeit the difference + is 1/10000th of a basis point. + """ + irs_npv = self.npv(curves, solver) + specified_spd = 0 if self.leg2.float_spread is NoInput(0) else self.leg2.float_spread + curves, _, _ = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + fx, + base, + self.leg1.currency, + ) + return self.leg2._spread(-irs_npv, curves[2], curves[3]) + specified_spd + # leg2_analytic_delta = self.leg2.analytic_delta(curves[2], curves[3]) + # return irs_npv / leg2_analytic_delta + specified_spd + + +class STIRFuture(IRS): + """ + Create a short term interest rate (STIR) future. + + Parameters + ---------- + args : dict + Required positional args to :class:`BaseDerivative`. + price : float + The traded price of the future. Defined as 100 minus the fixed rate. + contracts : int + The number of traded contracts. + bp_value : float. + The value of 1bp on the contract as specified by the exchange, e.g. SOFR 3M futures are + $25 per bp. This is not the same as tick value where the tick size can be different across + different futures. + nominal : float + The nominal value of the contract. E.g. SOFR 3M futures are $1mm. If not given will use the + default notional. + fixed_rate : float or None + The fixed rate applied to the :class:`~rateslib.legs.FixedLeg`. If `None` + will be set to mid-market when curves are provided. + leg2_float_spread : float, optional + The spread applied to the :class:`~rateslib.legs.FloatLeg`. Can be set to + `None` and designated + later, perhaps after a mid-market spread for all periods has been calculated. + leg2_spread_compound_method : str, optional + The method to use for adding a floating spread to compounded rates. Available + options are `{"none_simple", "isda_compounding", "isda_flat_compounding"}`. + leg2_fixings : float, list, or Series optional + If a float scalar, will be applied as the determined fixing for the first + period. If a list of *n* fixings will be used as the fixings for the first *n* + periods. If any sublist of length *m* is given, is used as the first *m* RFR + fixings for that :class:`~rateslib.periods.FloatPeriod`. If a datetime + indexed ``Series`` will use the fixings that are available in that object, + and derive the rest from the ``curve``. + leg2_fixing_method : str, optional + The method by which floating rates are determined, set by default. See notes. + leg2_method_param : int, optional + A parameter that is used for the various ``fixing_method`` s. See notes. + kwargs : dict + Required keyword arguments to :class:`BaseDerivative`. + + Examples + -------- + Construct a curve to price the example. + + .. ipython:: python + + usd = Curve( + nodes={ + dt(2022, 1, 1): 1.0, + dt(2023, 1, 1): 0.965, + dt(2024, 1, 1): 0.94 + }, + id="usd_stir" + ) + + Create the *STIRFuture*, and demonstrate the :meth:`~rateslib.instruments.STIRFuture.rate`, + :meth:`~rateslib.instruments.STIRFuture.npv`, + + .. ipython:: python + + stir = STIRFuture( + effective=dt(2022, 3, 16), + termination=dt(2022, 6, 15), + spec="usd_stir", + curves=usd, + price=99.50, + contracts=10, + ) + stir.rate(metric="price") + stir.npv() + + """ + + _fixed_rate_mixin = True + _leg2_float_spread_mixin = True + + def __init__( + self, + *args, + price: float | NoInput = NoInput(0), + contracts: int = 1, + bp_value: float | NoInput = NoInput(0), + nominal: float | NoInput = NoInput(0), + leg2_float_spread: float | NoInput = NoInput(0), + leg2_spread_compound_method: str | NoInput = NoInput(0), + leg2_fixings: float | list | Series | NoInput = NoInput(0), + leg2_fixing_method: str | NoInput = NoInput(0), + leg2_method_param: int | NoInput = NoInput(0), + **kwargs, + ): + nominal = defaults.notional if nominal is NoInput.blank else nominal + # TODO this overwrite breaks positional arguments + kwargs["notional"] = nominal * contracts * -1.0 + super(IRS, self).__init__(*args, **kwargs) # call BaseDerivative.__init__() + user_kwargs = dict( + price=price, + fixed_rate=NoInput(0) if price is NoInput.blank else (100 - price), + leg2_float_spread=leg2_float_spread, + leg2_spread_compound_method=leg2_spread_compound_method, + leg2_fixings=leg2_fixings, + leg2_fixing_method=leg2_fixing_method, + leg2_method_param=leg2_method_param, + nominal=nominal, + bp_value=bp_value, + contracts=contracts, + ) + self.kwargs = _update_not_noinput(self.kwargs, user_kwargs) + + self._fixed_rate = self.kwargs["fixed_rate"] + self._leg2_float_spread = leg2_float_spread + self.leg1 = FixedLeg( + **_get(self.kwargs, leg=1, filter=["price", "nominal", "bp_value", "contracts"]), + ) + self.leg2 = FloatLeg(**_get(self.kwargs, leg=2)) + + def npv( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + local: bool = False, + ): + """ + Return the NPV of the derivative by summing legs. + + See :meth:`BaseDerivative.npv`. + """ + # the test for an unpriced IRS is that its fixed rate is not set. + mid_price = self.rate(curves, solver, fx, base, metric="price") + if self.fixed_rate is NoInput.blank: + # set a fixed rate for the purpose of generic methods NPV will be zero. + self.leg1.fixed_rate = float(100 - mid_price) + + traded_price = 100 - self.leg1.fixed_rate + _ = (mid_price - traded_price) * 100 * self.kwargs["contracts"] * self.kwargs["bp_value"] + if local: + return {self.leg1.currency: _} + else: + return _ + + def rate( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + metric: str = "rate", + ): + """ + Return the mid-market rate of the IRS. + + Parameters + ---------- + curves : Curve, str or list of such + A single :class:`~rateslib.curves.Curve` or id or a list of such. + A list defines the following curves in the order: + + - Forecasting :class:`~rateslib.curves.Curve` for floating leg. + - Discounting :class:`~rateslib.curves.Curve` for both legs. + solver : Solver, optional + The numerical :class:`~rateslib.solver.Solver` that + constructs :class:`~rateslib.curves.Curve` from calibrating instruments. + + .. note:: + + The arguments ``fx`` and ``base`` are unused by single currency + derivatives rates calculations. + metric : str in {"rate", "price"} + The calculation metric that will be returned. + + Returns + ------- + float, Dual or Dual2 + + Notes + ----- + The arguments ``fx`` and ``base`` are unused by single currency derivatives + rates calculations. + """ + curves, _, _ = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + fx, + base, + self.leg1.currency, + ) + leg2_npv = self.leg2.npv(curves[2], curves[3]) + + _ = self.leg1._spread(-leg2_npv, curves[0], curves[1]) / 100 + if metric.lower() == "rate": + return _ + elif metric.lower() == "price": + return 100 - _ + else: + raise ValueError("`metric` must be in {'price', 'rate'}.") + + def analytic_delta(self, *args, **kwargs): + """ + Return the analytic delta of the *STIRFuture*. + + See :meth:`BasePeriod.analytic_delta()`. + For *STIRFuture* this method requires no arguments. + """ + return -1.0 * self.kwargs["contracts"] * self.kwargs["bp_value"] + + def cashflows( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + ): + return DataFrame.from_records( + [ + { + defaults.headers["type"]: type(self).__name__, + defaults.headers["stub_type"]: "Regular", + defaults.headers["currency"]: self.leg1.currency.upper(), + defaults.headers["a_acc_start"]: self.leg1.schedule.effective, + defaults.headers["a_acc_end"]: self.leg1.schedule.termination, + defaults.headers["payment"]: None, + defaults.headers["convention"]: "Exchange", + defaults.headers["dcf"]: float(self.leg1.notional) + / self.kwargs["nominal"] + * self.kwargs["bp_value"] + / 100.0, + defaults.headers["notional"]: float(self.leg1.notional), + defaults.headers["df"]: 1.0, + defaults.headers["collateral"]: self.leg1.currency.lower(), + }, + ], + ) + + def spread(self): + """ + Not implemented for *STIRFuture*. + """ + return NotImplementedError() + + +# Licence: Creative Commons - Attribution-NonCommercial-NoDerivatives 4.0 International +# Commercial use of this code, and/or copying and redistribution is prohibited. +# This code cannot be installed or executed on a corporate computer without a paid licence extension +# Contact info at rateslib.com if this code is observed outside its intended sphere of use. + + +class IIRS(BaseDerivative): + """ + Create an indexed interest rate swap (IIRS) composing an + :class:`~rateslib.legs.IndexFixedLeg` and a :class:`~rateslib.legs.FloatLeg`. + + Parameters + ---------- + args : dict + Required positional args to :class:`BaseDerivative`. + fixed_rate : float or None + The fixed rate applied to the :class:`~rateslib.legs.ZeroFixedLeg`. If `None` + will be set to mid-market when curves are provided. + index_base : float or None, optional + The base index applied to all periods. + index_fixings : float, or Series, optional + If a float scalar, will be applied as the index fixing for the first + period. + If a list of *n* fixings will be used as the index fixings for the first *n* + periods. + If a datetime indexed ``Series`` will use the fixings that are available in + that object, and derive the rest from the ``curve``. + index_method : str + Whether the indexing uses a daily measure for settlement or the most recently + monthly data taken from the first day of month. + index_lag : int, optional + The number of months by which the index value is lagged. Used to ensure + consistency between curves and forecast values. Defined by default. + notional_exchange : bool, optional + Whether the legs include final notional exchanges and interim + amortization notional exchanges. + kwargs : dict + Required keyword arguments to :class:`BaseDerivative`. + + Examples + -------- + Construct a curve to price the example. + + .. ipython:: python + + usd = Curve( + nodes={ + dt(2022, 1, 1): 1.0, + dt(2027, 1, 1): 0.85, + dt(2032, 1, 1): 0.65, + }, + id="usd", + ) + us_cpi = IndexCurve( + nodes={ + dt(2022, 1, 1): 1.0, + dt(2027, 1, 1): 0.85, + dt(2032, 1, 1): 0.70, + }, + id="us_cpi", + index_base=100, + index_lag=3, + ) + + Create the IIRS, and demonstrate the :meth:`~rateslib.instruments.IIRS.rate`, and + :meth:`~rateslib.instruments.IIRS.npv`. + + .. ipython:: python + + iirs = IIRS( + effective=dt(2022, 1, 1), + termination="4Y", + frequency="A", + calendar="nyc", + currency="usd", + fixed_rate=2.05, + convention="1+", + notional=100e6, + index_base=100.0, + index_method="monthly", + index_lag=3, + notional_exchange=True, + leg2_convention="Act360", + curves=["us_cpi", "usd", "usd", "usd"], + ) + iirs.rate(curves=[us_cpi, usd, usd, usd]) + iirs.npv(curves=[us_cpi, usd, usd, usd]) + + A DataFrame of :meth:`~rateslib.instruments.IIRS.cashflows`. + + .. ipython:: python + + iirs.cashflows(curves=[us_cpi, usd, usd, usd]) + + For accurate sensitivity calculations; :meth:`~rateslib.instruments.IIRS.delta` + and :meth:`~rateslib.instruments.IIRS.gamma`, construct a curve model. + + .. ipython:: python + + sofr_kws = dict( + effective=dt(2022, 1, 1), + frequency="A", + convention="Act360", + calendar="nyc", + currency="usd", + curves=["usd"] + ) + cpi_kws = dict( + effective=dt(2022, 1, 1), + frequency="A", + convention="1+", + calendar="nyc", + leg2_index_method="monthly", + currency="usd", + curves=["usd", "usd", "us_cpi", "usd"] + ) + instruments = [ + IRS(termination="5Y", **sofr_kws), + IRS(termination="10Y", **sofr_kws), + ZCIS(termination="5Y", **cpi_kws), + ZCIS(termination="10Y", **cpi_kws), + ] + solver = Solver( + curves=[usd, us_cpi], + instruments=instruments, + s=[3.40, 3.60, 2.2, 2.05], + instrument_labels=["5Y", "10Y", "5Yi", "10Yi"], + id="us", + ) + iirs.delta(solver=solver) + iirs.gamma(solver=solver) + """ + + _fixed_rate_mixin = True + _index_base_mixin = True + _leg2_float_spread_mixin = True + + def __init__( + self, + *args, + fixed_rate: float | NoInput = NoInput(0), + index_base: float | Series | NoInput = NoInput(0), + index_fixings: float | Series | NoInput = NoInput(0), + index_method: str | NoInput = NoInput(0), + index_lag: int | NoInput = NoInput(0), + notional_exchange: bool | NoInput = False, + payment_lag_exchange: int | NoInput = NoInput(0), + leg2_float_spread: float | NoInput = NoInput(0), + leg2_fixings: float | list | NoInput = NoInput(0), + leg2_fixing_method: str | NoInput = NoInput(0), + leg2_method_param: int | NoInput = NoInput(0), + leg2_spread_compound_method: str | NoInput = NoInput(0), + leg2_payment_lag_exchange: int | NoInput = NoInput(1), + **kwargs, + ): + super().__init__(*args, **kwargs) + if leg2_payment_lag_exchange is NoInput.inherit: + leg2_payment_lag_exchange = payment_lag_exchange + user_kwargs = dict( + fixed_rate=fixed_rate, + index_base=index_base, + index_fixings=index_fixings, + index_method=index_method, + index_lag=index_lag, + initial_exchange=False, + final_exchange=notional_exchange, + payment_lag_exchange=payment_lag_exchange, + leg2_float_spread=leg2_float_spread, + leg2_spread_compound_method=leg2_spread_compound_method, + leg2_fixings=leg2_fixings, + leg2_fixing_method=leg2_fixing_method, + leg2_method_param=leg2_method_param, + leg2_payment_lag_exchange=leg2_payment_lag_exchange, + leg2_initial_exchange=False, + leg2_final_exchange=notional_exchange, + ) + self.kwargs = _update_not_noinput(self.kwargs, user_kwargs) + + self._index_base = self.kwargs["index_base"] + self._fixed_rate = self.kwargs["fixed_rate"] + self.leg1 = IndexFixedLeg(**_get(self.kwargs, leg=1)) + self.leg2 = FloatLeg(**_get(self.kwargs, leg=2)) + + def _set_pricing_mid( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + ): + mid_market_rate = self.rate(curves, solver) + self.leg1.fixed_rate = float(mid_market_rate) + + def npv( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + local: bool = False, + ): + curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + fx, + base, + self.leg1.currency, + ) + if self.index_base is NoInput.blank: + # must forecast for the leg + self.leg1.index_base = curves[0].index_value( + self.leg1.schedule.effective, + self.leg1.index_method, + ) + if self.fixed_rate is NoInput.blank: + # set a fixed rate for the purpose of pricing NPV, which should be zero. + self._set_pricing_mid(curves, solver) + return super().npv(curves, solver, fx_, base_, local) + + def cashflows( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + ): + curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + fx, + base, + self.leg1.currency, + ) + if self.index_base is NoInput.blank: + # must forecast for the leg + self.leg1.index_base = curves[0].index_value( + self.leg1.schedule.effective, + self.leg1.index_method, + ) + if self.fixed_rate is NoInput.blank: + # set a fixed rate for the purpose of pricing NPV, which should be zero. + self._set_pricing_mid(curves, solver) + return super().cashflows(curves, solver, fx_, base_) + + def rate( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + ): + """ + Return the mid-market rate of the IRS. + + Parameters + ---------- + curves : Curve, str or list of such + A single :class:`~rateslib.curves.Curve` or id or a list of such. + A list defines the following curves in the order: + + - Forecasting :class:`~rateslib.curves.Curve` for floating leg. + - Discounting :class:`~rateslib.curves.Curve` for both legs. + solver : Solver, optional + The numerical :class:`~rateslib.solver.Solver` that + constructs :class:`~rateslib.curves.Curve` from calibrating instruments. + + .. note:: + + The arguments ``fx`` and ``base`` are unused by single currency + derivatives rates calculations. + + Returns + ------- + float, Dual or Dual2 + + Notes + ----- + The arguments ``fx`` and ``base`` are unused by single currency derivatives + rates calculations. + """ + curves, _, _ = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + fx, + base, + self.leg1.currency, + ) + if self.index_base is NoInput.blank: + # must forecast for the leg + self.leg1.index_base = curves[0].index_value( + self.leg1.schedule.effective, + self.leg1.index_method, + ) + leg2_npv = self.leg2.npv(curves[2], curves[3]) + + if self.fixed_rate is NoInput.blank: + self.leg1.fixed_rate = 0.0 + _existing = self.leg1.fixed_rate + leg1_npv = self.leg1.npv(curves[0], curves[1]) + + _ = self.leg1._spread(-leg2_npv - leg1_npv, curves[0], curves[1]) / 100 + return _ + _existing + + def spread( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + ): + """ + Return the mid-market float spread (bps) required to equate to the fixed rate. + + Parameters + ---------- + curves : Curve, str or list of such + A single :class:`~rateslib.curves.Curve` or id or a list of such. + A list defines the following curves in the order: + + - Forecasting :class:`~rateslib.curves.Curve` for floating leg. + - Discounting :class:`~rateslib.curves.Curve` for both legs. + solver : Solver, optional + The numerical :class:`~rateslib.solver.Solver` that constructs + :class:`~rateslib.curves.Curve` from calibrating instruments. + + .. note:: + + The arguments ``fx`` and ``base`` are unused by single currency + derivatives rates calculations. + + Returns + ------- + float, Dual or Dual2 + + Notes + ----- + If the :class:`IRS` is specified without a ``fixed_rate`` this should always + return the current ``leg2_float_spread`` value or zero since the fixed rate used + for calculation is the implied rate including the current ``leg2_float_spread`` + parameter. + + Examples + -------- + For the most common parameters this method will be exact. + + .. ipython:: python + + irs.spread(curves=usd) + irs.leg2_float_spread = -6.948753 + irs.npv(curves=usd) + + When a non-linear spread compound method is used for float RFR legs this is + an approximation, via second order Taylor expansion. + + .. ipython:: python + + irs = IRS( + effective=dt(2022, 2, 15), + termination=dt(2022, 8, 15), + frequency="Q", + convention="30e360", + leg2_convention="Act360", + leg2_fixing_method="rfr_payment_delay", + leg2_spread_compound_method="isda_compounding", + payment_lag=2, + fixed_rate=2.50, + leg2_float_spread=0, + notional=50000000, + currency="usd", + ) + irs.spread(curves=usd) + irs.leg2_float_spread = -111.060143 + irs.npv(curves=usd) + irs.spread(curves=usd) + + The ``leg2_float_spread`` is determined through NPV differences. If the difference + is small since the defined spread is already quite close to the solution the + approximation is much more accurate. This is shown above where the second call + to ``irs.spread`` is different to the previous call, albeit the difference + is 1/10000th of a basis point. + """ + irs_npv = self.npv(curves, solver) + specified_spd = 0 if self.leg2.float_spread is NoInput.blank else self.leg2.float_spread + curves, _, _ = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + fx, + base, + self.leg1.currency, + ) + return self.leg2._spread(-irs_npv, curves[2], curves[3]) + specified_spd + + +class ZCS(BaseDerivative): + """ + Create a zero coupon swap (ZCS) composing a :class:`~rateslib.legs.ZeroFixedLeg` + and a :class:`~rateslib.legs.ZeroFloatLeg`. + + Parameters + ---------- + args : dict + Required positional args to :class:`BaseDerivative`. + fixed_rate : float or None + The fixed rate applied to the :class:`~rateslib.legs.ZeroFixedLeg`. If `None` + will be set to mid-market when curves are provided. + leg2_float_spread : float, optional + The spread applied to the :class:`~rateslib.legs.FloatLeg`. Can be set to + `None` and designated + later, perhaps after a mid-market spread for all periods has been calculated. + leg2_spread_compound_method : str, optional + The method to use for adding a floating spread to compounded rates. Available + options are `{"none_simple", "isda_compounding", "isda_flat_compounding"}`. + leg2_fixings : float, list, or Series optional + If a float scalar, will be applied as the determined fixing for the first + period. If a list of *n* fixings will be used as the fixings for the first *n* + periods. If any sublist of length *m* is given, is used as the first *m* RFR + fixings for that :class:`~rateslib.periods.FloatPeriod`. If a datetime + indexed ``Series`` will use the fixings that are available in that object, + and derive the rest from the ``curve``. + leg2_fixing_method : str, optional + The method by which floating rates are determined, set by default. See notes. + leg2_method_param : int, optional + A parameter that is used for the various ``fixing_method`` s. See notes. + kwargs : dict + Required keyword arguments to :class:`BaseDerivative`. + + Examples + -------- + Construct a curve to price the example. + + .. ipython:: python + + usd = Curve( + nodes={ + dt(2022, 1, 1): 1.0, + dt(2027, 1, 1): 0.85, + dt(2032, 1, 1): 0.70, + }, + id="usd" + ) + + Create the ZCS, and demonstrate the :meth:`~rateslib.instruments.ZCS.rate`, + :meth:`~rateslib.instruments.ZCS.npv`, + :meth:`~rateslib.instruments.ZCS.analytic_delta`, and + + .. ipython:: python + + zcs = ZCS( + effective=dt(2022, 1, 1), + termination="10Y", + frequency="Q", + calendar="nyc", + currency="usd", + fixed_rate=4.0, + convention="Act360", + notional=100e6, + curves=["usd"], + ) + zcs.rate(curves=usd) + zcs.npv(curves=usd) + zcs.analytic_delta(curve=usd) + + A DataFrame of :meth:`~rateslib.instruments.ZCS.cashflows`. + + .. ipython:: python + + zcs.cashflows(curves=usd) + + For accurate sensitivity calculations; :meth:`~rateslib.instruments.ZCS.delta` + and :meth:`~rateslib.instruments.ZCS.gamma`, construct a curve model. + + .. ipython:: python + + sofr_kws = dict( + effective=dt(2022, 1, 1), + frequency="A", + convention="Act360", + calendar="nyc", + currency="usd", + curves=["usd"] + ) + instruments = [ + IRS(termination="5Y", **sofr_kws), + IRS(termination="10Y", **sofr_kws), + ] + solver = Solver( + curves=[usd], + instruments=instruments, + s=[3.40, 3.60], + instrument_labels=["5Y", "10Y"], + id="sofr", + ) + zcs.delta(solver=solver) + zcs.gamma(solver=solver) + """ + + _fixed_rate_mixin = True + _leg2_float_spread_mixin = True + + def __init__( + self, + *args, + fixed_rate: float | NoInput = NoInput(0), + leg2_float_spread: float | NoInput = NoInput(0), + leg2_spread_compound_method: str | NoInput = NoInput(0), + leg2_fixings: float | list | Series | NoInput = NoInput(0), + leg2_fixing_method: str | NoInput = NoInput(0), + leg2_method_param: int | NoInput = NoInput(0), + **kwargs, + ): + super().__init__(*args, **kwargs) + user_kwargs = dict( + fixed_rate=fixed_rate, + leg2_float_spread=leg2_float_spread, + leg2_spread_compound_method=leg2_spread_compound_method, + leg2_fixings=leg2_fixings, + leg2_fixing_method=leg2_fixing_method, + leg2_method_param=leg2_method_param, + ) + self.kwargs = _update_not_noinput(self.kwargs, user_kwargs) + self._fixed_rate = fixed_rate + self._leg2_float_spread = leg2_float_spread + self.leg1 = ZeroFixedLeg(**_get(self.kwargs, leg=1)) + self.leg2 = ZeroFloatLeg(**_get(self.kwargs, leg=2)) + + def analytic_delta(self, *args, **kwargs): + """ + Return the analytic delta of a leg of the derivative object. + + See + :meth:`BaseDerivative.analytic_delta`. + """ + return super().analytic_delta(*args, **kwargs) + + def _set_pricing_mid(self, curves, solver): + if self.fixed_rate is NoInput.blank: + # set a fixed rate for the purpose of pricing NPV, which should be zero. + mid_market_rate = self.rate(curves, solver) + self.leg1.fixed_rate = float(mid_market_rate) + + def npv( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + local: bool = False, + ): + """ + Return the NPV of the derivative by summing legs. + + See :meth:`BaseDerivative.npv`. + """ + self._set_pricing_mid(curves, solver) + return super().npv(curves, solver, fx, base, local) + + def rate( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + ): + """ + Return the mid-market rate of the ZCS. + + Parameters + ---------- + curves : Curve, str or list of such + A single :class:`~rateslib.curves.Curve` or id or a list of such. + A list defines the following curves in the order: + + - Forecasting :class:`~rateslib.curves.Curve` for floating leg. + - Discounting :class:`~rateslib.curves.Curve` for both legs. + solver : Solver, optional + The numerical :class:`~rateslib.solver.Solver` that + constructs :class:`~rateslib.curves.Curve` from calibrating instruments. + + .. note:: + + The arguments ``fx`` and ``base`` are unused by single currency + derivatives rates calculations. + + Returns + ------- + float, Dual or Dual2 + + Notes + ----- + The arguments ``fx`` and ``base`` are unused by single currency derivatives + rates calculations. + + The *'irr'* ``fixed_rate`` defines a cashflow by: + + .. math:: + + -notional * ((1 + irr / f)^{f \\times dcf} - 1) + + where :math:`f` is associated with the compounding frequency. + """ + curves, _, _ = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + fx, + base, + self.leg1.currency, + ) + leg2_npv = self.leg2.npv(curves[2], curves[3]) + _ = self.leg1._spread(-leg2_npv, curves[0], curves[1]) / 100 + return _ + + def cashflows( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + ): + """ + Return the properties of all legs used in calculating cashflows. + + See :meth:`BaseDerivative.cashflows`. + """ + self._set_pricing_mid(curves, solver) + return super().cashflows(curves, solver, fx, base) + + +class ZCIS(BaseDerivative): + """ + Create a zero coupon index swap (ZCIS) composing an + :class:`~rateslib.legs.ZeroFixedLeg` + and a :class:`~rateslib.legs.ZeroIndexLeg`. + + Parameters + ---------- + args : dict + Required positional args to :class:`BaseDerivative`. + fixed_rate : float or None + The fixed rate applied to the :class:`~rateslib.legs.ZeroFixedLeg`. If `None` + will be set to mid-market when curves are provided. + index_base : float or None, optional + The base index applied to all periods. + index_fixings : float, or Series, optional + If a float scalar, will be applied as the index fixing for the first + period. + If a list of *n* fixings will be used as the index fixings for the first *n* + periods. + If a datetime indexed ``Series`` will use the fixings that are available in + that object, and derive the rest from the ``curve``. + index_method : str + Whether the indexing uses a daily measure for settlement or the most recently + monthly data taken from the first day of month. + index_lag : int, optional + The number of months by which the index value is lagged. Used to ensure + consistency between curves and forecast values. Defined by default. + kwargs : dict + Required keyword arguments to :class:`BaseDerivative`. + + Examples + -------- + Construct a curve to price the example. + + .. ipython:: python + + usd = Curve( + nodes={ + dt(2022, 1, 1): 1.0, + dt(2027, 1, 1): 0.85, + dt(2032, 1, 1): 0.65, + }, + id="usd", + ) + us_cpi = IndexCurve( + nodes={ + dt(2022, 1, 1): 1.0, + dt(2027, 1, 1): 0.85, + dt(2032, 1, 1): 0.70, + }, + id="us_cpi", + index_base=100, + index_lag=3, + ) + + Create the ZCIS, and demonstrate the :meth:`~rateslib.instruments.ZCIS.rate`, + :meth:`~rateslib.instruments.ZCIS.npv`, + :meth:`~rateslib.instruments.ZCIS.analytic_delta`, and + + .. ipython:: python + + zcis = ZCIS( + effective=dt(2022, 1, 1), + termination="10Y", + frequency="A", + calendar="nyc", + currency="usd", + fixed_rate=2.05, + convention="1+", + notional=100e6, + leg2_index_base=100.0, + leg2_index_method="monthly", + leg2_index_lag=3, + curves=["usd", "usd", "us_cpi", "usd"], + ) + zcis.rate(curves=[usd, usd, us_cpi, usd]) + zcis.npv(curves=[usd, usd, us_cpi, usd]) + zcis.analytic_delta(usd, usd) + + A DataFrame of :meth:`~rateslib.instruments.ZCIS.cashflows`. + + .. ipython:: python + + zcis.cashflows(curves=[usd, usd, us_cpi, usd]) + + For accurate sensitivity calculations; :meth:`~rateslib.instruments.ZCIS.delta` + and :meth:`~rateslib.instruments.ZCIS.gamma`, construct a curve model. + + .. ipython:: python + + sofr_kws = dict( + effective=dt(2022, 1, 1), + frequency="A", + convention="Act360", + calendar="nyc", + currency="usd", + curves=["usd"] + ) + cpi_kws = dict( + effective=dt(2022, 1, 1), + frequency="A", + convention="1+", + calendar="nyc", + leg2_index_method="monthly", + currency="usd", + curves=["usd", "usd", "us_cpi", "usd"] + ) + instruments = [ + IRS(termination="5Y", **sofr_kws), + IRS(termination="10Y", **sofr_kws), + ZCIS(termination="5Y", **cpi_kws), + ZCIS(termination="10Y", **cpi_kws), + ] + solver = Solver( + curves=[usd, us_cpi], + instruments=instruments, + s=[3.40, 3.60, 2.2, 2.05], + instrument_labels=["5Y", "10Y", "5Yi", "10Yi"], + id="us", + ) + zcis.delta(solver=solver) + zcis.gamma(solver=solver) + """ + + _fixed_rate_mixin = True + _leg2_index_base_mixin = True + + def __init__( + self, + *args, + fixed_rate: float | NoInput = NoInput(0), + leg2_index_base: float | Series | NoInput = NoInput(0), + leg2_index_fixings: float | Series | NoInput = NoInput(0), + leg2_index_method: str | NoInput = NoInput(0), + leg2_index_lag: int | NoInput = NoInput(0), + **kwargs, + ): + super().__init__(*args, **kwargs) + user_kwargs = dict( + fixed_rate=fixed_rate, + leg2_index_base=leg2_index_base, + leg2_index_fixings=leg2_index_fixings, + leg2_index_lag=leg2_index_lag, + leg2_index_method=leg2_index_method, + ) + self.kwargs = _update_not_noinput(self.kwargs, user_kwargs) + self._fixed_rate = fixed_rate + self._leg2_index_base = leg2_index_base + self.leg1 = ZeroFixedLeg(**_get(self.kwargs, leg=1)) + self.leg2 = ZeroIndexLeg(**_get(self.kwargs, leg=2)) + + def _set_pricing_mid(self, curves, solver): + if self.fixed_rate is NoInput.blank: + # set a fixed rate for the purpose of pricing NPV, which should be zero. + mid_market_rate = self.rate(curves, solver) + self.leg1.fixed_rate = float(mid_market_rate) + + def cashflows( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + ): + self._set_pricing_mid(curves, solver) + return super().cashflows(curves, solver, fx, base) + + def npv( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + local: bool = False, + ): + self._set_pricing_mid(curves, solver) + return super().npv(curves, solver, fx, base, local) + + def rate( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + ): + """ + Return the mid-market IRR rate of the ZCIS. + + Parameters + ---------- + curves : Curve, str or list of such + A single :class:`~rateslib.curves.Curve` or id or a list of such. + A list defines the following curves in the order: + + - Forecasting :class:`~rateslib.curves.Curve` for floating leg. + - Discounting :class:`~rateslib.curves.Curve` for both legs. + solver : Solver, optional + The numerical :class:`~rateslib.solver.Solver` that + constructs :class:`~rateslib.curves.Curve` from calibrating instruments. + + .. note:: + + The arguments ``fx`` and ``base`` are unused by single currency + derivatives rates calculations. + + Returns + ------- + float, Dual or Dual2 + + Notes + ----- + The arguments ``fx`` and ``base`` are unused by single currency derivatives + rates calculations. + """ + curves, _, _ = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + fx, + base, + self.leg1.currency, + ) + if self.leg2_index_base is NoInput.blank: + # must forecast for the leg + forecast_value = curves[2].index_value( + self.leg2.schedule.effective, + self.leg2.index_method, + ) + if abs(forecast_value) < 1e-13: + raise ValueError( + "Forecasting the `index_base` for the ZCIS yielded 0.0, which is infeasible.\n" + "This might occur if the ZCIS starts in the past, or has a 'monthly' " + "`index_method` which uses the 1st day of the effective month, which is in the " + "past.\nA known `index_base` value should be input with the ZCIS " + "specification.", + ) + self.leg2.index_base = forecast_value + leg2_npv = self.leg2.npv(curves[2], curves[3]) + + return self.leg1._spread(-leg2_npv, curves[0], curves[1]) / 100 + + +# Licence: Creative Commons - Attribution-NonCommercial-NoDerivatives 4.0 International +# Commercial use of this code, and/or copying and redistribution is prohibited. +# This code cannot be installed or executed on a corporate computer without a paid licence extension +# Contact info at rateslib.com if this code is observed outside its intended sphere of use. + + +class SBS(BaseDerivative): + """ + Create a single currency basis swap composing two + :class:`~rateslib.legs.FloatLeg` s. + + Parameters + ---------- + args : tuple + Required positional args to :class:`BaseDerivative`. + float_spread : float, optional + The spread applied to the :class:`~rateslib.legs.FloatLeg`. Can be set to + `None` and designated + later, perhaps after a mid-market spread for all periods has been calculated. + spread_compound_method : str, optional + The method to use for adding a floating spread to compounded rates. Available + options are `{"none_simple", "isda_compounding", "isda_flat_compounding"}`. + fixings : float, list, or Series optional + If a float scalar, will be applied as the determined fixing for the first + period. If a list of *n* fixings will be used as the fixings for the first *n* + periods. If any sublist of length *m* is given, is used as the first *m* RFR + fixings for that :class:`~rateslib.periods.FloatPeriod`. If a datetime + indexed ``Series`` will use the fixings that are available in that object, + and derive the rest from the ``curve``. + fixing_method : str, optional + The method by which floating rates are determined, set by default. See notes. + method_param : int, optional + A parameter that is used for the various ``fixing_method`` s. See notes. + leg2_float_spread : float or None + The floating spread applied in a simple way (after daily compounding) to the + second :class:`~rateslib.legs.FloatLeg`. If `None` will be set to zero. + float_spread : float, optional + The spread applied to the :class:`~rateslib.legs.FloatLeg`. Can be set to + `None` and designated + later, perhaps after a mid-market spread for all periods has been calculated. + leg2_spread_compound_method : str, optional + The method to use for adding a floating spread to compounded rates. Available + options are `{"none_simple", "isda_compounding", "isda_flat_compounding"}`. + leg2_fixings : float, list, or Series optional + If a float scalar, will be applied as the determined fixing for the first + period. If a list of *n* fixings will be used as the fixings for the first *n* + periods. If any sublist of length *m* is given, is used as the first *m* RFR + fixings for that :class:`~rateslib.periods.FloatPeriod`. If a datetime + indexed ``Series`` will use the fixings that are available in that object, + and derive the rest from the ``curve``. + leg2_fixing_method : str, optional + The method by which floating rates are determined, set by default. See notes. + leg2_method_param : int, optional + A parameter that is used for the various ``fixing_method`` s. See notes. + kwargs : dict + Required keyword arguments to :class:`BaseDerivative`. + + Examples + -------- + Construct curves to price the example. + + .. ipython:: python + + eur3m = Curve( + nodes={ + dt(2022, 1, 1): 1.0, + dt(2023, 1, 1): 0.965, + dt(2024, 1, 1): 0.94 + }, + id="eur3m", + ) + eur6m = Curve( + nodes={ + dt(2022, 1, 1): 1.0, + dt(2023, 1, 1): 0.962, + dt(2024, 1, 1): 0.936 + }, + id="eur6m", + ) + + Create the SBS, and demonstrate the :meth:`~rateslib.instruments.SBS.rate`, + :meth:`~rateslib.instruments.SBS.npv`, + :meth:`~rateslib.instruments.SBS.analytic_delta`, and + :meth:`~rateslib.instruments.SBS.spread`. + + .. ipython:: python + + sbs = SBS( + effective=dt(2022, 1, 1), + termination="18M", + frequency="Q", + leg2_frequency="S", + calendar="tgt", + currency="eur", + fixing_method="ibor", + method_param=2, + convention="Act360", + leg2_float_spread=-22.9, + notional=100e6, + curves=["eur3m", "eur3m", "eur6m", "eur3m"], + ) + sbs.rate(curves=[eur3m, eur3m, eur6m, eur3m]) + sbs.npv(curves=[eur3m, eur3m, eur6m, eur3m]) + sbs.analytic_delta(curve=eur6m, disc_curve=eur3m, leg=2) + sbs.spread(curves=[eur3m, eur3m, eur6m, eur3m], leg=2) + + A DataFrame of :meth:`~rateslib.instruments.SBS.cashflows`. + + .. ipython:: python + + sbs.cashflows(curves=[eur3m, eur3m, eur6m, eur3m]) + + For accurate sensitivity calculations; :meth:`~rateslib.instruments.SBS.delta` + and :meth:`~rateslib.instruments.SBS.gamma`, construct a curve model. + + .. ipython:: python + + irs_kws = dict( + effective=dt(2022, 1, 1), + frequency="A", + leg2_frequency="Q", + convention="30E360", + leg2_convention="Act360", + leg2_fixing_method="ibor", + leg2_method_param=2, + calendar="tgt", + currency="eur", + curves=["eur3m", "eur3m"], + ) + sbs_kws = dict( + effective=dt(2022, 1, 1), + frequency="Q", + leg2_frequency="S", + convention="Act360", + fixing_method="ibor", + method_param=2, + leg2_convention="Act360", + calendar="tgt", + currency="eur", + curves=["eur3m", "eur3m", "eur6m", "eur3m"] + ) + instruments = [ + IRS(termination="1Y", **irs_kws), + IRS(termination="2Y", **irs_kws), + SBS(termination="1Y", **sbs_kws), + SBS(termination="2Y", **sbs_kws), + ] + solver = Solver( + curves=[eur3m, eur6m], + instruments=instruments, + s=[1.55, 1.6, 5.5, 6.5], + instrument_labels=["1Y", "2Y", "1Y 3s6s", "2Y 3s6s"], + id="eur", + ) + sbs.delta(solver=solver) + sbs.gamma(solver=solver) + + """ + + _float_spread_mixin = True + _leg2_float_spread_mixin = True + _rate_scalar = 100.0 + + def __init__( + self, + *args, + float_spread: float | NoInput = NoInput(0), + spread_compound_method: str | NoInput = NoInput(0), + fixings: float | list | Series | NoInput = NoInput(0), + fixing_method: str | NoInput = NoInput(0), + method_param: int | NoInput = NoInput(0), + leg2_float_spread: float | NoInput = NoInput(0), + leg2_spread_compound_method: str | NoInput = NoInput(0), + leg2_fixings: float | list | Series | NoInput = NoInput(0), + leg2_fixing_method: str | NoInput = NoInput(0), + leg2_method_param: int | NoInput = NoInput(0), + **kwargs, + ): + super().__init__(*args, **kwargs) + user_kwargs = dict( + float_spread=float_spread, + spread_compound_method=spread_compound_method, + fixings=fixings, + fixing_method=fixing_method, + method_param=method_param, + leg2_float_spread=leg2_float_spread, + leg2_spread_compound_method=leg2_spread_compound_method, + leg2_fixings=leg2_fixings, + leg2_fixing_method=leg2_fixing_method, + leg2_method_param=leg2_method_param, + ) + self.kwargs = _update_not_noinput(self.kwargs, user_kwargs) + self._float_spread = float_spread + self._leg2_float_spread = leg2_float_spread + self.leg1 = FloatLeg(**_get(self.kwargs, leg=1)) + self.leg2 = FloatLeg(**_get(self.kwargs, leg=2)) + + def _set_pricing_mid(self, curves, solver): + if self.float_spread is NoInput.blank and self.leg2_float_spread is NoInput.blank: + # set a pricing parameter for the purpose of pricing NPV at zero. + rate = self.rate(curves, solver) + self.leg1.float_spread = float(rate) + + def analytic_delta(self, *args, **kwargs): + """ + Return the analytic delta of a leg of the derivative object. + + See :meth:`BaseDerivative.analytic_delta`. + """ + return super().analytic_delta(*args, **kwargs) + + def cashflows( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + ): + """ + Return the properties of all legs used in calculating cashflows. + + See :meth:`BaseDerivative.cashflows`. + """ + self._set_pricing_mid(curves, solver) + return super().cashflows(curves, solver, fx, base) + + def npv( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + local: bool = False, + ): + """ + Return the NPV of the derivative object by summing legs. + + See :meth:`BaseDerivative.npv`. + """ + self._set_pricing_mid(curves, solver) + return super().npv(curves, solver, fx, base, local) + + def rate( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + leg: int = 1, + ): + """ + Return the mid-market float spread on the specified leg of the SBS. + + Parameters + ---------- + curves : Curve, str or list of such + A list defines the following curves in the order: + + - Forecasting :class:`~rateslib.curves.Curve` for floating leg1. + - Discounting :class:`~rateslib.curves.Curve` for both legs. + - Forecasting :class:`~rateslib.curves.Curve` for floating leg2. + solver : Solver, optional + The numerical :class:`~rateslib.solver.Solver` that constructs + :class:`~rateslib.curves.Curve` from calibrating + instruments. + leg: int in [1, 2] + Specify which leg the spread calculation is applied to. + + Returns + ------- + float, Dual or Dual2 + """ + core_npv = super().npv(curves, solver) + curves, _, _ = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + fx, + base, + self.leg1.currency, + ) + if leg == 1: + leg_obj, args = self.leg1, (curves[0], curves[1]) + else: + leg_obj, args = self.leg2, (curves[2], curves[3]) + + specified_spd = 0 if leg_obj.float_spread is NoInput.blank else leg_obj.float_spread + return leg_obj._spread(-core_npv, *args) + specified_spd + + # irs_npv = self.npv(curves, solver) + # curves, _ = self._get_curves_and_fx_maybe_from_solver(solver, curves, None) + # if leg == 1: + # args = (curves[0], curves[1]) + # else: + # args = (curves[2], curves[3]) + # leg_analytic_delta = getattr(self, f"leg{leg}").analytic_delta(*args) + # adjust = getattr(self, f"leg{leg}").float_spread + # adjust = 0 if adjust is NoInput.blank else adjust + # _ = irs_npv / leg_analytic_delta + adjust + # return _ + + def spread(self, *args, **kwargs): + """ + Return the mid-market float spread on the specified leg of the SBS. + + Alias for :meth:`~rateslib.instruments.SBS.rate`. + """ + return self.rate(*args, **kwargs) + + +class FRA(Sensitivities, BaseMixin): + """ + Create a forward rate agreement composing single period :class:`~rateslib.legs.FixedLeg` + and :class:`~rateslib.legs.FloatLeg` valued in a customised manner. + + Parameters + ---------- + args : dict + Required positional args to :class:`BaseDerivative`. + fixed_rate : float or None + The fixed rate applied to the :class:`~rateslib.legs.FixedLeg`. If `None` + will be set to mid-market when curves are provided. + fixings : float or list, optional + If a float scalar, will be applied as the determined fixing for the first + period. If a list of *n* fixings will be used as the fixings for the first *n* + periods. If any sublist of length *m* is given as the first *m* RFR fixings + within individual curve and composed into the overall rate. + method_param : int, optional + A parameter that is used for the various ``fixing_method`` s. See notes. + kwargs : dict + Required keyword arguments to :class:`BaseDerivative`. + + Notes + ----- + FRAs are a legacy derivative whose *FloatLeg* ``fixing_method`` is set to *"ibor"*. + + Examples + -------- + Construct curves to price the example. + + .. ipython:: python + + eur3m = Curve( + nodes={ + dt(2022, 1, 1): 1.0, + dt(2023, 1, 1): 0.965, + dt(2024, 1, 1): 0.94 + }, + id="eur3m", + ) + + Create the FRA, and demonstrate the :meth:`~rateslib.instruments.FRA.rate`, + :meth:`~rateslib.instruments.FRA.npv`, + :meth:`~rateslib.instruments.FRA.analytic_delta`. + + .. ipython:: python + + fra = FRA( + effective=dt(2023, 2, 15), + termination="3M", + frequency="Q", + calendar="tgt", + currency="eur", + method_param=2, + convention="Act360", + notional=100e6, + fixed_rate=2.617, + curves=["eur3m"], + ) + fra.rate(curves=eur3m) + fra.npv(curves=eur3m) + fra.analytic_delta(curve=eur3m) + + A DataFrame of :meth:`~rateslib.instruments.FRA.cashflows`. + + .. ipython:: python + + fra.cashflows(curves=eur3m) + + For accurate sensitivity calculations; :meth:`~rateslib.instruments.FRA.delta` + and :meth:`~rateslib.instruments.FRA.gamma`, construct a curve model. + + .. ipython:: python + + irs_kws = dict( + effective=dt(2022, 1, 1), + frequency="A", + leg2_frequency="Q", + convention="30E360", + leg2_convention="Act360", + leg2_fixing_method="ibor", + leg2_method_param=2, + calendar="tgt", + currency="eur", + curves=["eur3m", "eur3m"], + ) + instruments = [ + IRS(termination="1Y", **irs_kws), + IRS(termination="2Y", **irs_kws), + ] + solver = Solver( + curves=[eur3m], + instruments=instruments, + s=[1.55, 1.6], + instrument_labels=["1Y", "2Y"], + id="eur", + ) + fra.delta(solver=solver) + fra.gamma(solver=solver) + + """ + + _fixed_rate_mixin = True + + def __init__( + self, + effective: datetime | NoInput = NoInput(0), + termination: datetime | str | NoInput = NoInput(0), + frequency: int | NoInput = NoInput(0), + roll: str | int | NoInput = NoInput(0), + eom: bool | NoInput = NoInput(0), + modifier: str | None | NoInput = NoInput(0), + calendar: CalInput = NoInput(0), + payment_lag: int | NoInput = NoInput(0), + notional: float | NoInput = NoInput(0), + currency: str | NoInput = NoInput(0), + convention: str | NoInput = NoInput(0), + method_param: int | NoInput = NoInput(0), + fixed_rate: float | NoInput = NoInput(0), + fixings: float | Series | NoInput = NoInput(0), + curves: str | list | Curve | NoInput = NoInput(0), + spec: str | NoInput = NoInput(0), + ) -> None: + self.kwargs = dict( + effective=effective, + termination=termination, + frequency=_upper(frequency), + roll=roll, + eom=eom, + modifier=_upper(modifier), + calendar=calendar, + payment_lag=payment_lag, + notional=notional, + currency=_lower(currency), + convention=_upper(convention), + fixed_rate=fixed_rate, + leg2_effective=NoInput(1), + leg2_termination=NoInput(1), + leg2_convention=NoInput(1), + leg2_frequency=NoInput(1), + leg2_notional=NoInput(-1), + leg2_modifier=NoInput(1), + leg2_currency=NoInput(1), + leg2_calendar=NoInput(1), + leg2_roll=NoInput(1), + leg2_eom=NoInput(1), + leg2_payment_lag=NoInput(1), + leg2_fixing_method="ibor", + leg2_method_param=method_param, + leg2_spread_compound_method="none_simple", + leg2_fixings=fixings, + ) + self.kwargs = _push(spec, self.kwargs) + + # set defaults for missing values + default_kwargs = dict( + notional=defaults.notional, + payment_lag=defaults.payment_lag_specific[type(self).__name__], + currency=defaults.base_currency, + modifier=defaults.modifier, + eom=defaults.eom, + convention=defaults.convention, + ) + self.kwargs = _update_with_defaults(self.kwargs, default_kwargs) + self.kwargs = _inherit_or_negate(self.kwargs) + + # Build + self.curves = curves + + self._fixed_rate = self.kwargs["fixed_rate"] + self.leg1 = FixedLeg(**_get(self.kwargs, leg=1)) + self.leg2 = FloatLeg(**_get(self.kwargs, leg=2)) + + if self.leg1.schedule.n_periods != 1 or self.leg2.schedule.n_periods != 1: + raise ValueError("FRA scheduling inputs did not define a single period.") + + def _set_pricing_mid( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + ) -> None: + if self.fixed_rate is NoInput.blank: + mid_market_rate = self.rate(curves, solver) + self.leg1.fixed_rate = mid_market_rate.real + + def analytic_delta( + self, + curve: Curve, + disc_curve: Curve | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + ) -> DualTypes: + """ + Return the analytic delta of the FRA. + + For arguments see :meth:`~rateslib.periods.BasePeriod.analytic_delta`. + """ + disc_curve_: Curve = _disc_from_curve(curve, disc_curve) + fx, base = _get_fx_and_base(self.leg1.currency, fx, base) + rate = self.rate([curve]) + _ = ( + self.leg1.notional + * self.leg1.periods[0].dcf + * disc_curve_[self.leg1.schedule.pschedule[0]] + / 10000 + ) + return fx * _ / (1 + self.leg1.periods[0].dcf * rate / 100) + + def npv( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + local: bool = False, + ) -> DualTypes: + """ + Return the NPV of the derivative. + + See :meth:`BaseDerivative.npv`. + """ + + self._set_pricing_mid(curves, solver) + curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + fx, + base, + self.leg1.currency, + ) + fx, base = _get_fx_and_base(self.leg1.currency, fx_, base_) + value = self.cashflow(curves[0]) * curves[1][self.leg1.schedule.pschedule[0]] + if local: + return {self.leg1.currency: value} + else: + return fx * value + + def rate( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + ) -> DualTypes: + """ + Return the mid-market rate of the FRA. + + Only the forecasting curve is required to price an FRA. + + Parameters + ---------- + curves : Curve, str or list of such + A single :class:`~rateslib.curves.Curve` or id or a list of such. + A list defines the following curves in the order: + + - Forecasting :class:`~rateslib.curves.Curve` for floating leg. + - Discounting :class:`~rateslib.curves.Curve` for floating leg. + solver : Solver, optional + The numerical :class:`~rateslib.solver.Solver` that + constructs :class:`~rateslib.curves.Curve` from calibrating instruments. + fx : unused + base : unused + + Returns + ------- + float, Dual or Dual2 + """ + curves, _, _ = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + fx, + base, + self.leg1.currency, + ) + return self.leg2.periods[0].rate(curves[0]) + + def cashflow(self, curve: Curve | LineCurve): + """ + Calculate the local currency cashflow on the FRA from current floating rate + and fixed rate. + + Parameters + ---------- + curve : Curve or LineCurve, + The forecasting curve for determining the floating rate. + + Returns + ------- + float, Dual or Dual2 + """ + cf1 = self.leg1.periods[0].cashflow + cf2 = self.leg2.periods[0].cashflow(curve) + if cf1 is not NoInput.blank and cf2 is not NoInput.blank: + cf = cf1 + cf2 + else: + return None + rate = ( + None + if curve is NoInput.blank + else 100 * cf2 / (-self.leg2.notional * self.leg2.periods[0].dcf) + ) + cf /= 1 + self.leg1.periods[0].dcf * rate / 100 + + # if self.fixed_rate is NoInput.blank: + # return 0 # set the fixed rate = to floating rate netting to zero + # rate = self.leg2.rate(curve) + # cf = self.notional * self.leg1.dcf * (rate - self.fixed_rate) / 100 + # cf /= 1 + self.leg1.dcf * rate / 100 + return cf + + def cashflows( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + ) -> DataFrame: + """ + Return the properties of the leg used in calculating cashflows. + + Parameters + ---------- + args : + Positional arguments supplied to :meth:`~rateslib.periods.BasePeriod.cashflows`. + kwargs : + Keyword arguments supplied to :meth:`~rateslib.periods.BasePeriod.cashflows`. + + Returns + ------- + DataFrame + """ + self._set_pricing_mid(curves, solver) + curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + fx, + base, + self.leg1.currency, + ) + fx_, base_ = _get_fx_and_base(self.leg1.currency, fx_, base_) + + cf = float(self.cashflow(curves[0])) + df = float(curves[1][self.leg1.schedule.pschedule[0]]) + npv_local = cf * df + + _fix = None if self.fixed_rate is NoInput.blank else -float(self.fixed_rate) + _spd = None if curves[1] is NoInput.blank else -float(self.rate(curves[1])) * 100 + cfs = self.leg1.periods[0].cashflows(curves[0], curves[1], fx_, base_) + cfs[defaults.headers["type"]] = "FRA" + cfs[defaults.headers["payment"]] = self.leg1.schedule.pschedule[0] + cfs[defaults.headers["cashflow"]] = cf + cfs[defaults.headers["rate"]] = _fix + cfs[defaults.headers["spread"]] = _spd + cfs[defaults.headers["npv"]] = npv_local + cfs[defaults.headers["df"]] = df + cfs[defaults.headers["fx"]] = float(fx_) + cfs[defaults.headers["npv_fx"]] = npv_local * float(fx_) + return DataFrame.from_records([cfs]) + + def delta(self, *args, **kwargs): + """ + Calculate the delta of the *Instrument*. + + For arguments see :meth:`Sensitivities.delta()`. + """ + return super().delta(*args, **kwargs) + + def gamma(self, *args, **kwargs): + """ + Calculate the gamma of the *Instrument*. + + For arguments see :meth:`Sensitivities.gamma()`. + """ + return super().gamma(*args, **kwargs) diff --git a/python/rateslib/instruments/rates_multi_ccy.py b/python/rateslib/instruments/rates_multi_ccy.py new file mode 100644 index 00000000..151875d5 --- /dev/null +++ b/python/rateslib/instruments/rates_multi_ccy.py @@ -0,0 +1,1131 @@ +from __future__ import annotations + +import warnings +from datetime import datetime + +from pandas import DataFrame, MultiIndex, Series + +from rateslib import defaults +from rateslib.curves import Curve +from rateslib.default import NoInput +from rateslib.dual import Dual, Dual2, DualTypes +from rateslib.fx import FXForwards, FXRates, forward_fx +from rateslib.instruments.core import ( + BaseMixin, + Sensitivities, + _get, + _get_curves_fx_and_base_maybe_from_solver, + _update_not_noinput, +) +from rateslib.instruments.rates_derivatives import BaseDerivative +from rateslib.legs import ( + FixedLeg, + FixedLegMtm, + FloatLeg, + FloatLegMtm, +) +from rateslib.periods import ( + Cashflow, +) +from rateslib.solver import Solver + +# Licence: Creative Commons - Attribution-NonCommercial-NoDerivatives 4.0 International +# Commercial use of this code, and/or copying and redistribution is prohibited. +# This code cannot be installed or executed on a corporate computer without a paid licence extension +# Contact info at rateslib.com if this code is observed outside its intended sphere of use. + + +class FXExchange(Sensitivities, BaseMixin): + """ + Create a simple exchange of two currencies. + + Parameters + ---------- + settlement : datetime + The date of the currency exchange. + pair: str + The curreny pair of the exchange, e.g. "eurusd", using 3-digit iso codes. + fx_rate : float, optional + The FX rate used to derive the notional exchange on *Leg2*. + notional : float + The cashflow amount of the LHS currency. + curves : Curve, LineCurve, str or list of such, optional + For *FXExchange* only discounting curves are required in each currency and not rate + forecasting curves. + The signature should be: `[None, eur_curve, None, usd_curve]` for a "eurusd" pair. + """ + + def __init__( + self, + settlement: datetime, + pair: str, + fx_rate: float | NoInput = NoInput(0), + notional: float | NoInput = NoInput(0), + curves: list | str | Curve | NoInput = NoInput(0), + ): + self.curves = curves + self.settlement = settlement + self.pair = pair.lower() + self.leg1 = Cashflow( + notional=-defaults.notional if notional is NoInput.blank else -notional, + currency=self.pair[0:3], + payment=settlement, + stub_type="Exchange", + rate=NoInput(0), + ) + self.leg2 = Cashflow( + notional=1.0, # will be determined by setting fx_rate + currency=self.pair[3:6], + payment=settlement, + stub_type="Exchange", + rate=fx_rate, + ) + self.fx_rate = fx_rate + + @property + def fx_rate(self): + return self._fx_rate + + @fx_rate.setter + def fx_rate(self, value): + self._fx_rate = value + self.leg2.notional = 0.0 if value is NoInput.blank else value * -self.leg1.notional + self.leg2._rate = value + + def _set_pricing_mid( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + ): + if self.fx_rate is NoInput.blank: + mid_market_rate = self.rate(curves, solver, fx) + self.fx_rate = float(mid_market_rate) + self._fx_rate = NoInput(0) + + def npv( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + local: bool = False, + ): + """ + Return the NPV of the *FXExchange* by summing legs. + + For arguments see :meth:`BaseMixin.npv` + """ + self._set_pricing_mid(curves, solver, fx) + + curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + fx, + base, + self.leg1.currency, + ) + + if fx_ is NoInput.blank: + raise ValueError( + "Must have some FX information to price FXExchange, either `fx` or " + "`solver` containing an FX object.", + ) + if not isinstance(fx_, (FXRates, FXForwards)): + # force base_ leg1 currency to be converted consistent. + leg1_npv = self.leg1.npv(curves[0], curves[1], fx_, base_, local) + leg2_npv = self.leg2.npv(curves[2], curves[3], 1.0, base_, local) + warnings.warn( + "When valuing multi-currency derivatives it not best practice to " + "supply `fx` as numeric.\nYour input:\n" + f"`npv(solver={'None' if solver is NoInput.blank else ''}, " + f"fx={fx}, base='{base if base is not NoInput.blank else 'None'}')\n" + "has been implicitly converted into the following by this operation:\n" + f"`npv(solver={'None' if solver is NoInput.blank else ''}, " + f"fx=FXRates({{'{self.leg2.currency}{self.leg1.currency}: {fx}}}), " + f"base='{self.leg2.currency}')\n.", + UserWarning, + ) + else: + leg1_npv = self.leg1.npv(curves[0], curves[1], fx_, base_, local) + leg2_npv = self.leg2.npv(curves[2], curves[3], fx_, base_, local) + + if local: + return { + k: leg1_npv.get(k, 0) + leg2_npv.get(k, 0) for k in set(leg1_npv) | set(leg2_npv) + } + else: + return leg1_npv + leg2_npv + + def cashflows( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + ): + """ + Return the cashflows of the *FXExchange* by aggregating legs. + + For arguments see :meth:`BaseMixin.npv` + """ + self._set_pricing_mid(curves, solver, fx) + curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + fx, + base, + NoInput(0), + ) + seq = [ + self.leg1.cashflows(curves[0], curves[1], fx_, base_), + self.leg2.cashflows(curves[2], curves[3], fx_, base_), + ] + _ = DataFrame.from_records(seq) + _.index = MultiIndex.from_tuples([("leg1", 0), ("leg2", 0)]) + return _ + + def rate( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: float | FXRates | FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + ): + """ + Return the mid-market rate of the instrument. + + For arguments see :meth:`BaseMixin.rate` + """ + curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + fx, + base, + self.leg1.currency, + ) + if isinstance(fx_, (FXRates, FXForwards)): + imm_fx = fx_.rate(self.pair) + else: + imm_fx = fx_ + + if imm_fx is NoInput.blank: + raise ValueError( + "`fx` must be supplied to price FXExchange object.\n" + "Note: it can be attached to and then gotten from a Solver.", + ) + _ = forward_fx(self.settlement, curves[1], curves[3], imm_fx) + return _ + + def delta(self, *args, **kwargs): + """ + Calculate the delta of the *Instrument*. + + For arguments see :meth:`Sensitivities.delta()`. + """ + return super().delta(*args, **kwargs) + + def gamma(self, *args, **kwargs): + """ + Calculate the gamma of the *Instrument*. + + For arguments see :meth:`Sensitivities.gamma()`. + """ + return super().gamma(*args, **kwargs) + + def analytic_delta(self, *args, **kwargs): + raise NotImplementedError("`analytic_delta` for FXExchange not defined.") + + +class XCS(BaseDerivative): + """ + Create a cross-currency swap (XCS) composing relevant fixed or floating *Legs*. + + MTM-XCSs will introduce a MTM *Leg* as *Leg2*. + + .. warning:: + + ``leg2_notional`` is unused by *XCS*. That notional is always dynamically determined by + ``fx_fixings``, i.e. an initial FX fixing and/or forecast forward FX rates if ``leg2_mtm`` + is set to *True*. See also the parameter definition for ``fx_fixings``. + + Parameters + ---------- + args : tuple + Required positional arguments for :class:`~rateslib.instruments.BaseDerivative`. + fixed : bool, optional + Whether *leg1* is fixed or floating rate. Defaults to *False*. + payment_lag_exchange : int + The number of business days by which to delay notional exchanges, aligned with + the accrual schedule. + fixed_rate : float, optional + If ``fixed``, the fixed rate of *leg1*. + float_spread : float, optional + If not ``fixed``, the spread applied to the :class:`~rateslib.legs.FloatLeg`. Can be set to + `None` and designated + later, perhaps after a mid-market spread for all periods has been calculated. + spread_compound_method : str, optional + If not ``fixed``, the method to use for adding a floating spread to compounded rates. + Available options are `{"none_simple", "isda_compounding", "isda_flat_compounding"}`. + fixings : float, list, or Series optional + If not ``fixed``, then if a float scalar, will be applied as the determined fixing for + the first period. If a list of *n* fixings will be used as the fixings for the first *n* + periods. If any sublist of length *m* is given, is used as the first *m* RFR + fixings for that :class:`~rateslib.periods.FloatPeriod`. If a datetime + indexed ``Series`` will use the fixings that are available in that object, + and derive the rest from the ``curve``. + fixing_method : str, optional + If not ``fixed``, the method by which floating rates are determined, set by default. + See notes. + method_param : int, optional + If not ``fixed`` A parameter that is used for the various ``fixing_method`` s. See notes. + leg2_fixed : bool, optional + Whether *leg2* is fixed or floating rate. Defaults to *False* + leg2_mtm : bool optional + Whether *leg2* is a mark-to-market leg. Defaults to *True* + leg2_payment_lag_exchange : int + The number of business days by which to delay notional exchanges, aligned with + the accrual schedule. + leg2_fixed_rate : float, optional + If ``leg2_fixed``, the fixed rate of *leg2*. + leg2_float_spread : float, optional + If not ``leg2_fixed``, the spread applied to the :class:`~rateslib.legs.FloatLeg`. + Can be set to `None` and designated + later, perhaps after a mid-market spread for all periods has been calculated. + leg2_spread_compound_method : str, optional + If not ``leg2_fixed``, the method to use for adding a floating spread to compounded rates. + Available options are `{"none_simple", "isda_compounding", "isda_flat_compounding"}`. + leg2_fixings : float, list, or Series optional + If not ``leg2_fixed``, then if a float scalar, will be applied as the determined fixing for + the first period. If a list of *n* fixings will be used as the fixings for the first *n* + periods. If any sublist of length *m* is given, is used as the first *m* RFR + fixings for that :class:`~rateslib.periods.FloatPeriod`. If a datetime + indexed ``Series`` will use the fixings that are available in that object, + and derive the rest from the ``curve``. + leg2_fixing_method : str, optional + If not ``leg2_fixed``, the method by which floating rates are determined, set by default. + See notes. + leg2_method_param : int, optional + If not ``leg2_fixed`` A parameter that is used for the various ``fixing_method`` s. + See notes. + fx_fixings : float, Dual, Dual2, list of such, optional + Specify a known initial FX fixing or a list of such for ``mtm`` legs, where leg 1 is + considered the domestic currency. For example for an ESTR/SOFR XCS in 100mm EUR notional + a value of 1.10 EURUSD for fx_fixings implies the notional on leg 2 is 110m USD. Fixings + that are not specified will be forecast at pricing time with an + :class:`~rateslib.fx.FXForwards` object. + kwargs : dict + Required keyword arguments for :class:`~rateslib.instruments.BaseDerivative`. + """ + + def __init__( + self, + *args, + fixed: bool | NoInput = NoInput(0), + payment_lag_exchange: int | NoInput = NoInput(0), + fixed_rate: float | NoInput = NoInput(0), + float_spread: float | NoInput = NoInput(0), + spread_compound_method: str | NoInput = NoInput(0), + fixings: float | list | Series | NoInput = NoInput(0), + fixing_method: str | NoInput = NoInput(0), + method_param: int | NoInput = NoInput(0), + leg2_fixed: bool | NoInput = NoInput(0), + leg2_mtm: bool | NoInput = NoInput(0), + leg2_payment_lag_exchange: int | NoInput = NoInput(1), + leg2_fixed_rate: float | NoInput = NoInput(0), + leg2_float_spread: float | NoInput = NoInput(0), + leg2_fixings: float | list | NoInput = NoInput(0), + leg2_fixing_method: str | NoInput = NoInput(0), + leg2_method_param: int | NoInput = NoInput(0), + leg2_spread_compound_method: str | NoInput = NoInput(0), + fx_fixings: list | DualTypes | FXRates | FXForwards | NoInput = NoInput(0), + **kwargs, + ): + super().__init__(*args, **kwargs) + # set defaults for missing values + default_kwargs = dict( + fixed=False if fixed is NoInput.blank else fixed, + leg2_fixed=False if leg2_fixed is NoInput.blank else leg2_fixed, + leg2_mtm=True if leg2_mtm is NoInput.blank else leg2_mtm, + ) + self.kwargs = _update_not_noinput(self.kwargs, default_kwargs) + + if self.kwargs["fixed"]: + self._fixed_rate_mixin = True + self._fixed_rate = fixed_rate + leg1_user_kwargs = dict(fixed_rate=fixed_rate) + Leg1 = FixedLeg + else: + self._rate_scalar = 100.0 + self._float_spread_mixin = True + self._float_spread = float_spread + leg1_user_kwargs = dict( + float_spread=float_spread, + spread_compound_method=spread_compound_method, + fixings=fixings, + fixing_method=fixing_method, + method_param=method_param, + ) + Leg1 = FloatLeg + leg1_user_kwargs.update( + dict( + payment_lag_exchange=payment_lag_exchange, + initial_exchange=True, + final_exchange=True, + ), + ) + + if leg2_payment_lag_exchange is NoInput.inherit: + leg2_payment_lag_exchange = payment_lag_exchange + if self.kwargs["leg2_fixed"]: + self._leg2_fixed_rate_mixin = True + self._leg2_fixed_rate = leg2_fixed_rate + leg2_user_kwargs = dict(leg2_fixed_rate=leg2_fixed_rate) + Leg2 = FixedLeg if not leg2_mtm else FixedLegMtm + else: + self._leg2_float_spread_mixin = True + self._leg2_float_spread = leg2_float_spread + leg2_user_kwargs = dict( + leg2_float_spread=leg2_float_spread, + leg2_spread_compound_method=leg2_spread_compound_method, + leg2_fixings=leg2_fixings, + leg2_fixing_method=leg2_fixing_method, + leg2_method_param=leg2_method_param, + ) + Leg2 = FloatLeg if not leg2_mtm else FloatLegMtm + leg2_user_kwargs.update( + dict( + leg2_payment_lag_exchange=leg2_payment_lag_exchange, + leg2_initial_exchange=True, + leg2_final_exchange=True, + ), + ) + + if self.kwargs["leg2_mtm"]: + self._is_mtm = True + leg2_user_kwargs.update( + dict( + leg2_alt_currency=self.kwargs["currency"], + leg2_alt_notional=-self.kwargs["notional"], + leg2_fx_fixings=fx_fixings, + ), + ) + else: + self._is_mtm = False + + self.kwargs = _update_not_noinput(self.kwargs, {**leg1_user_kwargs, **leg2_user_kwargs}) + + self.leg1 = Leg1(**_get(self.kwargs, leg=1, filter=["fixed"])) + self.leg2 = Leg2(**_get(self.kwargs, leg=2, filter=["leg2_fixed", "leg2_mtm"])) + self._initialise_fx_fixings(fx_fixings) + + @property + def fx_fixings(self): + return self._fx_fixings + + @fx_fixings.setter + def fx_fixings(self, value): + self._fx_fixings = value + self._set_leg2_notional(value) + + def _initialise_fx_fixings(self, fx_fixings): + """ + Sets the `fx_fixing` for non-mtm XCS instruments, which require only a single + value. + """ + if not self._is_mtm: + self.pair = self.leg1.currency + self.leg2.currency + # if self.fx_fixing is NoInput.blank this indicates the swap is unfixed and will be set + # later. If a fixing is given this means the notional is fixed without any + # further sensitivity, hence the downcast to a float below. + if isinstance(fx_fixings, FXForwards): + self.fx_fixings = float(fx_fixings.rate(self.pair, self.leg2.periods[0].payment)) + elif isinstance(fx_fixings, FXRates): + self.fx_fixings = float(fx_fixings.rate(self.pair)) + elif isinstance(fx_fixings, (float, Dual, Dual2)): + self.fx_fixings = float(fx_fixings) + else: + self._fx_fixings = NoInput(0) + else: + self._fx_fixings = fx_fixings + + def _set_fx_fixings(self, fx): + """ + Checks the `fx_fixings` and sets them according to given object if null. + + Used by ``rate`` and ``npv`` methods when ``fx_fixings`` are not + initialised but required for pricing and can be inferred from an FX object. + """ + if not self._is_mtm: # then we manage the initial FX from the pricing object. + if self.fx_fixings is NoInput.blank: + if fx is NoInput.blank: + if defaults.no_fx_fixings_for_xcs.lower() == "raise": + raise ValueError( + "`fx` is required when `fx_fixings` is not pre-set and " + "if rateslib option `no_fx_fixings_for_xcs` is set to " + "'raise'.", + ) + else: + fx_fixing = 1.0 + if defaults.no_fx_fixings_for_xcs.lower() == "warn": + warnings.warn( + "Using 1.0 for FX, no `fx` or `fx_fixings` given and " + "rateslib option `no_fx_fixings_for_xcs` is set to " + "'warn'.", + UserWarning, + ) + else: + if isinstance(fx, FXForwards): + # this is the correct pricing path + fx_fixing = fx.rate(self.pair, self.leg2.periods[0].payment) + elif isinstance(fx, FXRates): + # maybe used in debugging + fx_fixing = fx.rate(self.pair) + else: + # possible float used in debugging also + fx_fixing = fx + self._set_leg2_notional(fx_fixing) + else: + self._set_leg2_notional(fx) + + def _set_leg2_notional(self, fx_arg: float | FXForwards): + """ + Update the notional on leg2 (foreign leg) if the initial fx rate is unfixed. + + ---------- + fx_arg : float or FXForwards + For non-MTM XCSs this input must be a float. + The FX rate to use as the initial notional fixing. + Will only update the leg if ``NonMtmXCS.fx_fixings`` has been initially + set to `None`. + + For MTM XCSs this input must be ``FXForwards``. + The FX object from which to determine FX rates used as the initial + notional fixing, and to determine MTM cashflow exchanges. + """ + if self._is_mtm: + self.leg2._set_periods(fx_arg) + self.leg2_notional = self.leg2.notional + else: + self.leg2_notional = self.leg1.notional * -fx_arg + self.leg2.notional = self.leg2_notional + if self.kwargs["amortization"] is not NoInput.blank: + self.leg2_amortization = self.leg1.amortization * -fx_arg + self.leg2.amortization = self.leg2_amortization + + @property + def _is_unpriced(self): + if getattr(self, "_unpriced", None) is True: + return True + if self._fixed_rate_mixin and self._leg2_fixed_rate_mixin: + # Fixed/Fixed where one leg is unpriced. + if self.fixed_rate is NoInput.blank or self.leg2_fixed_rate is NoInput.blank: # noqa: SIM103 + return True # noqa: SIM103 + return False # noqa: SIM103 + elif self._fixed_rate_mixin and self.fixed_rate is NoInput.blank: + # Fixed/Float where fixed leg is unpriced + return True + elif self._float_spread_mixin and self.float_spread is NoInput.blank: + # Float leg1 where leg1 is + pass # goto 2) + else: + return False + + # 2) leg1 is Float + if self._leg2_fixed_rate_mixin and self.leg2_fixed_rate is NoInput.blank: # noqa: SIM114, SIM103 + return True # noqa: SIM114, SIM103 + elif self._leg2_float_spread_mixin and self.leg2_float_spread is NoInput.blank: # noqa: SIM114, SIM103 + return True # noqa: SIM114, SIM103 + else: # noqa: SIM114, SIM103 + return False # noqa: SIM114, SIM103 + + def _set_pricing_mid( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: FXForwards | NoInput = NoInput(0), + ): + leg: int = 1 + lookup = { + 1: ["_fixed_rate_mixin", "_float_spread_mixin"], + 2: ["_leg2_fixed_rate_mixin", "_leg2_float_spread_mixin"], + } + if self._leg2_fixed_rate_mixin and self.leg2_fixed_rate is NoInput.blank: + # Fixed/Fixed or Float/Fixed + leg = 2 + + rate = self.rate(curves, solver, fx, leg=leg) + if getattr(self, lookup[leg][0]): + getattr(self, f"leg{leg}").fixed_rate = float(rate) + elif getattr(self, lookup[leg][1]): + getattr(self, f"leg{leg}").float_spread = float(rate) + else: + # this line should not be hit: internal code check + raise AttributeError("BaseXCS leg1 must be defined fixed or float.") # pragma: no cover + + def npv( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + local: bool = False, + ): + """ + Return the NPV of the derivative by summing legs. + + .. warning:: + + If ``fx_fixing`` has not been set for the instrument requires + ``fx`` as an FXForwards object to dynamically determine this. + + See :meth:`BaseDerivative.npv`. + """ + curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + fx, + base, + self.leg1.currency, + ) + + if self._is_unpriced: + self._set_pricing_mid(curves, solver, fx_) + + self._set_fx_fixings(fx_) + if self._is_mtm: + self.leg2._do_not_repeat_set_periods = True + + ret = super().npv(curves, solver, fx_, base_, local) + if self._is_mtm: + self.leg2._do_not_repeat_set_periods = False # reset for next calculation + return ret + + def rate( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: FXForwards | NoInput = NoInput(0), + leg: int = 1, + ): + """ + Return the mid-market pricing parameter of the XCS. + + Parameters + ---------- + curves : list of Curves + A list defines the following curves in the order: + + - Forecasting :class:`~rateslib.curves.Curve` for leg1 (if floating). + - Discounting :class:`~rateslib.curves.Curve` for leg1. + - Forecasting :class:`~rateslib.curves.Curve` for leg2 (if floating). + - Discounting :class:`~rateslib.curves.Curve` for leg2. + solver : Solver, optional + The numerical :class:`~rateslib.solver.Solver` that + constructs :class:`~rateslib.curves.Curve` from calibrating instruments. + fx : FXForwards, optional + The FX forwards object that is used to determine the initial FX fixing for + determining ``leg2_notional``, if not specified at initialisation, and for + determining mark-to-market exchanges on mtm XCSs. + leg : int in [1, 2] + The leg whose pricing parameter is to be determined. + + Returns + ------- + float, Dual or Dual2 + + Notes + ----- + Fixed legs have pricing parameter returned in percentage terms, and + float legs have pricing parameter returned in basis point (bp) terms. + + If the ``XCS`` type is specified without a ``fixed_rate`` on any leg then an + implied ``float_spread`` will return as its originaly value or zero since + the fixed rate used + for calculation is the implied mid-market rate including the + current ``float_spread`` parameter. + + Examples + -------- + """ + curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + fx, + NoInput(0), + self.leg1.currency, + ) + + if leg == 1: + tgt_fore_curve, tgt_disc_curve = curves[0], curves[1] + alt_fore_curve, alt_disc_curve = curves[2], curves[3] + else: + tgt_fore_curve, tgt_disc_curve = curves[2], curves[3] + alt_fore_curve, alt_disc_curve = curves[0], curves[1] + + leg2 = 1 if leg == 2 else 2 + # tgt_str, alt_str = "" if leg == 1 else "leg2_", "" if leg2 == 1 else "leg2_" + tgt_leg, alt_leg = getattr(self, f"leg{leg}"), getattr(self, f"leg{leg2}") + base_ = tgt_leg.currency + + _is_float_tgt_leg = "Float" in type(tgt_leg).__name__ + _is_float_alt_leg = "Float" in type(alt_leg).__name__ + if not _is_float_alt_leg and alt_leg.fixed_rate is NoInput.blank: + raise ValueError( + "Cannot solve for a `fixed_rate` or `float_spread` where the " + "`fixed_rate` on the non-solvable leg is NoInput.blank.", + ) + + # Licence: Creative Commons - Attribution-NonCommercial-NoDerivatives 4.0 International + # Commercial use of this code, and/or copying and redistribution is prohibited. + # Contact rateslib at gmail.com if this code is observed outside its intended sphere. + + if not _is_float_tgt_leg: + tgt_leg_fixed_rate = tgt_leg.fixed_rate + if tgt_leg_fixed_rate is NoInput.blank: + # set the target fixed leg to a null fixed rate for calculation + tgt_leg.fixed_rate = 0.0 + else: + # set the fixed rate to a float for calculation and no Dual Type crossing PR: XXX + tgt_leg.fixed_rate = float(tgt_leg_fixed_rate) + + self._set_fx_fixings(fx_) + if self._is_mtm: + self.leg2._do_not_repeat_set_periods = True + + tgt_leg_npv = tgt_leg.npv(tgt_fore_curve, tgt_disc_curve, fx_, base_) + alt_leg_npv = alt_leg.npv(alt_fore_curve, alt_disc_curve, fx_, base_) + fx_a_delta = 1.0 if not tgt_leg._is_mtm else fx_ + _ = tgt_leg._spread( + -(tgt_leg_npv + alt_leg_npv), + tgt_fore_curve, + tgt_disc_curve, + fx_a_delta, + ) + + specified_spd = 0.0 + if _is_float_tgt_leg and tgt_leg.float_spread is not NoInput.blank: + specified_spd = tgt_leg.float_spread + elif not _is_float_tgt_leg: + specified_spd = tgt_leg.fixed_rate * 100 + + _ += specified_spd + + if self._is_mtm: + self.leg2._do_not_repeat_set_periods = False # reset the mtm calc + + return _ if _is_float_tgt_leg else _ * 0.01 + + def spread(self, *args, **kwargs): + """ + Alias for :meth:`~rateslib.instruments.BaseXCS.rate` + """ + return self.rate(*args, **kwargs) + + def cashflows( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + ): + curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + fx, + base, + self.leg1.currency, + ) + + if self._is_unpriced: + self._set_pricing_mid(curves, solver, fx_) + + self._set_fx_fixings(fx_) + if self._is_mtm: + self.leg2._do_not_repeat_set_periods = True + + ret = super().cashflows(curves, solver, fx_, base_) + if self._is_mtm: + self.leg2._do_not_repeat_set_periods = False # reset the mtm calc + return ret + + +# Licence: Creative Commons - Attribution-NonCommercial-NoDerivatives 4.0 International +# Commercial use of this code, and/or copying and redistribution is prohibited. +# This code cannot be installed or executed on a corporate computer without a paid licence extension +# Contact info at rateslib.com if this code is observed outside its intended sphere of use. + + +class FXSwap(XCS): + """ + Create an FX swap simulated via a *Fixed-Fixed* :class:`XCS`. + + Parameters + ---------- + args : dict + Required positional args to :class:`XCS`. + pair : str, optional + The FX pair, e.g. "eurusd" as 3-digit ISO codes. If not given, fallsback to the base + implementation of *XCS* which defines separate inputs as ``currency`` and ``leg2_currency``. + If overspecified, ``pair`` will dominate. + fx_fixings : float, FXForwards or None + The initial FX fixing where leg 1 is considered the domestic currency. For + example for an ESTR/SOFR XCS in 100mm EUR notional a value of 1.10 for `fx0` + implies the notional on leg 2 is 110m USD. If `None` determines this + dynamically. + points : float, optional + The pricing parameter for the FX Swap, which will determine the implicit + fixed rate on leg2. + split_notional : float, optional + The accrued notional at termination of the domestic leg accounting for interest + payable at domestic interest rates. + kwargs : dict + Required keyword arguments to :class:`XCS`. + + Notes + ----- + .. warning:: + + ``leg2_notional`` is determined by the ``fx_fixings`` either initialised or at price + time and the value of ``notional``. The argument value of ``leg2_notional`` does + not impact calculations. + + *FXSwaps* are technically complicated instruments. To define a fully **priced** *Instrument* + they require at least two pricing parameters; ``fx_fixings`` and ``points``. If a + ``split_notional`` is also given at initialisation it will be assumed to be a split notional + *FXSwap*. If not, then it will not be assumed to be. + + If ``fx_fixings`` is given then the market pricing parameter ``points`` can be calculated. + This is an unusual partially *priced* parametrisation, however, and a warning will be emitted. + As before, if ``split_notional`` is given, or not, at initialisation the *FXSwap* will be + assumed to be split notional or not. + + If the *FXSwap* is not initialised with any parameters this defines an **unpriced** + *Instrument* and it will be assumed to be split notional, inline with interbank + market standards. The mid-market rate of an unpriced FXSwap is the same regardless of whether + it is split notional or not, albeit split notional FXSwaps result in smaller FX rate + sensitivity. + + Other combinations of arguments, just providing ``points`` or ``split_notional`` or both of + those will raise an error. An *FXSwap* cannot be parametrised by these in isolation. This is + summarised in the below table. + + .. list-table:: Resultant initialisation dependent upon given pricing parameters. + :widths: 10 10 10 70 + :header-rows: 1 + + * - fx_fixings + - points + - split_notional + - Result + * - X + - X + - X + - A fully *priced* instrument defined with split notionals. + * - X + - X + - + - A fully *priced* instruments without split notionals. + * - + - + - + - An *unpriced* instrument with assumed split notionals. + * - X + - + - X + - A partially priced instrument with split notionals. Warns about unconventionality. + * - X + - + - + - A partially priced instrument without split notionals. Warns about unconventionality. + * - + - X + - X + - Raises ValueError. Not allowable partially priced instrument. + * - + - X + - + - Raises ValueError. Not allowable partially priced instrument. + * - + - + - X + - Raises ValueError. Not allowable partially priced instrument. + + Examples + -------- + To value the *FXSwap* we create *Curves* and :class:`~rateslib.fx.FXForwards` + objects. + + .. ipython:: python + + usd = Curve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.95}, id="usd") + eur = Curve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.97}, id="eur") + eurusd = Curve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.971}, id="eurusd") + fxr = FXRates({"eurusd": 1.10}, settlement=dt(2022, 1, 3)) + fxf = FXForwards( + fx_rates=fxr, + fx_curves={"usdusd": usd, "eureur": eur, "eurusd": eurusd}, + ) + + Then we define the *FXSwap*. This in an unpriced instrument. + + .. ipython:: python + + fxs = FXSwap( + effective=dt(2022, 1, 18), + termination=dt(2022, 4, 19), + pair="usdeur", + calendar="nyc", + notional=1000000, + curves=["usd", "usd", "eur", "eurusd"], + ) + + Now demonstrate the :meth:`~rateslib.instruments.FXSwap.npv` and + :meth:`~rateslib.instruments.FXSwap.rate` methods: + + .. ipython:: python + + fxs.npv(curves=[None, usd, None, eurusd], fx=fxf) + fxs.rate(curves=[None, usd, None, eurusd], fx=fxf) + + In the case of *FXSwaps*, whose mid-market price is the difference between two + forward FX rates we can also derive this quantity using the independent + :meth:`FXForwards.swap` method. + + .. ipython:: python + + fxf.swap("usdeur", [dt(2022, 1, 18), dt(2022, 4, 19)]) + + The following is an example of a fully priced *FXSwap* with split notionals. + + .. ipython:: python + + fxs = FXSwap( + effective=dt(2022, 1, 18), + termination=dt(2022, 4, 19), + pair="usdeur", + calendar="nyc", + notional=1000000, + curves=["usd", "usd", "eur", "eurusd"], + fx_fixings=0.90, + split_notional=1001500, + points=-49.0 + ) + fxs.npv(curves=[None, usd, None, eurusd], fx=fxf) + fxs.cashflows(curves=[None, usd, None, eurusd], fx=fxf) + fxs.cashflows_table(curves=[None, usd, None, eurusd], fx=fxf) + + """ + + _unpriced = True + + def _parse_split_flag(self, fx_fixings, points, split_notional): + """ + Determine the rules for a priced, unpriced or partially priced derivative and whether + it is inferred as split notional or not. + """ + is_none = [_ is NoInput.blank for _ in [fx_fixings, points, split_notional]] + if all(is_none) or not any(is_none): + self._is_split = True + elif split_notional is NoInput.blank and not any( + _ is NoInput.blank for _ in [fx_fixings, points] + ): + self._is_split = False + elif fx_fixings is not NoInput.blank: + warnings.warn( + "Initialising FXSwap with `fx_fixings` but without `points` is unconventional.\n" + "Pricing can still be performed to determine `points`.", + UserWarning, + ) + if split_notional is not NoInput.blank: + self._is_split = True + else: + self._is_split = False + else: + if points is not NoInput.blank: + raise ValueError("Cannot initialise FXSwap with `points` but without `fx_fixings`.") + else: + raise ValueError( + "Cannot initialise FXSwap with `split_notional` but without `fx_fixings`", + ) + + def _set_split_notional(self, curve: Curve | NoInput = NoInput(0), at_init: bool = False): + """ + Will set the fixed rate, if not zero, for leg1, given provided split not or forecast splnot. + + self._split_notional is used as a temporary storage when mid market price is determined. + """ + if not self._is_split: + self._split_notional = self.kwargs["notional"] + # fixed rate at zero remains + + # a split notional is given by a user and then this is set and never updated. + elif self.kwargs["split_notional"] is not NoInput.blank: + if at_init: # this will be run for one time only at initialisation + self._split_notional = self.kwargs["split_notional"] + self._set_leg1_fixed_rate() + else: + return None + + # else new pricing parameters will affect and unpriced split notional + else: + if at_init: + self._split_notional = None + else: + dt1, dt2 = self.leg1.periods[0].payment, self.leg1.periods[2].payment + self._split_notional = self.kwargs["notional"] * curve[dt1] / curve[dt2] + self._set_leg1_fixed_rate() + + def _set_leg1_fixed_rate(self): + fixed_rate = (self.leg1.notional - self._split_notional) / ( + -self.leg1.notional * self.leg1.periods[1].dcf + ) + self.leg1.fixed_rate = fixed_rate * 100 + + def __init__( + self, + *args, + pair: str | NoInput = NoInput(0), + fx_fixings: float | FXRates | FXForwards | NoInput = NoInput(0), + points: float | NoInput = NoInput(0), + split_notional: float | NoInput = NoInput(0), + **kwargs, + ): + self._parse_split_flag(fx_fixings, points, split_notional) + currencies = {} + if isinstance(pair, str): + # TODO for version 2.0 should look to deprecate 'currency' and 'leg2_currency' as + # allowable inputs. + currencies = {"currency": pair.lower()[0:3], "leg2_currency": pair.lower()[3:6]} + + kwargs_overrides = dict( # specific args for FXSwap passed to the Base XCS + fixed=True, + leg2_fixed=True, + leg2_mtm=False, + fixed_rate=0.0, + frequency="Z", + leg2_frequency="Z", + leg2_fixed_rate=NoInput(0), + fx_fixings=fx_fixings, + ) + super().__init__(*args, **{**kwargs, **kwargs_overrides, **currencies}) + + self.kwargs["split_notional"] = split_notional + self._set_split_notional(curve=None, at_init=True) + # self._initialise_fx_fixings(fx_fixings) + self.points = points + + @property + def points(self): + return self._points + + @points.setter + def points(self, value): + self._unpriced = False + self._points = value + self._leg2_fixed_rate = NoInput(0) + + # setting points requires leg1.notional leg1.split_notional, fx_fixing and points value + + if value is not NoInput.blank: + # leg2 should have been properly set as part of fx_fixings and set_leg2_notional + fx_fixing = self.leg2.notional / -self.leg1.notional + + _ = self._split_notional * (fx_fixing + value / 10000) + self.leg2.notional + fixed_rate = _ / (self.leg2.periods[1].dcf * -self.leg2.notional) + + self.leg2_fixed_rate = fixed_rate * 100 + + # Licence: Creative Commons - Attribution-NonCommercial-NoDerivatives 4.0 International + + # Commercial use of this code, and/or copying and redistribution is prohibited. + # Contact rateslib at gmail.com if this code is observed outside its intended sphere. + + def _set_pricing_mid( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: FXForwards | NoInput = NoInput(0), + ): + # This function ASSUMES that the instrument is unpriced, i.e. all of + # split_notional, fx_fixing and points have been initialised as None. + + # first we set the split notional which is defined by interest rates on leg1. + points = self.rate(curves, solver, fx) + self.points = float(points) + self._unpriced = True # setting pricing mid does not define a priced instrument + + def rate( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: FXForwards | NoInput = NoInput(0), + fixed_rate: bool = False, + ): + """ + Return the mid-market pricing parameter of the FXSwapS. + + Parameters + ---------- + curves : list of Curves + A list defines the following curves in the order: + + - Forecasting :class:`~rateslib.curves.Curve` for leg1 (if floating). + - Discounting :class:`~rateslib.curves.Curve` for leg1. + - Forecasting :class:`~rateslib.curves.Curve` for leg2 (if floating). + - Discounting :class:`~rateslib.curves.Curve` for leg2. + solver : Solver, optional + The numerical :class:`~rateslib.solver.Solver` that + constructs :class:`~rateslib.curves.Curve` from calibrating instruments. + fx : FXForwards, optional + The FX forwards object that is used to determine the initial FX fixing for + determining ``leg2_notional``, if not specified at initialisation, and for + determining mark-to-market exchanges on mtm XCSs. + fixed_rate : bool + Whether to return the fixed rate for the leg or the FX swap points price. + + Returns + ------- + float, Dual or Dual2 + """ + curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + fx, + NoInput(0), + self.leg1.currency, + ) + # set the split notional from the curve if not available + self._set_split_notional(curve=curves[1]) + # then we will set the fx_fixing and leg2 initial notional. + + # self._set_fx_fixings(fx) # this will be done by super().rate() + leg2_fixed_rate = super().rate(curves, solver, fx_, leg=2) + + if fixed_rate: + return leg2_fixed_rate + else: + points = -self.leg2.notional * ( + (1 + leg2_fixed_rate * self.leg2.periods[1].dcf / 100) / self._split_notional + - 1 / self.kwargs["notional"] + ) + return points * 10000 + + def cashflows( + self, + curves: Curve | str | list | NoInput = NoInput(0), + solver: Solver | NoInput = NoInput(0), + fx: FXForwards | NoInput = NoInput(0), + base: str | NoInput = NoInput(0), + ): + if self._is_unpriced: + self._set_pricing_mid(curves, solver, fx) + ret = super().cashflows(curves, solver, fx, base) + return ret