Skip to content

Commit 262d37a

Browse files
mikelyncattack68
andauthored
ENH: add financial fixings sensitivity via AD (#215) (#1138)
Co-authored-by: JHM Darbyshire <[email protected]> Co-authored-by: JHM Darbyshire (M1) <[email protected]> Co-authored-by: Mike Lync <[email protected]>
1 parent fd1f464 commit 262d37a

File tree

17 files changed

+1256
-74
lines changed

17 files changed

+1256
-74
lines changed

python/rateslib/__init__.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def __exit__(self, *args) -> None: # type: ignore[no-untyped-def]
6060
index_left,
6161
index_value,
6262
)
63+
from rateslib.data.fixings import FXFixing, IBORFixing, IBORStubFixing, IndexFixing, RFRFixing
6364
from rateslib.dual import ADOrder, Dual, Dual2, Variable, dual_exp, dual_log, dual_solve, gradient
6465
from rateslib.enums.generics import NoInput
6566
from rateslib.fx import FXForwards, FXRates
@@ -201,12 +202,12 @@ def __exit__(self, *args) -> None: # type: ignore[no-untyped-def]
201202
"ProxyCurve",
202203
"index_left",
203204
"index_value",
204-
# # analytic_fixings.py cannot load due to circular import
205-
# "FXFixing",
206-
# "IBORFixing",
207-
# "IBORStubFixing",
208-
# "IndexFixing",
209-
# "RFRFixing",
205+
# fixings.py
206+
"FXFixing",
207+
"IBORFixing",
208+
"IBORStubFixing",
209+
"IndexFixing",
210+
"RFRFixing",
210211
# fx_volatility.py
211212
"FXDeltaVolSmile",
212213
"FXDeltaVolSurface",

python/rateslib/data/fixings.py

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
Result,
4949
_BaseCurve_,
5050
datetime_,
51+
int_,
5152
str_,
5253
)
5354

@@ -85,17 +86,53 @@ def __init__(
8586
self._state = 0
8687
self._date = date
8788

88-
def reset(self) -> None:
89+
def reset(self, state: int_ = NoInput(0)) -> None:
8990
"""
9091
Sets the ``value`` attribute to :class:`rateslib.enums.generics.NoInput`, which allows it
9192
to be redetermined from a timeseries.
9293
94+
.. rubric:: Examples
95+
96+
.. ipython:: python
97+
:suppress:
98+
99+
from rateslib import fixings, dt, NoInput, FXFixing
100+
from pandas import Series
101+
102+
.. ipython:: python
103+
104+
fx_fixing1 = FXFixing(date=dt(2021, 1, 1), pair="eurusd", identifier="A")
105+
fx_fixing2 = FXFixing(date=dt(2021, 1, 1), pair="gbpusd", identifier="B")
106+
fixings.add("A_eurusd", Series(index=[dt(2021, 1, 1)], data=[1.1]), state=100)
107+
fixings.add("B_gbpusd", Series(index=[dt(2021, 1, 1)], data=[1.4]), state=200)
108+
109+
# data is populated from the available Series
110+
fx_fixing1.value
111+
fx_fixing2.value
112+
113+
# fixings are reset according to the data state
114+
fx_fixing1.reset(state=100)
115+
fx_fixing2.reset(state=100)
116+
117+
# only the private data for fixing1 is removed because of its link to the data state
118+
fx_fixing1._value
119+
fx_fixing2._value
120+
121+
.. role:: green
122+
123+
Parameters
124+
----------
125+
state: int, :green:`optional`
126+
If given only fixings whose state matches this value will be reset. If no state is
127+
given then the value will be reset.
128+
93129
Returns
94130
-------
95131
None
96132
"""
97-
self._value = NoInput(0)
98-
self._state = 0
133+
if isinstance(state, NoInput) or self._state == state:
134+
self._value = NoInput(0)
135+
self._state = 0
99136

100137
@property
101138
def value(self) -> DualTypes_:
@@ -796,6 +833,13 @@ def value(self) -> DualTypes_:
796833
)
797834
return self._value
798835

836+
def reset(self, state: int_ = NoInput(0)) -> None:
837+
if not isinstance(self._fixing1, NoInput):
838+
self._fixing1.reset(state=state)
839+
if not isinstance(self._fixing2, NoInput):
840+
self._fixing2.reset(state=state)
841+
self._value = NoInput(0)
842+
799843
@cached_property
800844
def weights(self) -> tuple[float, float]:
801845
"""Scalar multiplier to apply to each tenor fixing for the interpolation."""
@@ -1019,6 +1063,12 @@ def __init__(
10191063
self._method_param = method_param
10201064
self._populated = Series(index=[], data=[], dtype=float) # type: ignore[assignment]
10211065

1066+
def reset(self, state: int_ = NoInput(0)) -> None:
1067+
if isinstance(state, NoInput) or self._state == state:
1068+
self._populated = Series(index=[], data=[], dtype=float) # type: ignore[assignment]
1069+
self._value = NoInput(0)
1070+
self._state = 0
1071+
10221072
@property
10231073
def fixing_method(self) -> FloatFixingMethod:
10241074
"""The :class:`FloatFixingMethod` object used to combine multiple RFR fixings."""

python/rateslib/data/loader.py

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,15 @@
1212
from rateslib.enums.generics import Err, NoInput, Ok, _drb
1313

1414
if TYPE_CHECKING:
15-
from rateslib.typing import Adjuster, CalTypes, DualTypes, FloatRateSeries, Result, datetime_
15+
from rateslib.typing import (
16+
Adjuster,
17+
CalTypes,
18+
DualTypes,
19+
FloatRateSeries,
20+
Result,
21+
datetime_,
22+
int_,
23+
)
1624

1725

1826
class _BaseFixingsLoader(metaclass=ABCMeta):
@@ -50,7 +58,7 @@ def __getitem__(self, name: str) -> tuple[int, Series[DualTypes], tuple[datetime
5058
pass
5159

5260
@abstractmethod
53-
def add(self, name: str, series: Series[DualTypes]) -> None: # type: ignore[type-var]
61+
def add(self, name: str, series: Series[DualTypes], state: int_ = NoInput(0)) -> None: # type: ignore[type-var]
5462
"""
5563
Add a timeseries to the data loader directly from Python.
5664
@@ -278,14 +286,18 @@ def __getitem__(self, name: str) -> tuple[int, Series[DualTypes], tuple[datetime
278286
self.loaded[name_] = data
279287
return data
280288

281-
def add(self, name: str, series: Series[DualTypes]) -> None: # type: ignore[type-var]
289+
def add(self, name: str, series: Series[DualTypes], state: int_ = NoInput(0)) -> None: # type: ignore[type-var]
282290
if name in self.loaded:
283291
raise ValueError(f"Fixing data for the index '{name}' has already been loaded.")
284292
s = series.sort_index(ascending=True)
285293
s.index.name = "reference_date"
286294
s.name = "rate"
287295
name_ = name.upper()
288-
self.loaded[name_] = (hash(os.urandom(8)), s, (s.index[0], s.index[-1]))
296+
if isinstance(state, NoInput):
297+
state_: int = hash(os.urandom(64))
298+
else:
299+
state_ = state
300+
self.loaded[name_] = (state_, s, (s.index[0], s.index[-1]))
289301

290302
def pop(self, name: str) -> Series[DualTypes] | None: # type: ignore[type-var]
291303
name_ = name.upper()
@@ -380,10 +392,44 @@ def loader(self) -> _BaseFixingsLoader:
380392
"""
381393
return self._loader
382394

383-
def add(self, name: str, series: Series[DualTypes]) -> None: # type: ignore[type-var]
384-
return self.loader.add(name, series)
395+
def add(self, name: str, series: Series[DualTypes], state: int_ = NoInput(0)) -> None: # type: ignore[type-var]
396+
"""
397+
Add a Series to the Fixings object directly from Python
398+
399+
.. role:: red
400+
401+
.. role:: green
402+
403+
Parameters
404+
----------
405+
name: str, :red:`required`
406+
The string identifier key for the timeseries.
407+
series: Series, :red:`required`
408+
The timeseries indexed by datetime.
409+
state, int, :green:`optional`
410+
The state id to be used upon insertion of the Series.
411+
412+
Returns
413+
-------
414+
None
415+
"""
416+
return self.loader.add(name, series, state)
385417

386418
def pop(self, name: str) -> Series[DualTypes] | None: # type: ignore[type-var]
419+
"""
420+
Remove a Series from the Fixings object.
421+
422+
.. role:: red
423+
424+
Parameters
425+
----------
426+
name: str, :red:`required`
427+
The string identifier key for the timeseries.
428+
429+
Returns
430+
-------
431+
Series, or None (if name not found)
432+
"""
387433
return self.loader.pop(name)
388434

389435

python/rateslib/default.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ class Defaults:
295295

296296
def __new__(cls) -> Defaults:
297297
if cls._instance is None:
298-
print("Creating the object")
298+
# Singleton pattern creates only one instance: TODO (low) might not be thread safe
299299
cls._instance = super(Defaults, cls).__new__(cls) # noqa: UP008
300300

301301
for k, v in DEFAULTS.items():

python/rateslib/instruments/protocols/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from rateslib.instruments.protocols.analytic_delta import _WithAnalyticDelta
77
from rateslib.instruments.protocols.analytic_fixings import _WithAnalyticRateFixings
88
from rateslib.instruments.protocols.cashflows import _WithCashflows
9+
from rateslib.instruments.protocols.fixings import _WithFixings
910
from rateslib.instruments.protocols.kwargs import _KWArgs
1011
from rateslib.instruments.protocols.npv import _WithNPV
1112
from rateslib.instruments.protocols.rate import _WithRate
@@ -21,6 +22,7 @@ class _BaseInstrument(
2122
_WithNPV,
2223
_WithRate,
2324
_WithCashflows,
25+
_WithFixings,
2426
_WithAnalyticDelta,
2527
_WithAnalyticRateFixings,
2628
metaclass=ABCMeta,
@@ -33,6 +35,7 @@ class _BaseInstrument(
3335
"_WithNPV",
3436
"_WithRate",
3537
"_WithCashflows",
38+
"_WithFixings",
3639
"_WithAnalyticDelta",
3740
"_WithAnalyticRateFixings",
3841
"_WithSensitivities",
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Protocol
4+
5+
from pandas import DataFrame, Series
6+
7+
from rateslib.enums.generics import NoInput
8+
from rateslib.periods.protocols.fixings import (
9+
_replace_fixings_with_ad_variables,
10+
_reset_fixings_data,
11+
_structure_sensitivity_data,
12+
)
13+
14+
if TYPE_CHECKING:
15+
from rateslib.typing import (
16+
CurvesT_,
17+
DualTypes,
18+
FXForwards_,
19+
Sequence,
20+
Solver_,
21+
VolT_,
22+
datetime_,
23+
int_,
24+
str_,
25+
)
26+
27+
28+
class _WithFixings(Protocol):
29+
"""
30+
Protocol for determining fixing sensitivity for a *Period* with AD.
31+
32+
.. rubric:: Provided methods
33+
34+
.. autosummary::
35+
36+
~_WithFixings.reset_fixings
37+
38+
"""
39+
40+
def npv(
41+
self,
42+
*,
43+
curves: CurvesT_ = NoInput(0),
44+
solver: Solver_ = NoInput(0),
45+
fx: FXForwards_ = NoInput(0),
46+
vol: VolT_ = NoInput(0),
47+
base: str_ = NoInput(0),
48+
local: bool = False,
49+
settlement: datetime_ = NoInput(0),
50+
forward: datetime_ = NoInput(0),
51+
) -> DualTypes | dict[str, DualTypes]: ...
52+
53+
def reset_fixings(self, state: int_ = NoInput(0)) -> None:
54+
"""
55+
Resets any fixings values of the *Instrument* derived using the given data state.
56+
57+
.. role:: green
58+
59+
Parameters
60+
----------
61+
state: int, :green:`optional`
62+
The *state id* of the data series that set the fixing. Only fixings determined by this
63+
data will be reset. If not given resets all fixings.
64+
65+
Returns
66+
-------
67+
None
68+
"""
69+
if hasattr(self, "legs"):
70+
for leg in self.legs:
71+
leg.reset_fixings(state)
72+
elif hasattr(self, "instruments"):
73+
for inst in self.instruments:
74+
inst.reset_fixings(state)
75+
76+
def local_fixings(
77+
self,
78+
identifiers: Sequence[tuple[str, Series]],
79+
scalars: Sequence[float] | NoInput = NoInput(0),
80+
curves: CurvesT_ = NoInput(0),
81+
solver: Solver_ = NoInput(0),
82+
fx: FXForwards_ = NoInput(0),
83+
vol: VolT_ = NoInput(0),
84+
settlement: datetime_ = NoInput(0),
85+
forward: datetime_ = NoInput(0),
86+
) -> DataFrame:
87+
"""
88+
Calculate the sensitivity to fixings of the *Instrument*, expressed in local
89+
settlement currency.
90+
91+
.. role:: red
92+
93+
.. role:: green
94+
95+
Parameters
96+
----------
97+
identifiers: Sequence of tuple[str, Series], :red:`required`
98+
These are the series string identifiers and the data values that will be used in each
99+
Series to determine the sensitivity against.
100+
scalars: Sequence of floats, :green:`optional (each set as 1.0)`
101+
A sequence of scalars to multiply the sensitivities by for each on of the
102+
``identifiers``.
103+
curves: _Curves, :green:`optional`
104+
Pricing objects. See **Pricing** on each *Instrument* for details of allowed inputs.
105+
solver: Solver, :green:`optional`
106+
A :class:`~rateslib.solver.Solver` object containing *Curve*, *Smile*, *Surface*, or
107+
*Cube* mappings for pricing.
108+
fx: FXForwards, :green:`optional`
109+
The :class:`~rateslib.fx.FXForwards` object used for forecasting FX rates, if necessary.
110+
vol: _Vol, :green:`optional`
111+
Pricing objects. See **Pricing** on each *Instrument* for details of allowed inputs.
112+
settlement: datetime, :green:`optional`
113+
The assumed settlement date of the *PV* determination. Used only to evaluate
114+
*ex-dividend* status.
115+
forward: datetime, :green:`optional`
116+
The future date to project the *PV* to using the ``disc_curve``.
117+
118+
Returns
119+
-------
120+
DataFrame
121+
"""
122+
original_data, index, state = _replace_fixings_with_ad_variables(identifiers)
123+
# Extract sensitivity data
124+
pv: dict[str, DualTypes] = self.npv( # type: ignore[assignment]
125+
curves=curves,
126+
solver=solver,
127+
fx=fx,
128+
vol=vol,
129+
settlement=settlement,
130+
forward=forward,
131+
local=True,
132+
)
133+
df = _structure_sensitivity_data(pv, index, identifiers, scalars)
134+
_reset_fixings_data(self, original_data, state, identifiers)
135+
return df

0 commit comments

Comments
 (0)