Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: add EUREX calc mode for CFs of Eur denominated GBs #699

Merged
merged 5 commits into from
Mar 8, 2025
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
3 changes: 3 additions & 0 deletions docs/source/i_whatsnew.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ email contact, see `rateslib <https://rateslib.com>`_.
- Allow custom calendar additions to ``defaults.calendars`` and fast fetching with
:meth:`~rateslib.calendars.get_calendar`.
(`684 <https://github.com/attack68/rateslib/pull/684>`_)
* - Instruments
- Add ``calc_mode`` *'eurex_eur'* for :class:`~rateslib.instruments.BondFuture`.
(`699 <https://github.com/attack68/rateslib/pull/699>`_)
* - Refactor
- Rename :class:`~rateslib.instruments.BaseMixin` to :class:`~rateslib.instruments.Metrics`.
(`678 <https://github.com/attack68/rateslib/pull/678>`_)
Expand Down
39 changes: 38 additions & 1 deletion python/rateslib/instruments/bonds/futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from pandas import DataFrame

from rateslib import defaults
from rateslib.calendars import _get_years_and_months, get_calendar
from rateslib.calendars import _get_years_and_months, add_tenor, get_calendar
from rateslib.curves import Curve
from rateslib.default import NoInput, _drb
from rateslib.dual.utils import _dual_float
Expand Down Expand Up @@ -51,6 +51,8 @@ class BondFuture(Sensitivities):
- *"ust_short"* which applies to CME 2y, 3y and 5y treasury futures. See
:download:`CME Treasury Conversion Factors<_static/us-treasury-cfs.pdf>`.
- *"ust_long"* which applies to CME 10y and 30y treasury futures.
- *"eurex_eur"* which applies to EUREX EUR denominated government bond futures, except
Italian BTPs which require a different CF formula.

Examples
--------
Expand Down Expand Up @@ -291,6 +293,8 @@ def _conversion_factors(self) -> tuple[DualTypes, ...]:
return tuple(self._cfs_ust(bond, True) for bond in self.basket)
elif self.calc_mode == "ust_long":
return tuple(self._cfs_ust(bond, False) for bond in self.basket)
elif self.calc_mode == "eurex_eur":
return tuple(self._cfs_eurex_eur(bond) for bond in self.basket)
else:
raise ValueError("`calc_mode` must be in {'ytm', 'ust_short', 'ust_long'}")

Expand Down Expand Up @@ -332,6 +336,39 @@ def _cfs_ust(self, bond: FixedRateBond, short: bool) -> float:
_: float = round(factor, 4)
return _

def _cfs_eurex_eur(self, bond: FixedRateBond) -> float:
# TODO: This method is not AD safe: it uses "round" function which destroys derivatives
# See EUREX specs
dd = self.delivery[1]
i = bond._period_index(dd)
ncd = bond.leg1._regular_periods[i].end
ncd1y = add_tenor(ncd, "-1y", "none")
ncd2y = add_tenor(ncd, "-2y", "none")
lcd = bond.leg1._regular_periods[i].start

d_e = float((ncd1y - dd).days)
if d_e < 0:
act1 = float((ncd - ncd1y).days)
else:
act1 = float((ncd1y - ncd2y).days)

d_i = float((ncd1y - lcd).days)
if d_i < 0:
act2 = float((ncd - ncd1y).days)
else:
act2 = float((ncd1y - ncd2y).days)

f = 1.0 + d_e / act1
c = bond.fixed_rate
n = round((bond.leg1.schedule.termination - ncd).days / 365.25)
not_ = self.coupon

_ = 1.0 + not_ / 100

cf = 1 / _**f * (c / 100.0 * d_i / act2 + c / not_ * (_ - 1 / _**n) + 1 / _**n)
cf -= c / 100.0 * (d_i / act2 - d_e / act1)
return round(_dual_float(cf), 6)

def dlv(
self,
future_price: DualTypes,
Expand Down
2 changes: 1 addition & 1 deletion python/rateslib/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -1511,7 +1511,7 @@ def _update_step_(self, algorithm: str) -> NDArray[Nobject]:
@_new_state_post
def _update_fx(self) -> None:
if not isinstance(self.fx, NoInput):
self.fx.update() # note: with no variables this does nothing.
self.fx.update() # note: with no variables this only updates states
for solver in self.pre_solvers:
solver._update_fx()

Expand Down
35 changes: 33 additions & 2 deletions python/tests/test_instruments_bonds.py
Original file line number Diff line number Diff line change
Expand Up @@ -2336,10 +2336,10 @@ def test_repr(self):
(dt(2023, 12, 11), dt(2033, 2, 15), 2.3, 0.744390),
],
)
def test_conversion_factors_eurex_bund(self, delivery, mat, coupon, exp) -> None:
def test_conversion_factors_eurex_bund_ytm(self, delivery, mat, coupon, exp) -> None:
# The expected results are downloaded from the EUREX website
# regarding precalculated conversion factors.
# this test allows for an error in the cf < 1e-4.
# this test allows for an error in the cf < 1e-4, due to YTM method
kwargs = dict(
effective=dt(2020, 1, 1),
stub="ShortFront",
Expand All @@ -2354,6 +2354,37 @@ def test_conversion_factors_eurex_bund(self, delivery, mat, coupon, exp) -> None
result = fut.cfs
assert abs(result[0] - exp) < 1e-4

@pytest.mark.parametrize(
("delivery", "issue", "mat", "coupon", "exp"),
[
(dt(2023, 6, 12), dt(2022, 7, 1), dt(2032, 2, 15), 0.0, 0.603058),
(dt(2023, 6, 12), dt(2022, 7, 8), dt(2032, 8, 15), 1.7, 0.703125),
(dt(2023, 6, 12), dt(2023, 1, 13), dt(2033, 2, 15), 2.3, 0.733943),
(dt(2023, 9, 11), dt(2022, 7, 8), dt(2032, 8, 15), 1.7, 0.709321),
(dt(2023, 9, 11), dt(2023, 1, 13), dt(2033, 2, 15), 2.3, 0.739087),
(dt(2023, 12, 11), dt(2022, 7, 8), dt(2032, 8, 15), 1.7, 0.715464),
(dt(2023, 12, 11), dt(2023, 1, 13), dt(2033, 2, 15), 2.3, 0.744390),
],
)
def test_conversion_factors_eurex_bund_method(self, delivery, issue, mat, coupon, exp) -> None:
# The expected results are downloaded from the EUREX website
# regarding precalculated conversion factors.
# these should be exact due to specifically coded methods
kwargs = dict(
effective=issue,
stub="LongFront",
frequency="A",
calendar="tgt",
currency="eur",
convention="ActActICMA",
modifier="none",
)
bond1 = FixedRateBond(termination=mat, fixed_rate=coupon, **kwargs)

fut = BondFuture(delivery=delivery, coupon=6.0, basket=[bond1], calc_mode="eurex_eur")
result = fut.cfs
assert result[0] == exp

@pytest.mark.parametrize(
("mat", "coupon", "exp"),
[
Expand Down