Skip to content

Commit a8b5709

Browse files
authored
ENH: add Academic curves (#232) (#1157)
1 parent f711034 commit a8b5709

File tree

10 files changed

+612
-14
lines changed

10 files changed

+612
-14
lines changed

python/rateslib/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ def __exit__(self, *args) -> None: # type: ignore[no-untyped-def]
6060
index_left,
6161
index_value,
6262
)
63+
from rateslib.curves.academic import (
64+
NelsonSiegelCurve,
65+
NelsonSiegelSvenssonCurve,
66+
)
6367
from rateslib.data.fixings import (
6468
FloatRateIndex,
6569
FloatRateSeries,
@@ -213,6 +217,9 @@ def __exit__(self, *args) -> None: # type: ignore[no-untyped-def]
213217
"ProxyCurve",
214218
"index_left",
215219
"index_value",
220+
# academic curves
221+
"NelsonSiegelCurve",
222+
"NelsonSiegelSvenssonCurve",
216223
# fixings.py
217224
"FXFixing",
218225
"IBORFixing",
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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 rateslib.curves.academic.ns import NelsonSiegelCurve
10+
from rateslib.curves.academic.nss import NelsonSiegelSvenssonCurve
11+
12+
__all__ = ["NelsonSiegelCurve", "NelsonSiegelSvenssonCurve"]
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
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

Comments
 (0)