diff --git a/python/rateslib/default.py b/python/rateslib/default.py index f08c9ef8..e5c74665 100644 --- a/python/rateslib/default.py +++ b/python/rateslib/default.py @@ -159,6 +159,7 @@ def __init__(self) -> None: "Bill": 0, "FRA": 0, "CDS": 0, + "NDF": 2, } self.fixing_method = "rfr_payment_delay" self.fixing_method_param = { diff --git a/python/rateslib/instruments/rates/multi_currency.py b/python/rateslib/instruments/rates/multi_currency.py index 37fae54b..7cb3cc98 100644 --- a/python/rateslib/instruments/rates/multi_currency.py +++ b/python/rateslib/instruments/rates/multi_currency.py @@ -270,7 +270,40 @@ def analytic_delta(self, *args: Any, **kwargs: Any) -> DualTypes: raise NotImplementedError("`analytic_delta` for FXExchange not defined.") -class NDF(Sensitivities, BaseMixin): +class NDF(Sensitivities): + """ + Create a non-deliverable forward (NDF). + + Parameters + ---------- + pair: str + The FX pair against which settlement takes place (2 x 3-digit code). + settlement: datetime or str + The date on which settlement will occur. String tenors are allowed, e.g. "3M". + notional: float, Variable + The notional amount expressed in units of currency 1 of ``pair``. + fx_rate: float, Variable, optional + The agreed price on the NDF contact. May be omitted for unpriced contracts. + fx_fixing: float, Variable, optional + The rate against which settlement takes place. Will be forecast if not given or not known. + eval_date: datetime, optional + Required only if ``settlement`` is given as string tenor. + calendar: str or Calendar, optional + Determines settlement if given as string tenor and fixing date from settlement. + modifier: str, optional + Date modifier for determining string tenor. + currency: str, optional + The settlement currency of the contract. If not given is assumed to be currency 2 of the + ``pair``, e.g. USD in BRLUSD. Must be one of the currencies in ``pair``. + payment_lag: int, optional + Determines the FX rate fixing date from the settlement. Defaults to 2 (spot) if not given. + curves : Curve, str or list of such, optional + Only one curve is required for an *NDF*. This curve should discount cashflows in the + given ``currency`` at a known collateral rate. + spec : str, optional + An identifier to pre-populate many fields with conventional values. See + :ref:`here` for more info and available values. + """ def __init__( self, @@ -282,11 +315,14 @@ def __init__( eval_date: datetime_ = NoInput(0), calendar: CalInput = NoInput(0), modifier: str_ = NoInput(0), - spec: str_ = NoInput(0), + currency: str_ = NoInput(0), + payment_lag: int_ = NoInput(0), curves: Curves_ = NoInput(0), + spec: str_ = NoInput(0), ): self.kwargs: dict[str, Any] = dict( - pair=pair, + pair=pair.lower(), + currency=_drb(pair[3:], currency).lower(), notional=notional, fx_rate=fx_rate, settlement=settlement, @@ -302,6 +338,7 @@ def __init__( "modifier": defaults.modifier, "notional": defaults.notional, "calendar": get_calendar(self.kwargs["calendar"]), + "payment_lag": defaults.payment_lag_specific[type(self).__name__], } self.kwargs = _update_with_defaults(self.kwargs, default_kws) @@ -319,20 +356,35 @@ def __init__( mod_days=False ) + if not self.kwargs["currency"] in self.kwargs["pair"]: + raise ValueError("`currency` must be one of the currencies in `pair`.") + self.periods = [ NonDeliverableCashflow( notional=self.kwargs["notional"], - reference_currency=self.kwargs["reference_currency"], - settlement_currency=self.kwargs["settlement_currency"], + reference_currency=self.kwargs["pair"][0:3] if self.kwargs["pair"][0:3] != currency else self.kwargs["pair"][3:], + settlement_currency=self.kwargs["currency"], settlement=self.kwargs["settlement"], - fixing_date=, - fx_rate: DualTypes_ = NoInput(0), - fx_fixing: DualTypes_ = NoInput(0), + fixing_date=self.kwargs["calendar"].lag(self.kwargs["settlement"], -self.kwargs["payment_lag"], False), # a fixing date can be on a non-settlable date + fx_rate=self.kwargs["fx_rate"], + fx_fixing=self.kwargs["fx_fixing"], + reversed=self.kwargs["pair"][0:3] == self.kwargs["currency"] ) ] self.curves=curves self.spec=spec + def _set_pricing_mid( + self, + curves: Curves_ = NoInput(0), + solver: Solver_ = NoInput(0), + fx: FX_ = NoInput(0), + ) -> None: + if isinstance(self.fx_rate, NoInput): + mid_market_rate = self.rate(curves, solver, fx) + self.fx_rate = _dual_float(mid_market_rate) + self._fx_rate = NoInput(0) + def rate( self, curves: Curves_ = NoInput(0), @@ -348,22 +400,54 @@ def rate( base, self.leg1.currency, ) - curves_1 = _validate_curve_not_no_input(curves_[1]) - curves_3 = _validate_curve_not_no_input(curves_[3]) + return self.periods[0].rate(fx_) - if isinstance(fx_, FXRates | FXForwards): - imm_fx: DualTypes = fx_.rate(self.pair) - elif isinstance(fx_, NoInput): - raise ValueError( - "`fx` must be supplied to price FXExchange object.\n" - "Note: it can be attached to and then gotten from a Solver.", - ) - else: - imm_fx = fx_ + def cashflows( + self, + curves: Curves_ = NoInput(0), + solver: Solver_ = NoInput(0), + fx: FX_ = NoInput(0), + base: str_ = NoInput(0), + ) -> DataFrame: + """ + Return the cashflows of the *FXExchange* by aggregating legs. - _: DualTypes = forward_fx(self.settlement, curves_1, curves_3, imm_fx) + 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.periods[0].cashflows(curves_[0], curves_[1], fx_, base_), + ] + _: DataFrame = DataFrame.from_records(seq) + _.index = MultiIndex.from_tuples([("leg1", 0)]) return _ + def npv( + self, + curves: Curves_ = NoInput(0), + solver: Solver_ = NoInput(0), + fx: FX_ = NoInput(0), + base: str_ = NoInput(0), + local: bool = False, + ): + curves_, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver( + self.curves, + solver, + curves, + fx, + base, + self.kwargs["currency"], + ) + return self.periods[0].npv(NoInput(0), curves_[1], fx_, base_, local) + def delta(self, *args: Any, **kwargs: Any) -> DataFrame: """ Calculate the delta of the *Instrument*. @@ -381,7 +465,7 @@ def gamma(self, *args: Any, **kwargs: Any) -> DataFrame: return super().gamma(*args, **kwargs) def analytic_delta(self, *args: Any, **kwargs: Any) -> DualTypes: - raise NotImplementedError("`analytic_delta` for NDF not defined.") + return 0.0 class XCS(BaseDerivative):