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
1 change: 1 addition & 0 deletions docs/source/i_whatsnew.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ email contact, see `rateslib <https://rateslib.com>`_.
(`467 <https://github.com/attack68/rateslib/pull/467>`_)
(`470 <https://github.com/attack68/rateslib/pull/470>`_)
(`490 <https://github.com/attack68/rateslib/pull/490>`_)
(`493 <https://github.com/attack68/rateslib/pull/493>`_)
* - Instruments
- Add a :meth:`~rateslib.instruments.Portfolio.fixings_table` method to *Portfolio* to
aggregate fixings tables on contained and applicable *Instruments*.
Expand Down
33 changes: 33 additions & 0 deletions python/rateslib/instruments/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -992,3 +992,36 @@ def _upper(val: str | NoInput):
if isinstance(val, str):
return val.upper()
return val


def _composit_fixings_table(df_result, df):
"""
Add a DataFrame to an existing fixings table by extending or adding to relevant columns.

Parameters
----------
df_result: The main DataFrame that will be updated
df: The incoming DataFrame with new data to merge

Returns
-------
DataFrame
"""
# reindex the result DataFrame
df_result = df_result.reindex(index=df_result.index.union(df.index))

# update existing columns with missing data from the new available data
for c in [c for c in df.columns if c in df_result.columns and c[1] in ["dcf", "rates"]]:
df_result[c] = df_result[c].combine_first(df[c])

# merge by addition existing values with missing filled to zero
m = [c for c in df.columns if c in df_result.columns and c[1] in ["notional", "risk"]]
if len(m) > 0:
df_result[m] = df_result[m].add(df[m], fill_value=0.0)

# append new columns without additional calculation
a = [c for c in df.columns if c not in df_result.columns]
if len(a) > 0:
df_result[a] = df[a]

return df_result
31 changes: 8 additions & 23 deletions python/rateslib/instruments/generics.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from rateslib.instruments.core import (
BaseMixin,
Sensitivities,
_composit_fixings_table,
_get_curves_fx_and_base_maybe_from_solver,
_get_vol_maybe_from_solver,
)
Expand Down Expand Up @@ -692,27 +693,11 @@ def fixings_table(
"""
df_result = DataFrame()
for inst in self.instruments:
df1 = inst.fixings_table(
curves=curves, solver=solver, fx=fx, base=base, approximate=approximate
)

# reindex the result DataFrame
df_result = df_result.reindex(index=df_result.index.union(df1.index))

# update existing columns with missing data from the new available data
for c in [
c for c in df1.columns if c in df_result.columns and c[1] in ["dcf", "rates"]
]:
df_result[c] = df_result[c].combine_first(df1[c])

# merge by addition existing values with missing filled to zero
m = [c for c in df1.columns if c in df_result.columns and c[1] in ["notional", "risk"]]
if len(m) > 0:
df_result[m] = df_result[m].add(df1[m], fill_value=0.0)

# append new columns without additional calculation
a = [c for c in df1.columns if c not in df_result.columns]
if len(a) > 0:
df_result[a] = df1[a]

try:
df = inst.fixings_table(
curves=curves, solver=solver, fx=fx, base=base, approximate=approximate
)
except AttributeError:
continue
df_result = _composit_fixings_table(df_result, df)
return df_result
5 changes: 3 additions & 2 deletions python/rateslib/instruments/rates_derivatives.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from abc import ABCMeta, abstractmethod
from datetime import datetime

from pandas import DataFrame, Series, concat
from pandas import DataFrame, Series

from rateslib import defaults
from rateslib.calendars import CalInput
Expand All @@ -14,6 +14,7 @@
from rateslib.instruments.core import (
BaseMixin,
Sensitivities,
_composit_fixings_table,
_get,
_get_curves_fx_and_base_maybe_from_solver,
_inherit_or_negate,
Expand Down Expand Up @@ -2239,7 +2240,7 @@ def fixings_table(
df2 = self.leg2.fixings_table(
curve=curves[2], approximate=approximate, disc_curve=curves[3]
)
return concat([df1, df2], keys=("leg1", "leg2"), axis=1)
return _composit_fixings_table(df1, df2)


class FRA(Sensitivities, BaseMixin):
Expand Down
73 changes: 73 additions & 0 deletions python/rateslib/instruments/rates_multi_ccy.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from rateslib.instruments.core import (
BaseMixin,
Sensitivities,
_composit_fixings_table,
_get,
_get_curves_fx_and_base_maybe_from_solver,
_update_not_noinput,
Expand Down Expand Up @@ -755,6 +756,78 @@ def cashflows(
self.leg2._do_not_repeat_set_periods = False # reset the mtm calc
return ret

def fixings_table(
self,
curves: Curve | str | list | NoInput = NoInput(0),
solver: Solver | NoInput = NoInput(0),
fx: float | FXRates | FXForwards | NoInput = NoInput(0),
base: str | NoInput = NoInput(0),
approximate: bool = False,
):
"""
Return a DataFrame of fixing exposures on any :class:`~rateslib.legs.FloatLeg` or
:class:`~rateslib.legs.FloatLegMtm` associated with the *XCS*.

Parameters
----------
curves : Curve, str or list of such
A list defines the following curves in the order:

- Forecasting :class:`~rateslib.curves.Curve` for leg1 (if floating).
- Discounting :class:`~rateslib.curves.Curve` for leg1.
- Forecasting :class:`~rateslib.curves.Curve` for leg2 (if floating).
- Discounting :class:`~rateslib.curves.Curve` for leg2.

solver : Solver, optional
The numerical :class:`~rateslib.solver.Solver` that constructs
:class:`~rateslib.curves.Curve` from calibrating instruments.

.. note::

The arguments ``fx`` and ``base`` are unused and results are returned in
local currency of each *Leg*.

approximate : bool, optional
Perform a calculation that is broadly 10x faster but potentially loses
precision upto 0.1%.

Returns
-------
DataFrame
"""
curves, fx_, base_ = _get_curves_fx_and_base_maybe_from_solver(
self.curves,
solver,
curves,
fx,
base,
self.leg1.currency,
)

try:
df1 = self.leg1.fixings_table(
curve=curves[0],
disc_curve=curves[1],
fx=fx_,
base=base_,
approximate=approximate,
)
except AttributeError:
df1 = DataFrame()

try:
df2 = self.leg2.fixings_table(
curve=curves[2],
disc_curve=curves[3],
fx=fx_,
base=base_,
approximate=approximate,
)
except AttributeError:
df2 = DataFrame()

return _composit_fixings_table(df1, df2)


# Licence: Creative Commons - Attribution-NonCommercial-NoDerivatives 4.0 International
# Commercial use of this code, and/or copying and redistribution is prohibited.
Expand Down
40 changes: 38 additions & 2 deletions python/tests/test_instruments.py
Original file line number Diff line number Diff line change
Expand Up @@ -1300,7 +1300,7 @@ def test_fixings_table(self, curve):
result = inst.fixings_table()
assert isinstance(result, DataFrame)

def test_fixings_table_3s1s(self, curve):
def test_fixings_table_3s1s(self, curve, curve2):
inst = SBS(
dt(2022, 1, 15),
"6m",
Expand All @@ -1310,10 +1310,12 @@ def test_fixings_table_3s1s(self, curve):
leg2_method_param=1,
frequency="Q",
leg2_frequency="m",
curves=curve,
curves=[curve, curve, curve2, curve],
)
result = inst.fixings_table()
assert isinstance(result, DataFrame)
assert len(result.columns) == 8
assert len(result.index) == 8


class TestFRA:
Expand Down Expand Up @@ -2800,6 +2802,34 @@ def test_initialisation_nonmtm_xcs_leg_notional_unused(self) -> None:
)
assert abs(xcs.leg2.notional + 100e6) < 1e-8 # not 20e6

@pytest.mark.parametrize("fixed1", [True, False])
@pytest.mark.parametrize("fixed2", [True, False])
@pytest.mark.parametrize("mtm", [True, False])
def test_fixings_table(self, curve, curve2, fixed1, fixed2, mtm):
curve.id = "c1"
curve2.id = "c2"
fxf = FXForwards(
FXRates({"eurusd": 1.1}, settlement=dt(2022, 1, 3)),
{"usdusd": curve, "eurusd": curve2, "eureur": curve2},
)

xcs = XCS(
dt(2022, 2, 1),
"8M",
frequency="M",
payment_lag=0,
currency="eur",
leg2_currency="usd",
payment_lag_exchange=0,
fixed=fixed1,
leg2_fixed=fixed2,
leg2_mtm=mtm,
fixing_method="ibor",
leg2_fixing_method="ibor",
)
result = xcs.fixings_table(curves=[curve, curve, curve2, curve2], fx=fxf)
assert isinstance(result, DataFrame)


class TestFixedFloatXCS:
def test_mtmfixxcs_rate(self, curve, curve2) -> None:
Expand Down Expand Up @@ -3462,6 +3492,12 @@ def test_fixings_table(self, curve, curve2):
# c2 has DCF
assert abs(result["c2", "dcf"][dt(2022, 1, 22)] - 0.50277) < 1e-3

def test_fixings_table_null_inst(self, curve):
irs = IRS(dt(2022, 1, 15), "6m", spec="eur_irs3", curves=curve)
frb = FixedRateBond(dt(2022, 1, 1), "5y", "A", fixed_rate=2.0, curves=curve)
pf = Portfolio([irs, frb])
assert isinstance(pf.fixings_table(), DataFrame)


class TestFly:
@pytest.mark.parametrize("mechanism", [False, True])
Expand Down
Loading