Skip to content

Commit

Permalink
ENH: add non-deliverable forward
Browse files Browse the repository at this point in the history
  • Loading branch information
attack68 committed Jan 25, 2025
1 parent df6655e commit 51feed2
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 21 deletions.
1 change: 1 addition & 0 deletions python/rateslib/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
126 changes: 105 additions & 21 deletions python/rateslib/instruments/rates/multi_currency.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<defaults-doc>` for more info and available values.
"""

def __init__(
self,
Expand All @@ -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,
Expand All @@ -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)

Expand All @@ -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),
Expand All @@ -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<rateslib.instruments.BaseMixin.cashflows>`
"""
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*.
Expand All @@ -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):
Expand Down

0 comments on commit 51feed2

Please sign in to comment.