diff --git a/.gitignore b/.gitignore index 2348d8a5..60c771ed 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ share/python-wheels/ *.egg-info/ .installed.cfg *.egg +.pypirc MANIFEST # PyInstaller diff --git a/docs/source/_static/badges.json b/docs/source/_static/badges.json index 4c94fb1b..3a074f51 100644 --- a/docs/source/_static/badges.json +++ b/docs/source/_static/badges.json @@ -1,9 +1,9 @@ { "coverage": "99%", "licence": "CC-BY-NC-ND-4.0", - "pypi": "v0.7.0", - "conda": "v0.7.0", + "pypi": "v1.0.0", + "conda": "v1.0.0", "python": "3.9 | 3.10 | 3.11", "style": "black", - "status": "beta" + "status": "stable" } \ No newline at end of file diff --git a/docs/source/_static/switcher.json b/docs/source/_static/switcher.json index 0e054e66..db61c087 100644 --- a/docs/source/_static/switcher.json +++ b/docs/source/_static/switcher.json @@ -35,8 +35,13 @@ "url": "https://rateslib.readthedocs.io/en/0.6.x/" }, { - "name": "stable", + "name": "0.7.x", "version": "0.7.x", + "url": "https://rateslib.readthedocs.io/en/0.7.x/" + }, + { + "name": "stable", + "version": "1.0.0", "url": "https://rateslib.readthedocs.io/en/stable/" } ] \ No newline at end of file diff --git a/docs/source/_static/thumb_coding_3.png b/docs/source/_static/thumb_coding_3.png new file mode 100644 index 00000000..dae8c6c3 Binary files /dev/null and b/docs/source/_static/thumb_coding_3.png differ diff --git a/docs/source/_static/thumb_ptirds4.png b/docs/source/_static/thumb_ptirds4.png new file mode 100644 index 00000000..7c5de539 Binary files /dev/null and b/docs/source/_static/thumb_ptirds4.png differ diff --git a/docs/source/f_fxf.rst b/docs/source/f_fxf.rst index 70fd07c9..43c8b7da 100644 --- a/docs/source/f_fxf.rst +++ b/docs/source/f_fxf.rst @@ -181,7 +181,7 @@ not provided at initialisation, nor is the "eurgbp" curve. In these circumstances the :meth:`~rateslib.fx.FXForwards.curve` method will derive the combination of existing curves that can be combined to yield required DFs on-the-fly. -This creates a :class:`~rateslib.fx.ProxyCurve`. +This creates a :class:`~rateslib.curves.ProxyCurve`. In the above framework GBP is the cheapest to deliver collateral, and USD is the most expensive. We can observe this diff --git a/docs/source/f_fxr.rst b/docs/source/f_fxr.rst index 74927d07..321848dd 100644 --- a/docs/source/f_fxr.rst +++ b/docs/source/f_fxr.rst @@ -11,7 +11,7 @@ FX Spot Rates *********************** This documentation page discusses the methods of the -:class:`~rateslib.fx.FXRates` class which are summarised below, +:class:`~rateslib.fx.FXRates` class which are summarised below: .. autosummary:: rateslib.fx.FXRates diff --git a/docs/source/i_about.rst b/docs/source/i_about.rst index 4674b7a7..4e0ca971 100644 --- a/docs/source/i_about.rst +++ b/docs/source/i_about.rst @@ -17,22 +17,35 @@ The History and Context of Rateslib The foundations of *rateslib* is really the code library `Book IRDS3 `_, which laid down basic principles and was a sandbox code environment for the -publication *Pricing and Trading Interest Rate Derivatives: A Practical Guide to Swaps -(2022, 3rd Edition)*. Some of the code and algorithms also date back to the author's -time trading IRSs as a market-maker between 2006 and 2017. - -.. image:: _static/thumb_ptirds3.png - :alt: Pricing and Trading Interest Rate Derivatives - :target: https://www.amazon.com/Pricing-Trading-Interest-Rate-Derivatives/dp/0995455538 - :width: 92 - -The algorithms and mathematical work of *rateslib* is expected to be released in -early 2024 under the working title of *Coding Interest Rates: FX, Swaps and Bonds* - -.. image:: _static/thumb_coding_2.png - :alt: Coding Interest Rates: FX, Swaps and Bonds - :target: https://www.amazon.com/Pricing-Trading-Interest-Rate-Derivatives/dp/0995455538 - :width: 92 +publication `Pricing and Trading Interest Rate Derivatives: A Practical Guide to Swaps `_. +Some of the code and algorithms also date back to the author's +time trading IRSs as a market-maker between 2006 and 2017. The algorithms and mathematical +code developments of *rateslib* +are all characterised and explained in +`Coding Interest Rates: FX, Swaps and Bonds `_. + +.. container:: twocol + + .. container:: leftside40 + + .. image:: _static/thumb_coding_3.png + :alt: Coding Interest Rates: FX, Swaps and Bonds + :target: https://www.amazon.com/dp/0995455554 + :width: 145 + :align: center + + .. container:: rightside60 + + .. image:: _static/thumb_ptirds3.png + :alt: Pricing and Trading Interest Rate Derivatives + :target: https://www.amazon.com/Pricing-Trading-Interest-Rate-Derivatives/dp/0995455538 + :width: 145 + :align: center + +.. raw:: html + +
+ .. _pillars-doc: diff --git a/docs/source/i_get_started.rst b/docs/source/i_get_started.rst index 2bf3b0fe..6f7f01d9 100644 --- a/docs/source/i_get_started.rst +++ b/docs/source/i_get_started.rst @@ -166,9 +166,9 @@ Does *rateslib* ``solve`` curves relative to market prices? **Yes**, when a :class:`~rateslib.solver.Solver` is configured along with all the intended *Instruments* and their relevant *prices*. -Multiple algorithms (*gradient descent, Gauss-Newton, Leveberg-Marquardt*) and stopping criteria +Multiple algorithms (*gradient descent, Gauss-Newton, Levenberg-Marquardt*) and stopping criteria can be used within the optimization routine -to simultaneously solve multiple *Curve* parameter. +to simultaneously solve multiple *Curve* parameters. The *Solver* can even construct dependency chains, like sequentially building curves with dependencies to other desks on an investment bank trading floor, and internally manage all of diff --git a/docs/source/i_guide.rst b/docs/source/i_guide.rst index dab58902..a3065abe 100644 --- a/docs/source/i_guide.rst +++ b/docs/source/i_guide.rst @@ -160,7 +160,7 @@ parameters that are used to build up the instruments. Multi-currency instruments -------------------------- -Lets take a quick look at a multi-currency instrument: the +Let's take a quick look at a multi-currency instrument: the :class:`~rateslib.instruments.FXSwap`. All instruments have a mid-market pricing function :meth:`rate()`. Keeping a consistent function name across all *Instruments* allows any of them to be used within a @@ -262,7 +262,7 @@ a set of instrument prices, ``s``. id="us_rates" ) -Solving was a success! Observe the DFs on the *Curve* have been updated: +Solving was a success! Observe that the DFs on the *Curve* have been updated: .. ipython:: python @@ -324,7 +324,7 @@ detailed instructions of the way in which the associations can be constructed in g_mechanisms.rst The **key takeway** is that when you initialise and create an *Instrument* you can do one -of three things; +of three things: 1) Not provide any *Curves* for pricing upfront (``curves=NoInput(0)``). 2) Create an explicit association to pre-existing Python objects, e.g. ``curves=my_curve``. @@ -398,4 +398,4 @@ into any one category. See the :ref:`Cookbook index `. .. toctree:: :hidden: - g_coverage.rst \ No newline at end of file + g_coverage.rst diff --git a/docs/source/i_whatsnew.rst b/docs/source/i_whatsnew.rst index 0996e7c8..38ea66c6 100644 --- a/docs/source/i_whatsnew.rst +++ b/docs/source/i_whatsnew.rst @@ -22,14 +22,6 @@ email contact through **rateslib@gmail.com**. - Description - Consideration - Timeframe - * - Coding Interest Rates - - Officially document this library's algorithms and release the book. - - Planned - - End 2023 - * - Version 1.0 - - Release the official first non-beta version of this library. - - Planned - - End 2023 * - Vanilla FX options and volatility products - Adding option instruments and benchmark trades such as risk-reversals. - Highly likely (v2.0?) @@ -53,8 +45,8 @@ email contact through **rateslib@gmail.com**. - no ETA -1.0.0 (Not released) -********************** +1.1.0 (not released) +********************* .. list-table:: :widths: 25 75 @@ -73,6 +65,23 @@ email contact through **rateslib@gmail.com**. settled payment date. +1.0.0 (1st Feb 2024) +********************** + +.. list-table:: + :widths: 25 75 + :header-rows: 1 + + * - Feature + - Description + * - Bug + - FRA :meth:`~rateslib.instruments.FRA.cashflows` now correctly identifies the DF at cash + settled payment date. + * - Bug + - :meth:`~rateslib.legs.FloatLeg.fixings_table` now generates exact results (not in approximate mode) when RFR + fixings are included in any period. + + 0.7.0 (29th Nov 2023) ********************** diff --git a/docs/source/index.rst b/docs/source/index.rst index eeb323ad..ed0f39ec 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -11,7 +11,7 @@ PyPi Conda Licence - Status + Status Coverage Code Style diff --git a/pyproject.toml b/pyproject.toml index b62c26b5..c6558bda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [project] name = "rateslib" -version = "0.7.0" +version = "1.0.0" description = "A fixed income library for trading interest rates" readme = "README.md" authors = [{ name = "J H M Darbyshire"}] diff --git a/rateslib/default.py b/rateslib/default.py index e6abdecf..4dbad1cf 100644 --- a/rateslib/default.py +++ b/rateslib/default.py @@ -15,7 +15,7 @@ class NoInput(Enum): """ Enumerable type to handle setting default values. - See :ref:``. + See :ref:`default values `. """ blank = 0 inherit = 1 diff --git a/rateslib/fx.py b/rateslib/fx.py index 82217b24..5dc7a518 100644 --- a/rateslib/fx.py +++ b/rateslib/fx.py @@ -125,6 +125,7 @@ def _convert_dual(k, v): # find currencies self.pairs = [k for k in self.fx_rates.keys()] + self.pairs_settlement = {pair: settlement for pair in self.pairs} self.variables = tuple(f"fx_{pair}" for pair in self.pairs) self.currencies = { k: i @@ -838,6 +839,9 @@ def update( fx_rates=fx_rates_obj, fx_curves=sub_curves, ) + settlement_pairs = { + pair: fx_rates_obj.settlement for pair in fx_rates_obj.pairs + } else: # calculate additional FX rates from previous objects # in the same settlement frame. @@ -860,6 +864,9 @@ def update( self.fx_curves, fx_rates_obj.currencies_list + pre_currencies ) acyclic_fxf = FXForwards(fx_rates=combined_fx_rates, fx_curves=sub_curves) + settlement_pairs.update( + {pair: fx_rates_obj.settlement for pair in fx_rates_obj.pairs} + ) if base is not NoInput.blank: acyclic_fxf.base = base.lower() @@ -874,6 +881,7 @@ def update( "pairs", ]: setattr(self, attr, getattr(acyclic_fxf, attr)) + self.pairs_settlement = settlement_pairs else: self.currencies = self.fx_rates.currencies self.q = len(self.currencies.keys()) @@ -885,6 +893,7 @@ def update( self.pairs = self.fx_rates.pairs self.variables = tuple(f"fx_{pair}" for pair in self.pairs) self.fx_rates_immediate = self._update_fx_rates_immediate() + self.pairs_settlement = self.fx_rates.pairs_settlement def __init__( self, diff --git a/rateslib/instruments.py b/rateslib/instruments.py index 50a13723..2b0fb61f 100644 --- a/rateslib/instruments.py +++ b/rateslib/instruments.py @@ -38,9 +38,9 @@ from rateslib import defaults from rateslib.default import NoInput -from rateslib.calendars import add_tenor, get_calendar, dcf, _get_years_and_months +from rateslib.calendars import add_tenor, get_calendar, dcf, _get_years_and_months, _DCF1d -from rateslib.curves import Curve, index_left, LineCurve, CompositeCurve, IndexCurve +from rateslib.curves import Curve, index_left, LineCurve, CompositeCurve, IndexCurve, average_rate from rateslib.solver import Solver from rateslib.periods import ( Cashflow, @@ -1455,6 +1455,7 @@ def fwd_from_repo( repo_rate: Union[float, Dual, Dual2], convention: Union[str, NoInput] = NoInput(0), dirty: bool = False, + method: str = "proceeds" ): """ Return a forward price implied by a given repo rate. @@ -1474,6 +1475,8 @@ def fwd_from_repo( values. dirty : bool, optional Whether the input and output price are specified including accrued interest. + method : str in {"proceeds", "compounded"}, optional + The method for determining the forward price. Returns ------- @@ -1515,9 +1518,17 @@ def fwd_from_repo( for p_idx in range(settlement_idx, fwd_settlement_idx): # deduct accrued coupon from dirty price - dcf_ = dcf(self.leg1.periods[p_idx].payment, forward_settlement, convention) - accrued_coup = self.leg1.periods[p_idx].cashflow * (1 + dcf_ * repo_rate / 100) - total_rtn -= accrued_coup + if method.lower() == "proceeds": + dcf_ = dcf(self.leg1.periods[p_idx].payment, forward_settlement, convention) + accrued_coup = self.leg1.periods[p_idx].cashflow * (1 + dcf_ * repo_rate / 100) + total_rtn -= accrued_coup + elif method.lower() == "compounded": + r_bar, d, _ = average_rate(settlement, forward_settlement, convention, repo_rate) + n = (forward_settlement - self.leg1.periods[p_idx].payment).days + accrued_coup = self.leg1.periods[p_idx].cashflow * (1 + d * r_bar / 100) ** n + total_rtn -= accrued_coup + else: + raise ValueError("`method` must be in {'proceeds', 'compounded'}.") forward_price = total_rtn / -self.leg1.notional * 100 if dirty: @@ -1881,27 +1892,36 @@ def oaspread( self.curves, solver, curves, fx, base, self.leg1.currency ) ad_ = curves[1].ad - curves[1]._set_ad_order(2) - disc_curve = curves[1].shift(Dual2(0, "z_spread"), composite=False) - curves[1]._set_ad_order(0) metric = "dirty_price" if dirty else "clean_price" + + curves[1]._set_ad_order(1) + disc_curve = curves[1].shift(Dual(0, "z_spread"), composite=False) npv_price = self.rate(curves=[curves[0], disc_curve], metric=metric) + # find a first order approximation of z + b = npv_price.gradient("z_spread", 1)[0] + c = float(npv_price) - float(price) + z_hat = -c / b + + # shift the curve to the first order approximation and fine tune with 2nd order approxim. + curves[1]._set_ad_order(2) + disc_curve = curves[1].shift(Dual2(z_hat, "z_spread"), composite=False) + npv_price = self.rate(curves=[curves[0], disc_curve], metric=metric) a, b = ( 0.5 * npv_price.gradient("z_spread", 2)[0][0], npv_price.gradient("z_spread", 1)[0], ) - z = _quadratic_equation(a, b, float(npv_price) - float(price)) - # first z is solved by using 1st and 2nd derivatives to get close to target NPV + z_hat2 = _quadratic_equation(a, b, float(npv_price) - float(price)) - # TODO (low) add a tolerance here to continually converge to the solution, via GradDes? - disc_curve = curves[1].shift(z, composite=False) + # perform one final approximation albeit the additional price calculation slows calc time + curves[1]._set_ad_order(0) + disc_curve = curves[1].shift(z_hat+z_hat2, composite=False) npv_price = self.rate(curves=[curves[0], disc_curve], metric=metric) - diff = npv_price - price - new_b = b + 2 * a * z - z = z - diff / new_b - # then a final linear adjustment is made which is usually very small + b = b + 2 * a * z_hat2 # forecast the new gradient + c = float(npv_price) - float(price) + z_hat3 = -c / b + z = z_hat + z_hat2 + z_hat3 curves[1]._set_ad_order(ad_) return z @@ -1998,7 +2018,7 @@ class FixedRateBond(Sensitivities, BondMixin, BaseMixin): - "ust": US Treasury street convention. Same as "ukg" except long stub periods have linear proportioning only in the segregated short stub part. - "ust_31bii": US Treasury convention that reprices examples in federal documents: Section - 31-B-ii). + 31-B-ii). Otherwise referred to as the 'Treasury' method. - "sgb": Swedish government bond convention. Accrued ignores the convention and calculates using 30e360, also for back stubs. - "cadgb" Canadian government bond convention. Accrued is calculated using an ACT365F @@ -2926,7 +2946,7 @@ def rate( Only used if ``fx`` is an ``FXRates`` or ``FXForwards`` object. metric : str, optional Metric returned by the method. Available options are {"clean_price", - "dirty_price", "ytm"} + "dirty_price", "ytm", "index_clean_price", "index_dirty_price"} forward_settlement : datetime, optional The forward settlement date. If not given uses the discount *Curve* and the ``settle`` attribute of the bond. @@ -7609,7 +7629,7 @@ class FXSwap(XCS): Parameters ---------- args : dict - Required positional args to :class:`BaseXCS`. + Required positional args to :class:`XCS`. fx_fixings : float, FXForwards or None The initial FX fixing where leg 1 is considered the domestic currency. For example for an ESTR/SOFR XCS in 100mm EUR notional a value of 1.10 for `fx0` @@ -7622,7 +7642,7 @@ class FXSwap(XCS): The accrued notional at termination of the domestic leg accounting for interest payable at domestic interest rates. kwargs : dict - Required keyword arguments to :class:`BaseXCS`. + Required keyword arguments to :class:`XCS`. Notes ----- diff --git a/rateslib/legs.py b/rateslib/legs.py index a37387ab..073c54ce 100644 --- a/rateslib/legs.py +++ b/rateslib/legs.py @@ -668,7 +668,8 @@ def _set_fixings( if fixings is NoInput.blank: fixings_ = [] elif isinstance(fixings, Series): - fixings_ = self._get_fixings_from_series(fixings) + fixings_ = fixings.sort_index() + fixings_ = self._get_fixings_from_series(fixings_) elif isinstance(fixings, tuple): fixings_ = [fixings[0]] + self._get_fixings_from_series(fixings[1], 1) elif not isinstance(fixings, list): diff --git a/rateslib/periods.py b/rateslib/periods.py index b2836856..ccb25001 100644 --- a/rateslib/periods.py +++ b/rateslib/periods.py @@ -1465,9 +1465,13 @@ def _rfr_fixings_array( rate = self._isda_compounded_rate_with_spread(rates_dual, dcf_vals) notional_exposure = Series( [rate.gradient(f"fixing_{i}")[0] for i in range(len(dcf_dates.index) - 1)] - ) + ).astype(float) v = disc_curve[self.payment] - notional_exposure *= -self.notional * (self.dcf / dcf_of_r) * v / v_with_r + mask = ~fixed.to_numpy() # exclude fixings that are already fixed + + notional_exposure[mask] *= -self.notional * (self.dcf / dcf_of_r[mask]) * float(v) + notional_exposure[mask] /= v_with_r[mask].astype(float) + # notional_exposure[mask] *= (-self.notional * (self.dcf / dcf_of_r[mask]) * v / v_with_r[mask]) # notional_exposure[fixed.drop_index(drop=True)] = 0.0 notional_exposure[fixed.to_numpy()] = 0.0 extra_cols = { diff --git a/rateslib/splines.py b/rateslib/splines.py index 0217ac08..3343cc0c 100644 --- a/rateslib/splines.py +++ b/rateslib/splines.py @@ -414,10 +414,10 @@ def ppev_single(self, x): \\$(x) = \\sum_{i=1}^n c_i B_{(i,k,\\mathbf{t})}(x) """ - sum = 0 - for i, c_ in enumerate(self.c): - sum += c_ * bsplev_single(x, i, self.k, self.t) - return sum + _ = np.array([ + bsplev_single(x, i, self.k, self.t) for i in range(self.n) + ]) + return np.dot(_, self.c) def ppev(self, x): """ diff --git a/tests/test_fx.py b/tests/test_fx.py index 94a0548b..d0cef117 100644 --- a/tests/test_fx.py +++ b/tests/test_fx.py @@ -903,6 +903,38 @@ def test_fxforwards_cyclic_system_restructured(): assert abs(result - expected) < 1e-2 +def test_fxforwards_settlement_pairs(): + fxr1 = FXRates({"eurusd": 1.05}, settlement=dt(2022, 1, 3)) + fxr2 = FXRates({"usdcad": 1.1}, settlement=dt(2022, 1, 2)) + fxr3 = FXRates({"gbpusd": 1.2}, settlement=dt(2022, 1, 3)) + fxf = FXForwards( + fx_rates=[fxr1, fxr2, fxr3], # FXRates as list + fx_curves={ + "usdusd": Curve({dt(2022, 1, 1): 1.0, dt(2022, 2, 1): 0.999}), + "eureur": Curve({dt(2022, 1, 1): 1.0, dt(2022, 2, 1): 0.999}), + "cadcad": Curve({dt(2022, 1, 1): 1.0, dt(2022, 2, 1): 0.999}), + "usdeur": Curve({dt(2022, 1, 1): 1.0, dt(2022, 2, 1): 0.999}), + "cadeur": Curve({dt(2022, 1, 1): 1.0, dt(2022, 2, 1): 0.999}), + "gbpcad": Curve({dt(2022, 1, 1): 1.0, dt(2022, 2, 1): 0.999}), + "gbpgbp": Curve({dt(2022, 1, 1): 1.0, dt(2022, 2, 1): 0.999}), + }, + ) + assert fxf.pairs_settlement["eurusd"] == dt(2022, 1, 3) + assert fxf.pairs_settlement["usdcad"] == dt(2022, 1, 2) + assert fxf.pairs_settlement["gbpusd"] == dt(2022, 1, 3) + + fxf = FXForwards( + fx_rates=fxr1, # FXRates as list + fx_curves={ + "usdusd": Curve({dt(2022, 1, 1): 1.0, dt(2022, 2, 1): 0.999}), + "eureur": Curve({dt(2022, 1, 1): 1.0, dt(2022, 2, 1): 0.999}), + "usdeur": Curve({dt(2022, 1, 1): 1.0, dt(2022, 2, 1): 0.999}), + }, + ) + assert fxf.pairs_settlement["eurusd"] == dt(2022, 1, 3) + + + def test_fxforwards_positions_when_immediate_aligns_with_settlement(): fxr1 = FXRates({"eurusd": 1.05}, settlement=dt(2022, 1, 1)) fxr2 = FXRates({"usdcad": 1.1}, settlement=dt(2022, 1, 1)) diff --git a/tests/test_instruments_bonds.py b/tests/test_instruments_bonds.py index 144450f7..8131c5b9 100644 --- a/tests/test_instruments_bonds.py +++ b/tests/test_instruments_bonds.py @@ -747,10 +747,10 @@ def test_fixed_rate_bond_implied_repo_analogue_dirty(self, f_s, f_p): assert abs(result - 1.0) < 1e-8 @pytest.mark.parametrize("price, tol", [ - (112.0, 1e-6), - (104.0, 1e-5), - (96.0, 1e-3), - (91.0, 1e-2) + (112.0, 1e-10), + (104.0, 1e-10), + (96.0, 1e-9), + (91.0, 1e-7) ]) def test_oaspread(self, price, tol): gilt = FixedRateBond( @@ -772,6 +772,34 @@ def test_oaspread(self, price, tol): result = gilt.rate(curve_z, metric="clean_price") assert abs(result - price) < tol + @pytest.mark.parametrize("price, tol", [ + (85, 1e-8), + (75, 1e-6), + (65, 1e-4), + (55, 1e-3), + (45, 1e-1), + (35, 0.20), + ]) + def test_oaspread_low_price(self, price, tol): + gilt = FixedRateBond( + effective=dt(1998, 12, 7), + termination=dt(2015, 12, 7), + frequency="S", + calendar="ldn", + currency="gbp", + convention="ActActICMA", + ex_div=7, + fixed_rate=1.0, + notional=-100, + settle=0, + ) + curve = Curve({dt(1999, 11, 25): 1.0, dt(2015, 12, 7): 0.85}) + # result = gilt.npv(curve) = 113.22198344812742 + result = gilt.oaspread(curve, price=price) + curve_z = curve.shift(result, composite=False) + result = gilt.rate(curve_z, metric="clean_price") + assert abs(result - price) < tol + def test_cashflows_no_curve(self): gilt = FixedRateBond( effective=dt(2001, 1, 1), @@ -1009,6 +1037,34 @@ def test_fixed_rate_bond_fwd_rate(self): # def test_convexity(self): # assert False + def test_latest_fixing(self): + # this is German government inflation bond with fixings given for a specific settlement + # calculation + + ibnd = IndexFixedRateBond( + effective=dt(2021, 2, 11), + front_stub=dt(2022, 4, 15), + termination=dt(2033, 4, 15), + convention="ActActICMA", + calendar="tgt", + frequency="A", + index_lag=3, + index_base=124.17000 / 1.18851, # implying from 1st Jan 2024 on webpage + index_method="daily", + payment_lag=0, + currency="eur", + fixed_rate=0.1, + ex_div=1, + settle=1, + index_fixings=Series( + data=[124.17, 123.46], + index=[dt(2024, 1, 1), dt(2024, 2, 1)] + ), + ) + result = ibnd.ytm(price=100.32, settlement=dt(2024, 1, 5)) + expected = 0.065 + assert (result - expected) < 1e-2 + class TestBill: def test_bill_discount_rate(self): diff --git a/tests/test_legs.py b/tests/test_legs.py index 8fdbf798..be961612 100644 --- a/tests/test_legs.py +++ b/tests/test_legs.py @@ -219,6 +219,84 @@ def test_float_leg_rfr_fixings_table(self, method, param, fixings, curve): ).set_index("obs_dates") assert_frame_equal(result, expected, rtol=1e-5) + def test_rfr_with_fixings_fixings_table_issue(self): + from rateslib import IRS + instruments = [ + IRS(dt(2024, 1, 15), dt(2024, 3, 20), spec="eur_irs", curves="estr"), + IRS(dt(2024, 3, 20), dt(2024, 6, 19), spec="eur_irs", curves="estr"), + IRS(dt(2024, 6, 19), dt(2024, 9, 18), spec="eur_irs", curves="estr"), + ] + curve = Curve( + nodes={ + dt(2024, 1, 11): 1.0, + dt(2024, 3, 20): 1.0, + dt(2024, 6, 19): 1.0, + dt(2024, 9, 18): 1.0, + }, + calendar="tgt", + convention="act360", + id="estr", + ) + from rateslib import Solver + Solver( + curves=[curve], + instruments=instruments, + s=[ + 3.89800324, + 3.63414284, + 3.16864932, + ], + id="eur", + ) + fixings = Series( + data=[ + 3.904, + 3.904, + 3.904, + 3.905, + 3.902, + 3.904, + 3.906, + 3.882, + 3.9, + 3.9, + 3.899, + 3.899, + 3.901, + 3.901, + ], + index=[ + dt(2024, 1, 10), + dt(2024, 1, 9), + dt(2024, 1, 8), + dt(2024, 1, 5), + dt(2024, 1, 4), + dt(2024, 1, 3), + dt(2024, 1, 2), + dt(2023, 12, 29), + dt(2023, 12, 28), + dt(2023, 12, 27), + dt(2023, 12, 22), + dt(2023, 12, 21), + dt(2023, 12, 20), + dt(2023, 12, 19), + ], + ) + + swap = IRS( + dt(2023, 12, 20), + dt(2024, 1, 31), + spec="eur_irs", + curves="estr", + leg2_fixings=fixings, + notional=3e9, + fixed_rate=3.922, + ) + result = swap.leg2.fixings_table(curve) + assert result.loc[dt(2024, 1, 10), "notional"] == 0.0 + assert abs(result.loc[dt(2024, 1, 11), "notional"] - 3006829846) < 1.0 + assert abs(result.loc[dt(2023, 12, 20), "rates"] - 3.901) < 0.001 + def test_float_leg_set_float_spread(self, curve): float_leg = FloatLeg( effective=dt(2022, 1, 1),