Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions python/rateslib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def __exit__(self, *args) -> None: # type: ignore[no-untyped-def]
index_left,
index_value,
)
from rateslib.data.fixings import FXFixing, IBORFixing, IBORStubFixing, IndexFixing, RFRFixing
from rateslib.dual import ADOrder, Dual, Dual2, Variable, dual_exp, dual_log, dual_solve, gradient
from rateslib.enums.generics import NoInput
from rateslib.fx import FXForwards, FXRates
Expand Down Expand Up @@ -201,12 +202,12 @@ def __exit__(self, *args) -> None: # type: ignore[no-untyped-def]
"ProxyCurve",
"index_left",
"index_value",
# # analytic_fixings.py cannot load due to circular import
# "FXFixing",
# "IBORFixing",
# "IBORStubFixing",
# "IndexFixing",
# "RFRFixing",
# fixings.py
"FXFixing",
"IBORFixing",
"IBORStubFixing",
"IndexFixing",
"RFRFixing",
# fx_volatility.py
"FXDeltaVolSmile",
"FXDeltaVolSurface",
Expand Down
56 changes: 53 additions & 3 deletions python/rateslib/data/fixings.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
Result,
_BaseCurve_,
datetime_,
int_,
str_,
)

Expand Down Expand Up @@ -85,17 +86,53 @@ def __init__(
self._state = 0
self._date = date

def reset(self) -> None:
def reset(self, state: int_ = NoInput(0)) -> None:
"""
Sets the ``value`` attribute to :class:`rateslib.enums.generics.NoInput`, which allows it
to be redetermined from a timeseries.

.. rubric:: Examples

.. ipython:: python
:suppress:

from rateslib import fixings, dt, NoInput, FXFixing
from pandas import Series

.. ipython:: python

fx_fixing1 = FXFixing(date=dt(2021, 1, 1), pair="eurusd", identifier="A")
fx_fixing2 = FXFixing(date=dt(2021, 1, 1), pair="gbpusd", identifier="B")
fixings.add("A_eurusd", Series(index=[dt(2021, 1, 1)], data=[1.1]), state=100)
fixings.add("B_gbpusd", Series(index=[dt(2021, 1, 1)], data=[1.4]), state=200)

# data is populated from the available Series
fx_fixing1.value
fx_fixing2.value

# fixings are reset according to the data state
fx_fixing1.reset(state=100)
fx_fixing2.reset(state=100)

# only the private data for fixing1 is removed because of its link to the data state
fx_fixing1._value
fx_fixing2._value

.. role:: green

Parameters
----------
state: int, :green:`optional`
If given only fixings whose state matches this value will be reset. If no state is
given then the value will be reset.

Returns
-------
None
"""
self._value = NoInput(0)
self._state = 0
if isinstance(state, NoInput) or self._state == state:
self._value = NoInput(0)
self._state = 0

@property
def value(self) -> DualTypes_:
Expand Down Expand Up @@ -796,6 +833,13 @@ def value(self) -> DualTypes_:
)
return self._value

def reset(self, state: int_ = NoInput(0)) -> None:
if not isinstance(self._fixing1, NoInput):
self._fixing1.reset(state=state)
if not isinstance(self._fixing2, NoInput):
self._fixing2.reset(state=state)
self._value = NoInput(0)

@cached_property
def weights(self) -> tuple[float, float]:
"""Scalar multiplier to apply to each tenor fixing for the interpolation."""
Expand Down Expand Up @@ -1019,6 +1063,12 @@ def __init__(
self._method_param = method_param
self._populated = Series(index=[], data=[], dtype=float) # type: ignore[assignment]

def reset(self, state: int_ = NoInput(0)) -> None:
if isinstance(state, NoInput) or self._state == state:
self._populated = Series(index=[], data=[], dtype=float) # type: ignore[assignment]
self._value = NoInput(0)
self._state = 0

@property
def fixing_method(self) -> FloatFixingMethod:
"""The :class:`FloatFixingMethod` object used to combine multiple RFR fixings."""
Expand Down
58 changes: 52 additions & 6 deletions python/rateslib/data/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,15 @@
from rateslib.enums.generics import Err, NoInput, Ok, _drb

if TYPE_CHECKING:
from rateslib.typing import Adjuster, CalTypes, DualTypes, FloatRateSeries, Result, datetime_
from rateslib.typing import (
Adjuster,
CalTypes,
DualTypes,
FloatRateSeries,
Result,
datetime_,
int_,
)


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

@abstractmethod
def add(self, name: str, series: Series[DualTypes]) -> None: # type: ignore[type-var]
def add(self, name: str, series: Series[DualTypes], state: int_ = NoInput(0)) -> None: # type: ignore[type-var]
"""
Add a timeseries to the data loader directly from Python.

Expand Down Expand Up @@ -278,14 +286,18 @@ def __getitem__(self, name: str) -> tuple[int, Series[DualTypes], tuple[datetime
self.loaded[name_] = data
return data

def add(self, name: str, series: Series[DualTypes]) -> None: # type: ignore[type-var]
def add(self, name: str, series: Series[DualTypes], state: int_ = NoInput(0)) -> None: # type: ignore[type-var]
if name in self.loaded:
raise ValueError(f"Fixing data for the index '{name}' has already been loaded.")
s = series.sort_index(ascending=True)
s.index.name = "reference_date"
s.name = "rate"
name_ = name.upper()
self.loaded[name_] = (hash(os.urandom(8)), s, (s.index[0], s.index[-1]))
if isinstance(state, NoInput):
state_: int = hash(os.urandom(64))
else:
state_ = state
self.loaded[name_] = (state_, s, (s.index[0], s.index[-1]))

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

def add(self, name: str, series: Series[DualTypes]) -> None: # type: ignore[type-var]
return self.loader.add(name, series)
def add(self, name: str, series: Series[DualTypes], state: int_ = NoInput(0)) -> None: # type: ignore[type-var]
"""
Add a Series to the Fixings object directly from Python

.. role:: red

.. role:: green

Parameters
----------
name: str, :red:`required`
The string identifier key for the timeseries.
series: Series, :red:`required`
The timeseries indexed by datetime.
state, int, :green:`optional`
The state id to be used upon insertion of the Series.

Returns
-------
None
"""
return self.loader.add(name, series, state)

def pop(self, name: str) -> Series[DualTypes] | None: # type: ignore[type-var]
"""
Remove a Series from the Fixings object.

.. role:: red

Parameters
----------
name: str, :red:`required`
The string identifier key for the timeseries.

Returns
-------
Series, or None (if name not found)
"""
return self.loader.pop(name)


Expand Down
2 changes: 1 addition & 1 deletion python/rateslib/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ class Defaults:

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

for k, v in DEFAULTS.items():
Expand Down
3 changes: 3 additions & 0 deletions python/rateslib/instruments/protocols/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from rateslib.instruments.protocols.analytic_delta import _WithAnalyticDelta
from rateslib.instruments.protocols.analytic_fixings import _WithAnalyticRateFixings
from rateslib.instruments.protocols.cashflows import _WithCashflows
from rateslib.instruments.protocols.fixings import _WithFixings
from rateslib.instruments.protocols.kwargs import _KWArgs
from rateslib.instruments.protocols.npv import _WithNPV
from rateslib.instruments.protocols.rate import _WithRate
Expand All @@ -21,6 +22,7 @@ class _BaseInstrument(
_WithNPV,
_WithRate,
_WithCashflows,
_WithFixings,
_WithAnalyticDelta,
_WithAnalyticRateFixings,
metaclass=ABCMeta,
Expand All @@ -33,6 +35,7 @@ class _BaseInstrument(
"_WithNPV",
"_WithRate",
"_WithCashflows",
"_WithFixings",
"_WithAnalyticDelta",
"_WithAnalyticRateFixings",
"_WithSensitivities",
Expand Down
135 changes: 135 additions & 0 deletions python/rateslib/instruments/protocols/fixings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Protocol

from pandas import DataFrame, Series

from rateslib.enums.generics import NoInput
from rateslib.periods.protocols.fixings import (
_replace_fixings_with_ad_variables,
_reset_fixings_data,
_structure_sensitivity_data,
)

if TYPE_CHECKING:
from rateslib.typing import (
CurvesT_,
DualTypes,
FXForwards_,
Sequence,
Solver_,
VolT_,
datetime_,
int_,
str_,
)


class _WithFixings(Protocol):
"""
Protocol for determining fixing sensitivity for a *Period* with AD.
.. rubric:: Provided methods
.. autosummary::
~_WithFixings.reset_fixings
"""

def npv(
self,
*,
curves: CurvesT_ = NoInput(0),
solver: Solver_ = NoInput(0),
fx: FXForwards_ = NoInput(0),
vol: VolT_ = NoInput(0),
base: str_ = NoInput(0),
local: bool = False,
settlement: datetime_ = NoInput(0),
forward: datetime_ = NoInput(0),
) -> DualTypes | dict[str, DualTypes]: ...

def reset_fixings(self, state: int_ = NoInput(0)) -> None:
"""
Resets any fixings values of the *Instrument* derived using the given data state.
.. role:: green
Parameters
----------
state: int, :green:`optional`
The *state id* of the data series that set the fixing. Only fixings determined by this
data will be reset. If not given resets all fixings.
Returns
-------
None
"""
if hasattr(self, "legs"):
for leg in self.legs:
leg.reset_fixings(state)
elif hasattr(self, "instruments"):
for inst in self.instruments:
inst.reset_fixings(state)

def local_fixings(
self,
identifiers: Sequence[tuple[str, Series]],
scalars: Sequence[float] | NoInput = NoInput(0),
curves: CurvesT_ = NoInput(0),
solver: Solver_ = NoInput(0),
fx: FXForwards_ = NoInput(0),
vol: VolT_ = NoInput(0),
settlement: datetime_ = NoInput(0),
forward: datetime_ = NoInput(0),
) -> DataFrame:
"""
Calculate the sensitivity to fixings of the *Instrument*, expressed in local
settlement currency.
.. role:: red
.. role:: green
Parameters
----------
identifiers: Sequence of tuple[str, Series], :red:`required`
These are the series string identifiers and the data values that will be used in each
Series to determine the sensitivity against.
scalars: Sequence of floats, :green:`optional (each set as 1.0)`
A sequence of scalars to multiply the sensitivities by for each on of the
``identifiers``.
curves: _Curves, :green:`optional`
Pricing objects. See **Pricing** on each *Instrument* for details of allowed inputs.
solver: Solver, :green:`optional`
A :class:`~rateslib.solver.Solver` object containing *Curve*, *Smile*, *Surface*, or
*Cube* mappings for pricing.
fx: FXForwards, :green:`optional`
The :class:`~rateslib.fx.FXForwards` object used for forecasting FX rates, if necessary.
vol: _Vol, :green:`optional`
Pricing objects. See **Pricing** on each *Instrument* for details of allowed inputs.
settlement: datetime, :green:`optional`
The assumed settlement date of the *PV* determination. Used only to evaluate
*ex-dividend* status.
forward: datetime, :green:`optional`
The future date to project the *PV* to using the ``disc_curve``.
Returns
-------
DataFrame
"""
original_data, index, state = _replace_fixings_with_ad_variables(identifiers)
# Extract sensitivity data
pv: dict[str, DualTypes] = self.npv( # type: ignore[assignment]
curves=curves,
solver=solver,
fx=fx,
vol=vol,
settlement=settlement,
forward=forward,
local=True,
)
df = _structure_sensitivity_data(pv, index, identifiers, scalars)
_reset_fixings_data(self, original_data, state, identifiers)
return df
Loading
Loading