diff --git a/pyproject.toml b/pyproject.toml index ea4c4c7d..00dfc31f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ features = ["pyo3/extension-module"] [project] name = "rateslib" -version = "2.5.0" +version = "2.5.dev0" description = "A fixed income library for trading interest rates" readme = "README.md" authors = [{ name = "J H M Darbyshire"}] diff --git a/python/rateslib/errors.py b/python/rateslib/errors.py index 6811a0ae..fb209830 100644 --- a/python/rateslib/errors.py +++ b/python/rateslib/errors.py @@ -49,9 +49,10 @@ ) VE_MISMATCHED_FX_PAIR_ND_PAIR = ( - "A non-deliverable FXOption must be configured with a non-deliverable currency `pair`" - "which includes the RHS currency of the `fx_pair` specific to the FXOption.\nGot " - "`pair': {0}' and RHS of `fx_pair`: '{1}'." + "Non-deliverable FXOptions into a third currency are not allowed.\n" + "Got nd-currency: '{0}' and option index pair: '{1}'.\n" + "FXOptions of this nature require quanto volatility adjustements that the basic models" + "do not include." ) # Fixings diff --git a/python/rateslib/instruments/iirs.py b/python/rateslib/instruments/iirs.py index 7a7e1b71..5b021a6a 100644 --- a/python/rateslib/instruments/iirs.py +++ b/python/rateslib/instruments/iirs.py @@ -218,6 +218,23 @@ class IIRS(_BaseInstrument): The value of the rate fixing. If a scalar, is used directly. If a string identifier, links to the central ``fixings`` object and data loader. + .. note:: + + The following parameters define **indexation**. + + index_method : IndexMethod, str, :green:`optional (set by 'defaults')` + The interpolation method, or otherwise, to determine index values from reference dates. + index_lag: int, :green:`optional (set by 'defaults')` + The indexation lag, in months, applied to the determination of index values. + index_base: float, Dual, Dual2, Variable, :green:`optional` + The specific value applied as the base index value for all *Periods*. + If not given and ``index_fixings`` is a string fixings identifier that will be + used to determine the base index value. + index_fixings: float, Dual, Dual2, Variable, Series, str, 2-tuple or list, :green:`optional` + The index value for the reference date. + Best practice is to supply this value as string identifier relating to the global + ``fixings`` object. + .. note:: The following are **meta parameters**. diff --git a/python/rateslib/instruments/zcis.py b/python/rateslib/instruments/zcis.py index 33fe73fc..277e8c18 100644 --- a/python/rateslib/instruments/zcis.py +++ b/python/rateslib/instruments/zcis.py @@ -43,6 +43,49 @@ class ZCIS(_BaseInstrument): An *indexed zero coupon swap (ZCIS)* composing a :class:`~rateslib.legs.ZeroFixedLeg` and a :class:`~rateslib.legs.ZeroIndexLeg`. + .. rubric:: Examples + + .. ipython:: python + :suppress: + + from rateslib.instruments import ZCIS + from datetime import datetime as dt + from rateslib import fixings + from pandas import Series + + .. ipython:: python + + fixings.add("CPI_UK", Series(index=[dt(1999, 10, 1), dt(1999, 11, 1)], data=[110.0, 112.0])) + zcis = ZCIS( + effective=dt(2000, 1, 10), + termination="2Y", + frequency="A", + fixed_rate=3.5, + currency="gbp", + leg2_index_fixings="CPI_UK", + leg2_index_method="daily", + ) + zcis.cashflows() + + .. ipython:: python + :suppress: + + fixings.pop("CPI_UK") + + .. rubric:: Pricing + + The methods of a *ZCIS* require a *disc curve* applicable to both legs and a *leg2 index curve*. + The following input formats are allowed: + + .. code-block:: python + + curves = [index_curve, disc_curve] # two curves + curves = [None, disc_curve, leg2_index_curve, disc_curve] # four curves + curves = { # dict form is explicit + "disc_curve": disc_curve, + "leg2_index_curve": leg2_index_curve, + } + .. role:: red .. role:: green @@ -137,9 +180,25 @@ class ZCIS(_BaseInstrument): The following are **rate parameters**. fixed_rate : float or None - The fixed rate applied to the :class:`~rateslib.legs.FixedLeg`. If `None` + The fixed rate applied to the :class:`~rateslib.legs.ZeroFixedLeg`. If `None` will be set to mid-market when curves are provided. + + .. note:: + The following parameters define **indexation**. + + leg2_index_method : IndexMethod, str, :green:`optional (set by 'defaults')` + The interpolation method, or otherwise, to determine index values from reference dates. + leg2_index_lag: int, :green:`optional (set by 'defaults')` + The indexation lag, in months, applied to the determination of index values. + leg2_index_base: float, Dual, Dual2, Variable, :green:`optional` + The specific value applied as the base index value for all *Periods*. + If not given and ``index_fixings`` is a string fixings identifier that will be + used to determine the base index value. + leg2_index_fixings: float, Dual, Dual2, Variable, Series, str, 2-tuple or list, :green:`optional` + The index value for the reference date. + Best practice is to supply this value as string identifier relating to the global + ``fixings`` object. .. note:: The following are **meta parameters**. @@ -162,9 +221,6 @@ class ZCIS(_BaseInstrument): ``leg2_fixings``, but a cookbook article is also produced for :ref:`working with fixings `. - Examples - -------- - Construct a curve to price the example. """ # noqa: E501 diff --git a/python/rateslib/legs/__init__.py b/python/rateslib/legs/__init__.py index e63fe484..392fd798 100644 --- a/python/rateslib/legs/__init__.py +++ b/python/rateslib/legs/__init__.py @@ -3,6 +3,7 @@ from rateslib.legs.custom import CustomLeg from rateslib.legs.fixed import FixedLeg, ZeroFixedLeg, ZeroIndexLeg from rateslib.legs.float import FloatLeg, ZeroFloatLeg +from rateslib.legs.protocols import _BaseLeg __all__ = [ "FixedLeg", @@ -14,4 +15,5 @@ "CreditProtectionLeg", "CustomLeg", "Amortization", + "_BaseLeg", ] diff --git a/python/rateslib/periods/credit.py b/python/rateslib/periods/credit.py index 0a475053..3268c89f 100644 --- a/python/rateslib/periods/credit.py +++ b/python/rateslib/periods/credit.py @@ -61,6 +61,25 @@ class CreditPremiumPeriod(_BasePeriod): For *analytic delta* purposes the :math:`\xi=-S`. + .. rubric:: Examples + + .. ipython:: python + :suppress: + + from rateslib.periods import CreditPremiumPeriod + from datetime import datetime as dt + + .. ipython:: python + + cp = CreditPremiumPeriod( + start=dt(2000, 3, 20), + end=dt(2000, 6, 20), + payment=dt(2000, 6, 20), + frequency="Q", + fixed_rate=1.00, + ) + cp.cashflows() + .. role:: red .. role:: green @@ -346,10 +365,32 @@ class CreditProtectionPeriod(_BasePeriod): The immediate expected valuation of the *Period* cashflow is defined as; - [TODO: NEEDS INPUT] + .. math:: + + \mathbb{E^Q}[V(m_T)C_T] = -N(1-RR) \int_{max(m_{a.s}, m_{today})}^{m_{a.e}} w_{loc:col}(m_s) Q(m_s) \lambda(s) ds + + where the integral is numerically determined. There is no *analytical delta* for this *Period* type and hence :math:`\xi` is not defined. + .. rubric:: Examples + + .. ipython:: python + :suppress: + + from rateslib.periods import CreditProtectionPeriod + from datetime import datetime as dt + + .. ipython:: python + + cp = CreditProtectionPeriod( + start=dt(2000, 3, 20), + end=dt(2000, 6, 20), + payment=dt(2000, 6, 20), + frequency="Q", + ) + cp.cashflows() + .. role:: red .. role:: green diff --git a/python/rateslib/periods/fx_volatility.py b/python/rateslib/periods/fx_volatility.py index 73685668..df2625f6 100644 --- a/python/rateslib/periods/fx_volatility.py +++ b/python/rateslib/periods/fx_volatility.py @@ -44,7 +44,6 @@ from rateslib.periods.parameters import ( _FXOptionParams, _IndexParams, - _init_or_none_IndexParams, _NonDeliverableParams, _SettlementParams, ) @@ -64,17 +63,14 @@ DualTypes, DualTypes_, FXForwards_, - IndexMethod, Number, Series, _BaseCurve, _BaseCurve_, _FXVolOption, _FXVolOption_, - bool_, datetime, datetime_, - int_, str_, ) @@ -86,95 +82,6 @@ class _BaseFXOptionPeriod(_BasePeriodStatic, _WithAnalyticFXOptionGreeks, metacl **See Also**: :class:`~rateslib.periods.FXCallPeriod`, :class:`~rateslib.periods.FXPutPeriod` - .. role:: red - - .. role:: green - - Parameters - ---------- - . - .. note:: - - The following define **fx option** and generalised **settlement** parameters. - - direction: OptionType, :red:`required` - Call or put. The value :math:`\phi` value in option formulae. - delivery: datetime, :red:`required` - The settlement date of the underlying FX rate of the option. Also used as the implied - payment date of the cashflow valuation date. - pair: str, :red:`required` - The currency pair of the :class:`~rateslib.data.fixings.FXFixing` against which the option - will settle. - expiry: datetime, :red:`required` - The expiry date of the option, when the option fixing is determined. - strike: float, Dual, Dual2, Variable, :green:`optional` - The strike price of the option. Can be set after initialisation. - notional: float, Dual, Dual2, Variable, :green:`optional (set by 'defaults')` - The notional of the option expressed in units of LHS currency of `pair`. - delta_type: FXDeltaMethod, str, :green:`optional (set by 'default')` - The definition of the delta for the option. - metric: FXDeltaMethod, str, :green:`optional` (set by 'default')` - The metric used by default in the - :meth:`~rateslib.periods.fx_volatility.FXOptionPeriod.rate` method. - option_fixings: float, Dual, Dual2, Variable, Series, str, :green:`optional` - The value of the option :class:`~rateslib.data.fixings.FXFixing`. If a scalar, is used - directly. If a string identifier, links to the central ``fixings`` object and data loader. - ex_dividend: datetime, :green:`optional (set as 'delivery')` - The ex-dividend date of the settled cashflow. - - .. note:: - - The following parameters define **non-deliverability**. A non-deliverable FX option is - one whose cash settlement value in RHS currency of ``pair`` is converted to a third - deliverable currency via a rate defined by ``fx_fixings``. The ``nd_pair`` must contain - RHS currency of `pair` in order to correctly define this conversion. - - If the *Period* is directly deliverable do not set these parameters. - - nd_pair: str, :green:`optional` - The currency pair of the :class:`~rateslib.data.fixings.FXFixing` that determines - non-deliverable settlement. The *reference currency* is implied from ``pair``. - Must include ``currency``. - fx_fixings: float, Dual, Dual2, Variable, Series, str, :green:`optional` - The value of the non-deliverable :class:`~rateslib.data.fixings.FXFixing`. If a scalar is - used directly. If a string identifier will link to the central ``fixings`` object and - data loader. - - .. note:: - - The following parameters define **indexation**. The *Period* will be considered - indexed if any of ``index_method``, ``index_lag``, ``index_base``, ``index_fixings`` - are given. FX Options are rarely, if ever, indexed so frequently these parameters - should be ignored. - - index_method : IndexMethod, str, :green:`optional (set by 'defaults')` - The interpolation method, or otherwise, to determine index values from reference dates. - index_lag: int, :green:`optional (set by 'defaults')` - The indexation lag, in months, applied to the determination of index values. - index_base: float, Dual, Dual2, Variable, :green:`optional` - The specific value set of the base index value. - If not given and ``index_fixings`` is a str fixings identifier that will be - used to determine the base index value. - index_fixings: float, Dual, Dual2, Variable, Series, str, :green:`optional` - The index value for the reference date. - If a scalar value this is used directly. If a string identifier will link to the - central ``fixings`` object and data loader. - index_base_date: datetime, :green:`optional` - The reference date for determining the base index value. Not required if ``_index_base`` - value is given directly. - index_reference_date: datetime, :green:`optional (set as 'end')` - The reference date for determining the index value. Not required if ``_index_fixings`` - is given as a scalar value. - index_only: bool, :green:`optional (set as False)` - A flag which determines non-payment of notional on supported *Periods*. - - - Notes - ------ - - Pricing model uses Black 76 log-normal volatility calculations with calendar day time - reference. - """ def analytic_greeks( @@ -246,27 +153,28 @@ def __init__( option_fixings: DualTypes | Series[DualTypes] | str_ = NoInput(0), # type: ignore[type-var] # currency args: ex_dividend: datetime_ = NoInput(0), - # non-deliverable args: - nd_pair: str_ = NoInput(0), - fx_fixings: DualTypes | Series[DualTypes] | str_ = NoInput(0), # type: ignore[type-var] - # index-args: - index_base: DualTypes_ = NoInput(0), - index_lag: int_ = NoInput(0), - index_method: IndexMethod | str_ = NoInput(0), - index_fixings: DualTypes | Series[DualTypes] | str_ = NoInput(0), # type: ignore[type-var] - index_only: bool_ = NoInput(0), - index_base_date: datetime_ = NoInput(0), - index_reference_date: datetime_ = NoInput(0), + # # non-deliverable args: + # nd_pair: str_ = NoInput(0), + # fx_fixings: DualTypes | Series[DualTypes] | str_ = NoInput(0), # type: ignore[type-var] + # # index-args: + # index_base: DualTypes_ = NoInput(0), + # index_lag: int_ = NoInput(0), + # index_method: IndexMethod | str_ = NoInput(0), + # index_fixings: DualTypes | Series[DualTypes] | str_ = NoInput(0), #type: ignore[type-var] + # index_only: bool_ = NoInput(0), + # index_base_date: datetime_ = NoInput(0), + # index_reference_date: datetime_ = NoInput(0), ) -> None: - self._index_params = _init_or_none_IndexParams( - _index_base=index_base, - _index_lag=index_lag, - _index_method=index_method, - _index_fixings=index_fixings, - _index_only=index_only, - _index_base_date=index_base_date, - _index_reference_date=_drb(delivery, index_reference_date), - ) + # self._index_params = _init_or_none_IndexParams( + # _index_base=index_base, + # _index_lag=index_lag, + # _index_method=index_method, + # _index_fixings=index_fixings, + # _index_only=index_only, + # _index_base_date=index_base_date, + # _index_reference_date=_drb(delivery, index_reference_date), + # ) + self._index_params = None self._fx_option_params = _FXOptionParams( _direction=direction, _expiry=expiry, @@ -280,6 +188,7 @@ def __init__( self._rate_params = None self._period_params = None + nd_pair = NoInput(0) if isinstance(nd_pair, NoInput): # then option is directly deliverable self._non_deliverable_params: _NonDeliverableParams | None = None @@ -291,27 +200,32 @@ def __init__( _ex_dividend=ex_dividend, ) else: - fx_ccy1, fx_ccy2 = self.fx_option_params.pair[:3], self.fx_option_params.pair[3:] - nd_ccy1, nd_ccy2 = nd_pair.lower()[:3], nd_pair.lower()[3:] - if fx_ccy2 == nd_ccy1: - currency = nd_ccy2 - elif fx_ccy2 != nd_ccy2: - currency = nd_ccy1 - else: - raise ValueError(err.VE_MISMATCHED_FX_PAIR_ND_PAIR.format(nd_pair.lower(), fx_ccy2)) - self._non_deliverable_params = _NonDeliverableParams( - _currency=currency, - _pair=nd_pair, - _delivery=delivery, - _fx_fixings=fx_fixings, - ) - self._settlement_params = _SettlementParams( - _notional=_drb(defaults.notional, notional), - _payment=delivery, - _currency=currency, - _notional_currency=fx_ccy1, - _ex_dividend=ex_dividend, - ) + pass + # fx_ccy1, fx_ccy2 = self.fx_option_params.pair[:3], self.fx_option_params.pair[3:] + # nd_ccy1, nd_ccy2 = nd_pair.lower()[:3], nd_pair.lower()[3:] + # + # if nd_ccy1 != fx_ccy1 and nd_ccy1 != fx_ccy2: + # raise ValueError( + # err.VE_MISMATCHED_FX_PAIR_ND_PAIR.format(nd_ccy1, self.fx_option_params.pair) + # ) + # elif nd_ccy2 != fx_ccy1 and nd_ccy2 != fx_ccy2: + # raise ValueError( + # err.VE_MISMATCHED_FX_PAIR_ND_PAIR.format(nd_ccy2, self.fx_option_params.pair) + # ) + # + # self._non_deliverable_params = _NonDeliverableParams( + # _currency=fx_ccy1, + # _pair=nd_pair, + # _delivery=delivery, + # _fx_fixings=fx_fixings, + # ) + # self._settlement_params = _SettlementParams( + # _notional=_drb(defaults.notional, notional), + # _payment=delivery, + # _currency=fx_ccy1, + # _notional_currency=fx_ccy1, + # _ex_dividend=ex_dividend, + # ) def __repr__(self) -> str: return f"" @@ -944,37 +858,199 @@ class FXCallPeriod(_BaseFXOptionPeriod): r""" A *Period* defined by a European FX call option. - For parameters see :class:`~rateslib.periods.FXOptionPeriod`, where `direction` is - set to 1.0. - The expected unindexed reference cashflow is given by, .. math:: \mathbb{E^Q}[\bar{C}_t] = \left \{ \begin{matrix} \max(f_d - K, 0) & \text{after expiry} \\ B76(f_d, K, t, \sigma) & \text{before expiry} \end{matrix} \right . - where :math:`B76(.)` is the Black-76 option pricing formula. + where :math:`B76(.)` is the Black-76 option pricing formula, using log-normal volatility + calculations with calendar day time reference. + + .. rubric:: Examples + + .. ipython:: python + :suppress: + + from rateslib.periods import FXCallPeriod + from datetime import datetime as dt + + .. ipython:: python + + fxo = FXCallPeriod( + delivery=dt(2000, 3, 1), + pair="eurusd", + expiry=dt(2000, 2, 28), + strike=1.10, + delta_type="forward", + ) + fxo.cashflows() + + .. role:: red + + .. role:: green + + Parameters + ---------- + . + .. note:: + + The following define **fx option** and generalised **settlement** parameters. + + delivery: datetime, :red:`required` + The settlement date of the underlying FX rate of the option. Also used as the implied + payment date of the cashflow valuation date. + pair: str, :red:`required` + The currency pair of the :class:`~rateslib.data.fixings.FXFixing` against which the option + will settle. + expiry: datetime, :red:`required` + The expiry date of the option, when the option fixing is determined. + strike: float, Dual, Dual2, Variable, :green:`optional` + The strike price of the option. Can be set after initialisation. + notional: float, Dual, Dual2, Variable, :green:`optional (set by 'defaults')` + The notional of the option expressed in units of LHS currency of `pair`. + delta_type: FXDeltaMethod, str, :green:`optional (set by 'default')` + The definition of the delta for the option. + metric: FXDeltaMethod, str, :green:`optional` (set by 'default')` + The metric used by default in the + :meth:`~rateslib.periods.fx_volatility.FXOptionPeriod.rate` method. + option_fixings: float, Dual, Dual2, Variable, Series, str, :green:`optional` + The value of the option :class:`~rateslib.data.fixings.FXFixing`. If a scalar, is used + directly. If a string identifier, links to the central ``fixings`` object and data loader. + ex_dividend: datetime, :green:`optional (set as 'delivery')` + The ex-dividend date of the settled cashflow. + + .. note:: + + This *Period* type has not implemented **indexation** or **non-deliverability**. + """ # noqa: E501 - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **{**kwargs, "direction": OptionType.Call}) + def __init__( + self, + *, + # option params: + delivery: datetime, # otherwise termed the 'payment' of the period + pair: str, + expiry: datetime, + strike: DualTypes_ = NoInput(0), + notional: DualTypes_ = NoInput(0), + delta_type: FXDeltaMethod | str_ = NoInput(0), + metric: FXOptionMetric | str_ = NoInput(0), + option_fixings: DualTypes | Series[DualTypes] | str_ = NoInput(0), # type: ignore[type-var] + # currency args: + ex_dividend: datetime_ = NoInput(0), + ) -> None: + super().__init__( + direction=OptionType.Call, + delivery=delivery, + pair=pair, + expiry=expiry, + strike=strike, + notional=notional, + delta_type=delta_type, + metric=metric, + option_fixings=option_fixings, + ex_dividend=ex_dividend, + ) class FXPutPeriod(_BaseFXOptionPeriod): r""" A *Period* defined by a European FX put option. - For parameters see :class:`~rateslib.periods.FXOptionPeriod`, where `direction` is - set to -1.0. - The expected unindexed reference cashflow is given by, .. math:: \mathbb{E^Q}[\bar{C}_t] = \left \{ \begin{matrix} \max(K - f_d, 0) & \text{after expiry} \\ B76(f_d, K, t, \sigma) & \text{before expiry} \end{matrix} \right . - where :math:`B76(.)` is the Black-76 option pricing formula. + where :math:`B76(.)` is the Black-76 option pricing formula, using log-normal volatility + calculations with calendar day time reference. + + .. rubric:: Examples + + .. ipython:: python + :suppress: + + from rateslib.periods import FXPutPeriod + from datetime import datetime as dt + + .. ipython:: python + + fxo = FXPutPeriod( + delivery=dt(2000, 3, 1), + pair="eurusd", + expiry=dt(2000, 2, 28), + strike=1.10, + delta_type="forward", + ) + fxo.cashflows() + + .. role:: red + + .. role:: green + + Parameters + ---------- + . + .. note:: + + The following define **fx option** and generalised **settlement** parameters. + + delivery: datetime, :red:`required` + The settlement date of the underlying FX rate of the option. Also used as the implied + payment date of the cashflow valuation date. + pair: str, :red:`required` + The currency pair of the :class:`~rateslib.data.fixings.FXFixing` against which the option + will settle. + expiry: datetime, :red:`required` + The expiry date of the option, when the option fixing is determined. + strike: float, Dual, Dual2, Variable, :green:`optional` + The strike price of the option. Can be set after initialisation. + notional: float, Dual, Dual2, Variable, :green:`optional (set by 'defaults')` + The notional of the option expressed in units of LHS currency of `pair`. + delta_type: FXDeltaMethod, str, :green:`optional (set by 'default')` + The definition of the delta for the option. + metric: FXDeltaMethod, str, :green:`optional` (set by 'default')` + The metric used by default in the + :meth:`~rateslib.periods.fx_volatility.FXOptionPeriod.rate` method. + option_fixings: float, Dual, Dual2, Variable, Series, str, :green:`optional` + The value of the option :class:`~rateslib.data.fixings.FXFixing`. If a scalar, is used + directly. If a string identifier, links to the central ``fixings`` object and data loader. + ex_dividend: datetime, :green:`optional (set as 'delivery')` + The ex-dividend date of the settled cashflow. + + .. note:: + + This *Period* type has not implemented **indexation** or **non-deliverability**. + """ # noqa: E501 - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **{**kwargs, "direction": OptionType.Put}) + def __init__( + self, + *, + # option params: + delivery: datetime, # otherwise termed the 'payment' of the period + pair: str, + expiry: datetime, + strike: DualTypes_ = NoInput(0), + notional: DualTypes_ = NoInput(0), + delta_type: FXDeltaMethod | str_ = NoInput(0), + metric: FXOptionMetric | str_ = NoInput(0), + option_fixings: DualTypes | Series[DualTypes] | str_ = NoInput(0), # type: ignore[type-var] + # currency args: + ex_dividend: datetime_ = NoInput(0), + ) -> None: + super().__init__( + direction=OptionType.Put, + delivery=delivery, + pair=pair, + expiry=expiry, + strike=strike, + notional=notional, + delta_type=delta_type, + metric=metric, + option_fixings=option_fixings, + ex_dividend=ex_dividend, + ) diff --git a/python/rateslib/periods/parameters/fx_volatility.py b/python/rateslib/periods/parameters/fx_volatility.py index e8eaebc1..55296da2 100644 --- a/python/rateslib/periods/parameters/fx_volatility.py +++ b/python/rateslib/periods/parameters/fx_volatility.py @@ -19,6 +19,9 @@ class _FXOptionParams: + """ + Parameters for *FX Option Period* cashflows. + """ _expiry: datetime _delivery: datetime _pair: str diff --git a/python/rateslib/periods/protocols/analytic_delta.py b/python/rateslib/periods/protocols/analytic_delta.py index 46717a8e..1c9ab2e8 100644 --- a/python/rateslib/periods/protocols/analytic_delta.py +++ b/python/rateslib/periods/protocols/analytic_delta.py @@ -39,14 +39,14 @@ class _WithAnalyticDelta(Protocol): .. autosummary:: - ~_WithAnalyticDelta.try_immediate_local_analytic_delta + ~_WithAnalyticDelta.try_immediate_local_analytic_delta .. rubric:: Provided methods .. autosummary:: - ~_WithAnalyticDelta.try_local_analytic_delta - ~_WithAnalyticDelta.analytic_delta + ~_WithAnalyticDelta.try_local_analytic_delta + ~_WithAnalyticDelta.analytic_delta Notes ----- @@ -103,7 +103,7 @@ def try_immediate_local_analytic_delta( Returns ------- - Result[DualTypes] + Result[float, Dual, Dual2, Variable] """ pass @@ -154,7 +154,7 @@ def try_local_analytic_delta( Returns ------- - Result[DualTypes] + Result[float, Dual, Dual2, Variable] """ # noqa: E501 local_immediate_result = self.try_immediate_local_analytic_delta( rate_curve=rate_curve, @@ -212,16 +212,25 @@ def analytic_delta( disc_curve: _BaseCurve, optional Used to discount cashflows. fx: FXForwards, optional - The :class:`~rateslib.fx.FXForward` object used for forecasting the + The :class:`~rateslib.fx.FXForwards` object used for forecasting the ``fx_fixing`` for deliverable cashflows, if necessary. Or, an - class:`~rateslib.fx.FXRates` object purely for immediate currency conversion. + :class:`~rateslib.fx.FXRates` object purely for immediate currency conversion. + fx_vol: FXDeltaVolSmile, FXSabrSmile, FXDeltaVolSurface, FXSabrSurface, optional + The FX volatility *Smile* or *Surface* object used for determining Black calendar + day implied volatility values. base: str, optional - The currency to return the result in. If not given is set to the *local settlement* - ``currency``. + The currency to convert the *local settlement* NPV to. + local: bool, optional + An override flag to return a dict of values indexed by string currency. + settlement: datetime, optional, (set as immediate date) + The assumed settlement date of the *PV* determination. Used only to evaluate + *ex-dividend* status. + forward: datetime, optional, (set as ``settlement``) + The future date to project the *PV* to using the ``disc_curve``. Returns ------- - float, Dual, Dual2, Variable + float, Dual, Dual2, Variable or dict """ local_delta = self.try_local_analytic_delta( rate_curve=rate_curve, @@ -252,18 +261,18 @@ class _WithAnalyticDeltaStatic( .. autosummary:: - ~_WithAnalyticDeltaStatic.try_unindexed_reference_cashflow_analytic_delta + ~_WithAnalyticDeltaStatic.try_unindexed_reference_cashflow_analytic_delta .. rubric:: Provided methods .. autosummary:: - ~_WithAnalyticDeltaStatic.try_reference_cashflow_analytic_delta - ~_WithAnalyticDeltaStatic.try_unindexed_cashflow_analytic_delta - ~_WithAnalyticDeltaStatic.try_cashflow_analytic_delta - ~_WithAnalyticDeltaStatic.try_immediate_local_analytic_delta - ~_WithAnalyticDeltaStatic.try_local_analytic_delta - ~_WithAnalyticDeltaStatic.analytic_delta + ~_WithAnalyticDeltaStatic.try_reference_cashflow_analytic_delta + ~_WithAnalyticDeltaStatic.try_unindexed_cashflow_analytic_delta + ~_WithAnalyticDeltaStatic.try_cashflow_analytic_delta + ~_WithAnalyticDeltaStatic.try_immediate_local_analytic_delta + ~_WithAnalyticDeltaStatic.try_local_analytic_delta + ~_WithAnalyticDeltaStatic.analytic_delta Notes ----- @@ -298,7 +307,7 @@ def try_unindexed_reference_cashflow_analytic_delta( Returns ------- - float, Dual, Dual2, Variable + Result[float, Dual, Dual2, Variable] """ raise NotImplementedError( f"type {type(self).__name__} has not implemented " @@ -320,6 +329,18 @@ def try_reference_cashflow_analytic_delta( I_r \frac{\partial \mathbb{E^Q}[\bar{C}_t]}{\partial \xi} + Parameters + ---------- + rate_curve: _BaseCurve or dict of such indexed by string tenor, optional + Used to forecast floating period rates, if necessary. + index_curve: _BaseCurve, optional + Used to forecast index values for indexation, if necessary. + disc_curve: _BaseCurve, optional + Used to discount cashflows. + + Returns + ------- + Result[float, Dual, Dual2, Variable] """ rrad = self.try_unindexed_reference_cashflow_analytic_delta( rate_curve=rate_curve, disc_curve=disc_curve @@ -340,6 +361,21 @@ def try_unindexed_cashflow_analytic_delta( .. math:: f(m_d) \frac{\partial \mathbb{E^Q}[\bar{C}_t]}{\partial \xi} + + Parameters + ---------- + rate_curve: _BaseCurve or dict of such indexed by string tenor, optional + Used to forecast floating period rates, if necessary. + disc_curve: _BaseCurve, optional + Used to discount cashflows. + fx: FXForwards, optional + The :class:`~rateslib.fx.FXForwards` object used for forecasting the + ``fx_fixing`` for deliverable cashflows, if necessary. Or, an + :class:`~rateslib.fx.FXRates` object purely for immediate currency conversion. + + Returns + ------- + Result[float, Dual, Dual2, Variable] """ rrad = self.try_unindexed_reference_cashflow_analytic_delta( rate_curve=rate_curve, disc_curve=disc_curve @@ -362,6 +398,27 @@ def try_cashflow_analytic_delta( .. math:: I_r f(m_d) \frac{\partial \mathbb{E^Q}[\bar{C}_t]}{\partial \xi} + + Parameters + ---------- + rate_curve: _BaseCurve or dict of such indexed by string tenor, optional + Used to forecast floating period rates, if necessary. + index_curve: _BaseCurve, optional + Used to forecast index values for indexation, if necessary. + disc_curve: _BaseCurve, optional + Used to discount cashflows. + fx: FXForwards, optional + The :class:`~rateslib.fx.FXForwards` object used for forecasting the + ``fx_fixing`` for deliverable cashflows, if necessary. Or, an + :class:`~rateslib.fx.FXRates` object purely for immediate currency conversion. + fx_vol: FXDeltaVolSmile, FXSabrSmile, FXDeltaVolSurface, FXSabrSurface, optional + The FX volatility *Smile* or *Surface* object used for determining Black calendar + day implied volatility values. + + Returns + ------- + Result[float, Dual, Dual2, Variable] + """ rad = self.try_reference_cashflow_analytic_delta( rate_curve=rate_curve, disc_curve=disc_curve, index_curve=index_curve diff --git a/python/rateslib/periods/protocols/cashflows.py b/python/rateslib/periods/protocols/cashflows.py index 3d4232e4..38f3411f 100644 --- a/python/rateslib/periods/protocols/cashflows.py +++ b/python/rateslib/periods/protocols/cashflows.py @@ -51,13 +51,13 @@ class _WithCashflows(_WithNPV, Protocol): .. autosummary:: - ~_WithNPVCashflows.try_cashflow + ~_WithCashflows.try_cashflow .. rubric:: Provided methods .. autosummary:: - ~_WithNPVCashflows.cashflows + ~_WithCashflows.cashflows """ @@ -71,7 +71,7 @@ def try_cashflow( fx_vol: _FXVolOption_ = NoInput(0), ) -> Result[DualTypes]: """ - Calculate the cashflow for the *Period* with settlement currency adjustment + Calculate the cashflow for the *Period* with any non-deliverable currency adjustment **and** indexation. Parameters @@ -80,9 +80,15 @@ def try_cashflow( Used to forecast floating period rates, if necessary. index_curve: _BaseCurve, optional Used to forecast index values for indexation, if necessary. + disc_curve: _BaseCurve, optional + Used to discount cashflows. fx: FXForwards, optional - The :class:`~rateslib.fx.FXForward` object used for forecasting the - ``fx_fixing`` for deliverable cashflows, if necessary. + The :class:`~rateslib.fx.FXForwards` object used for forecasting the + ``fx_fixing`` for deliverable cashflows, if necessary. Or, an + :class:`~rateslib.fx.FXRates` object purely for immediate currency conversion. + fx_vol: FXDeltaVolSmile, FXSabrSmile, FXDeltaVolSurface, FXSabrSurface, optional + The FX volatility *Smile* or *Surface* object used for determining Black calendar + day implied volatility values. Returns ------- @@ -181,7 +187,7 @@ class _WithCashflowsStatic(_WithNPVStatic, Protocol): .. autosummary:: - ~_WithNPVCashflows.cashflows + ~_WithCashflowsStatic.cashflows """ diff --git a/python/rateslib/periods/protocols/npv.py b/python/rateslib/periods/protocols/npv.py index 8ef8863c..bff59cab 100644 --- a/python/rateslib/periods/protocols/npv.py +++ b/python/rateslib/periods/protocols/npv.py @@ -430,7 +430,7 @@ def index_up(self, value: DualTypes, index_curve: _BaseCurve_) -> DualTypes: Returns ------- - Result[float, Dual, Dual2, Variable] + float, Dual, Dual2, Variable """ if self.index_params is None: # then no indexation of the cashflow will occur. @@ -447,6 +447,13 @@ def try_index_up(self, value: Result[DualTypes], index_curve: _BaseCurve_) -> Re Replicate :meth:`~rateslib.periods.protocols._WithIndexingStatic.index_up` with lazy exception handling. + Parameters + ---------- + value: Result[float, Dual, Dual2, Variable] + The possible value to apply indexation to. + index_curve: _BaseCurve, optional + The index curve used to forecast index values, if necessary. + Returns ------- Result[float, Dual, Dual2, Variable] @@ -489,18 +496,18 @@ def is_non_deliverable(self) -> bool: def convert_deliverable(self, value: DualTypes, fx: FXForwards_) -> DualTypes: """ Apply settlement currency conversion to a *Static Period* using its - ``non_deliverable_params``, with lazy error raising. + ``non_deliverable_params``. Parameters ---------- - value: Result[float, Dual, Dual2, Variable] + value: float, Dual, Dual2, Variable The possible value to apply settlement currency conversion to. fx: FXForwards, optional The object used to forecast forward FX rates, if necessary. Returns ------- - Result[float, Dual, Dual2, Variable] + float, Dual, Dual2, Variable """ if self.non_deliverable_params is None: # then cashflow is directly deliverable @@ -514,9 +521,16 @@ def try_convert_deliverable( self, value: Result[DualTypes], fx: FXForwards_ ) -> Result[DualTypes]: r""" - Replicate :meth:`~rateslib.periods.protocols._WithNonDeliverable.convert_deliverable` + Replicate :meth:`~rateslib.periods.protocols._WithNonDeliverableStatic.convert_deliverable` with lazy exception handling. + Parameters + ---------- + value: Result[float, Dual, Dual2, Variable] + The possible value to apply settlement currency conversion to. + fx: FXForwards, optional + The object used to forecast forward FX rates, if necessary. + Returns ------- Result[float, Dual, Dual2, Variable] diff --git a/python/tests/instruments/test_instruments_legacy.py b/python/tests/instruments/test_instruments_legacy.py index 907a3299..e259379d 100644 --- a/python/tests/instruments/test_instruments_legacy.py +++ b/python/tests/instruments/test_instruments_legacy.py @@ -6340,6 +6340,55 @@ def test_pricing_with_interpolated_sabr_surface(self, k, fxfo): assert np.all(gradient(result.vol, vars=["v_0_0", "v_1_0"]) > 49.2) assert np.all(gradient(result.vol, vars=["v_0_0", "v_1_0"]) < 50.6) + @pytest.mark.skip(reason="non-deliverability for FXOption instruments not yet implemented") + @pytest.mark.parametrize("ndpair", ["usdbrl", "brlusd"]) + def test_non_deliverable_fx_option_npv_vol_from_delta(self, ndpair): + # see the equivalent test for an FXOptionPeriod with static vol + fxf = FXForwards( + fx_rates=FXRates({"usdbrl": 5.0}, settlement=dt(2000, 1, 1)), + fx_curves={ + "usdusd": Curve({dt(2000, 1, 1): 1.0, dt(2000, 6, 1): 0.98}), + "brlusd": Curve({dt(2000, 1, 1): 1.0, dt(2000, 6, 1): 0.983}), + "brlbrl": Curve({dt(2000, 1, 1): 1.0, dt(2000, 6, 1): 0.984}), + }, + ) + fxv = FXDeltaVolSmile( + nodes={0.4: 10.0, 0.6: 11.0}, + eval_date=dt(2000, 1, 1), + expiry=dt(2000, 2, 28), + delta_type="forward", + ) + fxo = FXCall( + delivery_lag=dt(2000, 3, 1), + pair="USDBRL", + strike="50d", + delta_type="spot", + expiry=dt(2000, 2, 28), + ) + fxond = FXCall( + delivery_lag=dt(2000, 3, 1), + pair="USDBRL", + nd_pair=ndpair, + delta_type="spot", + strike="50d", + expiry=dt(2000, 2, 28), + ) + + npv = fxo.local_npv( + fx=fxf, + vol=fxv, + curves=[fxf.curve("usd", "usd"), fxf.curve("brl", "usd")], + ) + npv_nd = fxond.local_npv( + fx=fxf, + vol=fxv, + curves=[fxf.curve("usd", "usd"), fxf.curve("usd", "usd")], + ) + + # local NPV should be expressed in USD for ND type + result = npv / 5.0 - npv_nd + assert abs(result) < 1e-9 + class TestRiskReversal: @pytest.mark.parametrize( diff --git a/python/tests/periods/test_periods_legacy.py b/python/tests/periods/test_periods_legacy.py index 2b3bfaae..42e4989d 100644 --- a/python/tests/periods/test_periods_legacy.py +++ b/python/tests/periods/test_periods_legacy.py @@ -5645,61 +5645,121 @@ def test_try_rate_errs(self, fxfo): metric="Pips", ).is_err - def test_non_deliverable_fx_option(self, fxfo): + @pytest.mark.skip(reason="non-deliverability of FXOption period not implemented in v2.5") + def test_non_deliverable_fx_option_third_currency_raises(self, fxfo): # this is an NOKSEK FX option with notional in NOK, normal value in SEK but non-deliverable # requiring conversion to USD + with pytest.raises(ValueError, match=err.VE_MISMATCHED_FX_PAIR_ND_PAIR[:15]): + FXCallPeriod( + delivery=dt(2000, 3, 1), + pair="NOKSEK", + nd_pair="SEKUSD", + strike=1.0, + expiry=dt(2000, 2, 28), + ) + # assert fxo.settlement_params.notional_currency == "nok" + # assert fxo.settlement_params.currency == "usd" + # assert fxo.non_deliverable_params.reference_currency == "sek" + # + # fxo = FXCallPeriod( + # delivery=dt(2000, 3, 1), + # pair="NOKSEK", + # strike=1.0, + # expiry=dt(2000, 2, 28), + # ) + # assert fxo.settlement_params.notional_currency == "nok" + # assert fxo.settlement_params.currency == "sek" + # assert fxo.non_deliverable_params is None + + @pytest.mark.skip(reason="non-deliverability of FXOption period not implemented in v2.5") + @pytest.mark.parametrize("ndpair", ["usdbrl", "brlusd"]) + def test_non_deliverable_fx_option_npv_vol_given(self, ndpair): + # this is an USDBRL FX option period non-deliverable into USD. + fxf = FXForwards( + fx_rates=FXRates({"usdbrl": 5.0}, settlement=dt(2000, 1, 1)), + fx_curves={ + "usdusd": Curve({dt(2000, 1, 1): 1.0, dt(2000, 6, 1): 0.98}), + "brlusd": Curve({dt(2000, 1, 1): 1.0, dt(2000, 6, 1): 0.983}), + "brlbrl": Curve({dt(2000, 1, 1): 1.0, dt(2000, 6, 1): 0.984}), + }, + ) fxo = FXCallPeriod( delivery=dt(2000, 3, 1), - pair="NOKSEK", - nd_pair="SEKUSD", + pair="USDBRL", strike=1.0, expiry=dt(2000, 2, 28), ) - assert fxo.settlement_params.notional_currency == "nok" - assert fxo.settlement_params.currency == "usd" - assert fxo.non_deliverable_params.reference_currency == "sek" - - fxo = FXCallPeriod( + fxond = FXCallPeriod( delivery=dt(2000, 3, 1), - pair="NOKSEK", + pair="USDBRL", + nd_pair=ndpair, strike=1.0, expiry=dt(2000, 2, 28), ) - assert fxo.settlement_params.notional_currency == "nok" - assert fxo.settlement_params.currency == "sek" - assert fxo.non_deliverable_params is None - def test_non_deliverable_fx_option_npv(self, fxfo): - # this is an NOKSEK FX option with notional in NOK, normal value in SEK but non-deliverable - # requiring conversion to USD + npv = fxo.local_npv(fx=fxf, fx_vol=10.0, disc_curve=fxf.curve("brl", "usd")) + npv_nd = fxond.local_npv(fx=fxf, fx_vol=10.0, disc_curve=fxf.curve("usd", "usd")) + + # local NPV should be expressed in USD for ND type + result = npv / 5.0 - npv_nd + assert abs(result) < 1e-9 + + @pytest.mark.skip(reason="non-deliverability of FXOption period not implemented in v2.5") + @pytest.mark.parametrize(("ndpair", "fxfix"), [("usdbrl", 5.25), ("brlusd", 1 / 5.25)]) + def test_non_deliverable_fx_option_npv_vol_given_fx_fixing(self, ndpair, fxfix): + # this is an USDBRL FX option period non-deliverable into USD. fxf = FXForwards( - fx_rates=FXRates({"seknok": 0.95, "usdsek": 9.95}, settlement=dt(2000, 1, 1)), + fx_rates=FXRates({"usdbrl": 5.0}, settlement=dt(2000, 1, 1)), fx_curves={ "usdusd": Curve({dt(2000, 1, 1): 1.0, dt(2000, 6, 1): 0.98}), - "sekusd": Curve({dt(2000, 1, 1): 1.0, dt(2000, 6, 1): 0.981}), - "seksek": Curve({dt(2000, 1, 1): 1.0, dt(2000, 6, 1): 0.984}), - "noknok": Curve({dt(2000, 1, 1): 1.0, dt(2000, 6, 1): 0.986}), - "nokusd": Curve({dt(2000, 1, 1): 1.0, dt(2000, 6, 1): 0.989}), + "brlusd": Curve({dt(2000, 1, 1): 1.0, dt(2000, 6, 1): 0.983}), + "brlbrl": Curve({dt(2000, 1, 1): 1.0, dt(2000, 6, 1): 0.984}), }, ) - fxond = FXCallPeriod( + fxv = FXDeltaVolSmile( + nodes={0.4: 10.0, 0.6: 11.0}, + eval_date=dt(2000, 1, 1), + expiry=dt(2000, 2, 28), + delta_type="spot", + ) + fxo = FXCallPeriod( delivery=dt(2000, 3, 1), - pair="NOKSEK", - nd_pair="SEKUSD", + pair="USDBRL", strike=1.0, expiry=dt(2000, 2, 28), ) - fxo = FXCallPeriod( + fxond = FXCallPeriod( delivery=dt(2000, 3, 1), - pair="NOKSEK", + pair="USDBRL", + nd_pair=ndpair, + fx_fixings=fxfix, strike=1.0, expiry=dt(2000, 2, 28), ) - npv = fxo.try_local_npv(fx=fxf, fx_vol=10.0, disc_curve=fxf.curve("sek", "usd")) - npv_nd = fxond.try_local_npv(fx=fxf, fx_vol=10.0, disc_curve=fxf.curve("usd", "usd")) + + npv = fxo.local_npv( + fx=fxf, + fx_vol=fxv, + rate_curve=fxf.curve("usd", "usd"), + disc_curve=fxf.curve("brl", "usd"), + ) + npv_nd = fxond.local_npv( + fx=fxf, + fx_vol=fxv, + rate_curve=fxf.curve("usd", "usd"), + disc_curve=fxf.curve("usd", "usd"), + ) # local NPV should be expressed in USD for ND type - assert abs(npv.unwrap() / 9.95 - npv_nd.unwrap()) < 1e-10 + result = ( + npv_nd + * 5.25 + / fxf.curve("usd", "usd")[dt(2000, 3, 1)] + * fxf.curve("brl", "usd")[dt(2000, 3, 1)] + - npv + ) + # these should be different beucase of the fix: compare with test above + assert abs(result) < 1e-8 def test_cashflow_no_pricing_objects(self): # this is an NOKSEK FX option with notional in NOK, normal value in SEK but non-deliverable