|
| 1 | +############################################################# |
| 2 | +# COPYRIGHT 2022 Siffrorna Technology Limited |
| 3 | +# This code may not be copied, modified, used or distributed |
| 4 | +# except with the express permission and licence to |
| 5 | +# do so, provided by the copyright holder. |
| 6 | +# See: https://rateslib.com/py/en/latest/i_licence.html |
| 7 | +############################################################# |
| 8 | + |
| 9 | +from __future__ import annotations |
| 10 | + |
| 11 | +from typing import TYPE_CHECKING |
| 12 | +from uuid import uuid4 |
| 13 | + |
| 14 | +import numpy as np |
| 15 | + |
| 16 | +from rateslib import defaults |
| 17 | +from rateslib.curves import _BaseCurve, _CurveMeta, _CurveNodes, _CurveType, _WithMutability |
| 18 | +from rateslib.dual import Dual, Dual2, dual_exp, set_order_convert |
| 19 | +from rateslib.dual.utils import _dual_float |
| 20 | +from rateslib.enums.generics import NoInput, _drb |
| 21 | +from rateslib.mutability import _clear_cache_post, _new_state_post |
| 22 | +from rateslib.scheduling import Convention, dcf, get_calendar |
| 23 | +from rateslib.scheduling.convention import _get_convention |
| 24 | + |
| 25 | +if TYPE_CHECKING: |
| 26 | + from rateslib.typing import ( # pragma: no cover |
| 27 | + Any, |
| 28 | + CalInput, |
| 29 | + DualTypes, |
| 30 | + Variable, |
| 31 | + datetime, |
| 32 | + float_, |
| 33 | + int_, |
| 34 | + str_, |
| 35 | + ) |
| 36 | + |
| 37 | + |
| 38 | +class NelsonSiegelCurve(_WithMutability, _BaseCurve): |
| 39 | + r""" |
| 40 | + A Nelson-Siegel curve defined by discount factors. |
| 41 | +
|
| 42 | + The continuously compounded rate to maturity, :math:`r(T)`, is given by the following |
| 43 | + equation of **four** parameters, :math:`[\beta_0, \beta_1, \beta_2, \lambda]` |
| 44 | +
|
| 45 | + .. math:: |
| 46 | +
|
| 47 | + r(T) = \begin{bmatrix} \beta_0 & \beta_1 & \beta_2 \end{bmatrix} \begin{bmatrix} 1 \\ \lambda (1- e^{-T/ \lambda}) / T \\ \lambda (1- e^{-T/ \lambda})/ T - e^{-T/ \lambda} \end{bmatrix} |
| 48 | +
|
| 49 | + The **discount factors** on that curve equaling: |
| 50 | +
|
| 51 | + .. math:: |
| 52 | +
|
| 53 | + v(T) = e^{-T r(T)} |
| 54 | +
|
| 55 | + *T* is determined as the day count fraction between the start of the curve and the maturity |
| 56 | + under the given the ``convention`` and ``calendar``. |
| 57 | +
|
| 58 | + .. role:: red |
| 59 | +
|
| 60 | + .. role:: green |
| 61 | +
|
| 62 | + Parameters |
| 63 | + ---------- |
| 64 | + dates: 2-tuple of datetime, :red:`required` |
| 65 | + The dates defining the eval date and final date of the *Curve*. |
| 66 | + parameters: 4-tuple of Dual, Dual2, Variable, float, :red:`required` |
| 67 | + The parameters associated with the *Curve*. In order these are |
| 68 | + :math:`[\beta_0, \beta_1, \beta_2, \lambda]`. |
| 69 | + id : str, :green:`optional (set randomly)` |
| 70 | + The unique identifier to distinguish between curves in a multicurve framework. |
| 71 | + convention : Convention, str, :green:`optional (set as ActActISDA)` |
| 72 | + The convention of the curve for determining rates. Please see |
| 73 | + :meth:`dcf()<rateslib.scheduling.dcf>` for all available options. |
| 74 | + modifier : str, :green:`optional (set by 'defaults')` |
| 75 | + The modification rule, in {"F", "MF", "P", "MP"}, for determining rates when input as |
| 76 | + a tenor, e.g. "3M". |
| 77 | + calendar : calendar, str, :green:`optional (set as 'all')` |
| 78 | + The holiday calendar object to use. If str, looks up named calendar from |
| 79 | + static data. Used for determining rates. |
| 80 | + ad : int in {0, 1, 2}, :green:`optional` |
| 81 | + Sets the automatic differentiation order. Defines whether to convert node |
| 82 | + values to float, :class:`~rateslib.dual.Dual` or |
| 83 | + :class:`~rateslib.dual.Dual2`. It is advised against |
| 84 | + using this setting directly. It is mainly used internally. |
| 85 | + index_base: float, :green:`optional` |
| 86 | + The initial index value at the initial node date of the curve. Used for |
| 87 | + forecasting future index values. |
| 88 | + index_lag : int, :green:`optional (set by 'defaults')` |
| 89 | + Number of months of by which the index lags the date. For example if the initial |
| 90 | + curve node date is 1st Sep 2021 based on the inflation index published |
| 91 | + 17th June 2023 then the lag is 3 months. Best practice is to use 0 months. |
| 92 | + collateral : str, :green:`optional (set as None)` |
| 93 | + A currency identifier to denote the collateral currency against which the discount factors |
| 94 | + for this *Curve* are measured. |
| 95 | + credit_discretization : int, :green:`optional (set by 'defaults')` |
| 96 | + A parameter for numerically solving the integral for credit protection legs and default |
| 97 | + events. Expressed in calendar days. Only used by *Curves* functioning as *hazard Curves*. |
| 98 | + credit_recovery_rate : Variable | float, :green:`optional (set by 'defaults')` |
| 99 | + A parameter used in pricing credit protection legs and default events. |
| 100 | +
|
| 101 | + """ # noqa: E501 |
| 102 | + |
| 103 | + # ABC properties |
| 104 | + |
| 105 | + _ini_solve = 0 |
| 106 | + _base_type = _CurveType.dfs |
| 107 | + _id = None # type: ignore[assignment] |
| 108 | + _meta = None # type: ignore[assignment] |
| 109 | + _nodes = None # type: ignore[assignment] |
| 110 | + _ad = None # type: ignore[assignment] |
| 111 | + _interpolator = None # type: ignore[assignment] |
| 112 | + _n = 4 |
| 113 | + |
| 114 | + @_new_state_post |
| 115 | + def __init__( |
| 116 | + self, |
| 117 | + dates: tuple[datetime, datetime], |
| 118 | + parameters: tuple[DualTypes, DualTypes, DualTypes, DualTypes], |
| 119 | + id: str_ = NoInput(0), # noqa: A002 |
| 120 | + *, |
| 121 | + convention: Convention | str | NoInput = NoInput(0), |
| 122 | + modifier: str | NoInput = NoInput(0), |
| 123 | + calendar: CalInput = NoInput(0), |
| 124 | + ad: int = 0, |
| 125 | + index_base: Variable | float_ = NoInput(0), |
| 126 | + index_lag: int | NoInput = NoInput(0), |
| 127 | + collateral: str_ = NoInput(0), |
| 128 | + credit_discretization: int_ = NoInput(0), |
| 129 | + credit_recovery_rate: Variable | float_ = NoInput(0), |
| 130 | + ): |
| 131 | + self._nodes = _CurveNodes({dates[0]: 0.0, dates[1]: 0.0}) |
| 132 | + self._params = parameters |
| 133 | + self._meta = _CurveMeta( |
| 134 | + _calendar=get_calendar(calendar), |
| 135 | + _convention=_get_convention(_drb(Convention.ActActISDA, convention)), |
| 136 | + _modifier=_drb(defaults.modifier, modifier).upper(), |
| 137 | + _index_base=index_base, |
| 138 | + _index_lag=_drb(defaults.index_lag_curve, index_lag), |
| 139 | + _collateral=_drb(None, collateral), |
| 140 | + _credit_discretization=_drb( |
| 141 | + defaults.cds_protection_discretization, credit_discretization |
| 142 | + ), |
| 143 | + _credit_recovery_rate=_drb(defaults.cds_recovery_rate, credit_recovery_rate), |
| 144 | + ) |
| 145 | + |
| 146 | + self._id = _drb(uuid4().hex[:5], id) # 1 in a million clash |
| 147 | + self._set_ad_order(order=ad) # will also clear and initialise the cache |
| 148 | + |
| 149 | + @property |
| 150 | + def params(self) -> tuple[DualTypes, DualTypes, DualTypes, DualTypes]: |
| 151 | + r""" |
| 152 | + The parameters associated with the *Curve*. |
| 153 | + In order these are :math:`[\beta_0, \beta_1, \beta_2, \lambda]`. |
| 154 | + """ |
| 155 | + return self._params |
| 156 | + |
| 157 | + def __getitem__(self, date: datetime) -> DualTypes: |
| 158 | + if defaults.curve_caching and date in self._cache: |
| 159 | + return self._cache[date] |
| 160 | + |
| 161 | + if date < self.nodes.initial: |
| 162 | + return 0.0 |
| 163 | + elif date == self.nodes.initial: |
| 164 | + return 1.0 |
| 165 | + b0, b1, b2, l0 = self._params |
| 166 | + T = dcf( |
| 167 | + self.nodes.initial, date, convention=self.meta.convention, calendar=self.meta.calendar |
| 168 | + ) |
| 169 | + a1 = l0 * (1 - dual_exp(-T / l0)) / T |
| 170 | + a2 = a1 - dual_exp(-T / l0) |
| 171 | + r = b0 + a1 * b1 + a2 * b2 |
| 172 | + |
| 173 | + return self._cached_value(date, dual_exp(-T * r)) |
| 174 | + |
| 175 | + # Solver mutability methods |
| 176 | + |
| 177 | + def _get_node_vector(self) -> np.ndarray[tuple[int, ...], np.dtype[Any]]: |
| 178 | + return np.array(self._params) |
| 179 | + |
| 180 | + def _get_node_vars(self) -> tuple[str, ...]: |
| 181 | + return tuple(f"{self._id}{i}" for i in range(self._ini_solve, self._n)) |
| 182 | + |
| 183 | + @_new_state_post |
| 184 | + @_clear_cache_post |
| 185 | + def _set_node_vector(self, vector: list[DualTypes], ad: int) -> None: |
| 186 | + if ad == 0: |
| 187 | + self._params = tuple(_dual_float(_) for _ in vector) # type: ignore[assignment] |
| 188 | + elif ad == 1: |
| 189 | + self._params = tuple( # type: ignore[assignment] |
| 190 | + Dual(_dual_float(_), [f"{self._id}{i}"], []) for i, _ in enumerate(vector) |
| 191 | + ) |
| 192 | + else: # ad == 2 |
| 193 | + self._params = tuple( # type: ignore[assignment] |
| 194 | + Dual2(_dual_float(_), [f"{self._id}{i}"], [], []) for i, _ in enumerate(vector) |
| 195 | + ) |
| 196 | + |
| 197 | + @_clear_cache_post |
| 198 | + def _set_ad_order(self, order: int) -> None: |
| 199 | + if self.ad == order: |
| 200 | + return None |
| 201 | + elif order not in [0, 1, 2]: |
| 202 | + raise ValueError("`order` can only be in {0, 1, 2} for auto diff calcs.") |
| 203 | + |
| 204 | + self._ad = order |
| 205 | + self._params = tuple( # type: ignore[assignment] |
| 206 | + set_order_convert(_, order, [f"{self._id}{i}"]) for i, _ in enumerate(self.params) |
| 207 | + ) |
0 commit comments