From d79bae6cf5c432c952b400111ea26b0b4e672ddf Mon Sep 17 00:00:00 2001 From: JHM Darbyshire <24256554+attack68@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:31:49 +0100 Subject: [PATCH] DEPR: IndexCurve depr updates (#561) Co-authored-by: JHM Darbyshire (win11) --- docs/source/c_curves.rst | 47 ++++++++++--------- docs/source/g_curves.rst | 5 +- docs/source/i_api.rst | 7 +-- docs/source/i_get_started.rst | 8 ++-- docs/source/i_guide.rst | 19 ++++---- docs/source/i_whatsnew.rst | 2 +- docs/source/z_amortization.rst | 2 +- python/rateslib/curves/curves.py | 2 +- python/rateslib/instruments/generics.py | 4 +- .../rateslib/instruments/rates_derivatives.py | 2 +- python/rateslib/legs.py | 16 +++---- python/rateslib/periods.py | 38 +++++++-------- python/tests/test_legs.py | 30 ++++++++++++ python/tests/test_periods.py | 4 +- 14 files changed, 109 insertions(+), 77 deletions(-) diff --git a/docs/source/c_curves.rst b/docs/source/c_curves.rst index ab049ed3..5ec07447 100644 --- a/docs/source/c_curves.rst +++ b/docs/source/c_curves.rst @@ -13,17 +13,16 @@ Curves *********** -The ``rateslib.curves`` module allows the fundamental :class:`~rateslib.curves.Curve`, -:class:`~rateslib.curves.LineCurve`, or :class:`~rateslib.curves.IndexCurve` class -to be defined with parameters (for the purpose of the user guide an *IndexCurve* -can be considered a *Curve* with minor enhancements). +The ``rateslib.curves`` module allows the fundamental :class:`~rateslib.curves.Curve` or +:class:`~rateslib.curves.LineCurve` class +to be defined with parameters. These curve objects are slightly different in what they represent and how they operate. This module relies on the ultility modules :ref:`splines` and :ref:`dual`. -.. inheritance-diagram:: rateslib.curves.Curve rateslib.curves.LineCurve rateslib.curves.IndexCurve rateslib.curves.CompositeCurve rateslib.curves.MultiCsaCurve rateslib.curves.ProxyCurve +.. inheritance-diagram:: rateslib.curves.Curve rateslib.curves.LineCurve rateslib.curves.CompositeCurve rateslib.curves.MultiCsaCurve rateslib.curves.ProxyCurve :private-bases: :top-classes: rateslib.curves.Curve :parts: 1 @@ -31,7 +30,6 @@ and :ref:`dual`. .. autosummary:: rateslib.curves.Curve rateslib.curves.LineCurve - rateslib.curves.IndexCurve rateslib.curves.CompositeCurve rateslib.curves.ProxyCurve rateslib.curves.MultiCsaCurve @@ -39,8 +37,8 @@ and :ref:`dual`. rateslib.curves.index_left Each fundamental curve type has ``rate()``, ``plot()``, ``shift()``, ``roll()`` and -``translate()`` methods. :class:`~rateslib.curves.IndexCurve` can also calculate -future ``index_value()``. +``translate()`` methods. A :class:`~rateslib.curves.Curve` with included ``index_base`` and +``index_lag`` can also calculate future ``index_value()`` and ``plot_index()``. .. autosummary:: rateslib.curves.Curve.rate @@ -53,23 +51,23 @@ future ``index_value()``. rateslib.curves.LineCurve.shift rateslib.curves.LineCurve.roll rateslib.curves.LineCurve.translate - rateslib.curves.IndexCurve.index_value + rateslib.curves.Curve.index_value + rateslib.curves.Curve.plot_index The main parameter that must be supplied to either type of curve is its ``nodes``. This provides the curve with its degrees of freedom and represents a dict indexed by datetimes, each with a given value. In the case of a :class:`~rateslib.curves.Curve` -these -values are discount factors (DFs), and in the case of -a :class:`~rateslib.curves.LineCurve` +these values are discount factors (DFs) or, for credit purposes, survival probabilities, +and in the case of a :class:`~rateslib.curves.LineCurve` these are specific values, usually rates associated with that curve. Curve ******* -A :class:`~rateslib.curves.Curve` can only be used for **interest rates**. -It is a more specialised -object because of the way it is **defined by discount factors (DFs)**. These DFs -maintain an inherent interpolation technique, which is often log-linear or log-cubic +A :class:`~rateslib.curves.Curve` can only be used for **interest rates** (or credit +**hazard rates**). It is a more specialised +object because of the way it is **defined by discount factors (DFs)** (or survival probabilities). +These DFs maintain an inherent interpolation technique, which is often log-linear or log-cubic spline. These are generally the most efficient type of curve, and most easily parametrised, when working with compounded RFR rates. The initial node on a :class:`~rateslib.curves.Curve` should always have value 1.0, @@ -103,7 +101,7 @@ required. from rateslib import dt curve = Curve( nodes={ - dt(2022,1,1): 1.0, # <- initial DF should always be 1.0 + dt(2022,1,1): 1.0, # <- initial DF (/survival probability) should always be 1.0 dt(2023,1,1): 0.99, dt(2024,1,1): 0.979, dt(2025,1,1): 0.967, @@ -135,13 +133,18 @@ Initial Node Date ----------------- The initial node date for either curve type is important because it is implied -to be the date of the -construction of the curve (i.e. today's date). Any net present -values (NPVs) may assume other features -from this initial node, e.g. the regular settlement date of securities or the value of -cashflows on derivatives. This is the reason the initial discount factor should also +to be the date of the construction of the curve (i.e. today's date). +When a :class:`~rateslib.curves.Curve` acts as a discount curve any net present +values (NPVs) might assume other features +from this initial node, e.g. the regular settlement date of securities. +This is the also the reason the initial discount factor should also be exactly 1.0 on a :class:`~rateslib.curves.Curve`. +The only exception to this is when building a *Curve* used to forecast index vales, such +as inflation forecasts, it may be practical to start the curve using the most recent +inflation print which is usually assigned to the start of the month, +thus this may be before *today*. + Get Item -------- diff --git a/docs/source/g_curves.rst b/docs/source/g_curves.rst index 9951a2a5..d190bb00 100644 --- a/docs/source/g_curves.rst +++ b/docs/source/g_curves.rst @@ -13,17 +13,16 @@ Constructing Curves ******************** -*Rateslib* has six different interest rate *Curve* classes. Three of these are fundamental base +*Rateslib* has five different interest rate *Curve* classes. Two of these are fundamental base *Curves* of different types and for different purposes. Three are objects which are constructed via references to other curves to allow certain combinations. *Rateslib* also has one type of *Smile* for *FX volatility*. -The three fundamental curve classes are: +The two fundamental curve classes are: .. autosummary:: rateslib.curves.Curve rateslib.curves.LineCurve - rateslib.curves.IndexCurve The remaining, more complex, combination classes are: diff --git a/docs/source/i_api.rst b/docs/source/i_api.rst index 1b305d4f..7af6689a 100644 --- a/docs/source/i_api.rst +++ b/docs/source/i_api.rst @@ -45,6 +45,10 @@ Defaults :skip: Enum :skip: read_csv :skip: get_named_calendar + :skip: Cal + :skip: NamedCal + :skip: Series + :skip: UnionCal Calendars ========== @@ -59,8 +63,6 @@ Scheduling .. automodapi:: rateslib.scheduling :no-heading: :no-inheritance-diagram: - :skip: Any - :skip: CustomBusinessDay :skip: DataFrame :skip: Iterator :skip: datetime @@ -174,7 +176,6 @@ FX Volatility :skip: NoInput :skip: newton_1dim :skip: uuid4 - :skip: Union :skip: datetime :skip: dt :skip: timedelta diff --git a/docs/source/i_get_started.rst b/docs/source/i_get_started.rst index e0d49bb6..5d463777 100644 --- a/docs/source/i_get_started.rst +++ b/docs/source/i_get_started.rst @@ -171,10 +171,10 @@ Can ``Curves`` be constructed and plotted in *rateslib*? =========================================================== **Of course**. Building curves is a necessity for pricing fixed income instruments. -*Rateslib* has three primitive curve structures; :class:`~rateslib.curves.Curve` (which -is **discount factor based**), :class:`~rateslib.curves.LineCurve` (which is **purely value -based**), and :class:`~rateslib.curves.IndexCurve` (which is based on a *Curve* but also -calculates index values which is useful for inflation, for example). All *Curve* types offer +*Rateslib* has two primitive curve structures; :class:`~rateslib.curves.Curve`, which +is **discount factor, or survival probability based** and which can also calculate index values +for use with inflation products, for example, and :class:`~rateslib.curves.LineCurve` +(which is **purely value based**). All *Curve* types offer various interpolation methods, such as log-linear or log-cubic spline and can even splice certain interpolation types together. diff --git a/docs/source/i_guide.rst b/docs/source/i_guide.rst index 1c7625ad..bfe6b778 100644 --- a/docs/source/i_guide.rst +++ b/docs/source/i_guide.rst @@ -34,7 +34,7 @@ Let's start with some fundamental *Curve* and *Instrument* constructors. Trivial derivatives examples ---------------------------- -*Rateslib* has three fundamental :ref:`Curve types`. All can be constructed +*Rateslib* has two fundamental :ref:`Curve types`. Both can be constructed independently by providing basic inputs. .. tabs:: @@ -77,21 +77,23 @@ independently by providing basic inputs. id="us_ibor_3m", ) - .. tab:: IndexCurve + .. tab:: *(IndexCurve)* - An :class:`~rateslib.curves.IndexCurve` extends a *Curve* class by allowing - an ``index_base`` argument and the calculation of index values. It is DF based. - It is required for the pricing of index-linked *Instruments*. + An *Index Curve* is required by certain products, e.g. + inflation linked bonds (:class:`~rateslib.instruments.IndexFixedRateBond`) or + zero coupon inflation swaps (:class:`~rateslib.instruments.ZCIS`). Adding ``index_base`` + and ``index_lag`` argument inputs extends a DF based *Curve* to work in these cases. .. ipython:: python - usd_cpi = IndexCurve( + usd_cpi = Curve( nodes={ dt(2022, 1, 1): 1.00, dt(2022, 7, 1): 0.97, dt(2023, 1, 1): 0.955, }, index_base=308.95, + index_lag=3, id="us_cpi", ) @@ -532,9 +534,8 @@ Calibrating curves with a solver ================================= The guide for :ref:`Constructing Curves` introduces the main -curve classes, -:class:`~rateslib.curves.Curve`, :class:`~rateslib.curves.LineCurve`, and -:class:`~rateslib.curves.IndexCurve`. It also touches on some of the more +curve classes, :class:`~rateslib.curves.Curve` and :class:`~rateslib.curves.LineCurve`. +It also touches on some of the more advanced curves :class:`~rateslib.curves.CompositeCurve`, :class:`~rateslib.curves.ProxyCurve`, and :class:`~rateslib.curves.MultiCsaCurve`. diff --git a/docs/source/i_whatsnew.rst b/docs/source/i_whatsnew.rst index 80438ff6..31bf4f41 100644 --- a/docs/source/i_whatsnew.rst +++ b/docs/source/i_whatsnew.rst @@ -53,7 +53,7 @@ email contact, see `rateslib `_. * - Dependencies - Drop support for Python 3.9, only versions 3.10 - 3.13 now supported. * - Refactor - :class:`~rateslib.curves.CompositeCurve` no longer requires all curves to have the same ``index_base`` + - :class:`~rateslib.curves.CompositeCurve` no longer requires all curves to have the same ``index_base`` or ``index_lag``. Those values will be sampled from the first provided composited *Curve*. * - Refactor - :red:`Minor Breaking Change!` :meth:`~rateslib.calendars.get_calendar` has dropped the diff --git a/docs/source/z_amortization.rst b/docs/source/z_amortization.rst index 871e50b6..abb881eb 100644 --- a/docs/source/z_amortization.rst +++ b/docs/source/z_amortization.rst @@ -120,7 +120,7 @@ will also be indexed by the index. .. ipython:: python - icurve = IndexCurve({dt(2000, 1, 1): 1.0, dt(2010, 1, 1): 0.75}, index_base=100.0) + icurve = Curve({dt(2000, 1, 1): 1.0, dt(2010, 1, 1): 0.75}, index_base=100.0) il = IndexFixedLeg( effective=dt(2000, 1, 1), termination="1y", diff --git a/python/rateslib/curves/curves.py b/python/rateslib/curves/curves.py index d754cf19..5cc884f3 100644 --- a/python/rateslib/curves/curves.py +++ b/python/rateslib/curves/curves.py @@ -2037,7 +2037,7 @@ def __init__( # type: ignore[no-untyped-def] ) -> None: warnings.warn( "`IndexCurve` is deprecated: use a `Curve` instead and the same arguments.", - DeprecationWarning + DeprecationWarning, ) super().__init__(*args, **{"interpolation": "linear_index", **kwargs}) if isinstance(self.index_base, NoInput): diff --git a/python/rateslib/instruments/generics.py b/python/rateslib/instruments/generics.py index 0f6b36ec..fb663b2d 100644 --- a/python/rateslib/instruments/generics.py +++ b/python/rateslib/instruments/generics.py @@ -7,7 +7,7 @@ from rateslib import defaults from rateslib.calendars import dcf -from rateslib.curves import Curve, IndexCurve +from rateslib.curves import Curve from rateslib.default import NoInput from rateslib.dual import DualTypes, dual_log from rateslib.fx import FXForwards, FXRates @@ -123,8 +123,6 @@ def rate( _ = (dual_log(curves[0][self.effective]) / -dcf_) * 100 return _ elif metric == "index_value": - if not isinstance(curves[0], IndexCurve): - raise TypeError("`curve` used with `metric`='index_value' must be type IndexCurve.") _ = curves[0].index_value(self.effective) return _ raise ValueError("`metric`must be in {'curve_value', 'cc_zero_rate', 'index_value'}.") diff --git a/python/rateslib/instruments/rates_derivatives.py b/python/rateslib/instruments/rates_derivatives.py index 38f15ef5..273a1a77 100644 --- a/python/rateslib/instruments/rates_derivatives.py +++ b/python/rateslib/instruments/rates_derivatives.py @@ -1876,7 +1876,7 @@ def rate( base, self.leg1.currency, ) - if self.leg2_index_base is NoInput.blank: + if isinstance(self.leg2_index_base, NoInput): # must forecast for the leg forecast_value = curves[2].index_value( self.leg2.schedule.effective, diff --git a/python/rateslib/legs.py b/python/rateslib/legs.py index 9f29cd70..bee1986c 100644 --- a/python/rateslib/legs.py +++ b/python/rateslib/legs.py @@ -33,7 +33,7 @@ from rateslib import defaults from rateslib.calendars import add_tenor -from rateslib.curves import Curve, IndexCurve, index_left +from rateslib.curves import Curve, index_left from rateslib.default import NoInput, _drb from rateslib.dual import Dual, Dual2, DualTypes, gradient, set_order from rateslib.fx import FXForwards, FXRates @@ -1587,8 +1587,8 @@ def cashflows( defaults.headers["type"]: type(self).__name__, defaults.headers["stub_type"]: None, defaults.headers["currency"]: self.currency.upper(), - defaults.headers["a_acc_start"]: self.schedule.effective, - defaults.headers["a_acc_end"]: self.schedule.termination, + defaults.headers["a_acc_start"]: self.schedule.aschedule[0], + defaults.headers["a_acc_end"]: self.schedule.aschedule[-1], defaults.headers["payment"]: self.schedule.pschedule[-1], defaults.headers["convention"]: self.convention, defaults.headers["dcf"]: self.dcf, @@ -1709,7 +1709,7 @@ class ZeroIndexLeg(BaseLeg, _IndexLegMixin): -------- .. ipython:: python - index_curve = IndexCurve({dt(2022, 1, 1): 1.0, dt(2027, 1, 1): 0.95}, index_base=100.0) + index_curve = Curve({dt(2022, 1, 1): 1.0, dt(2027, 1, 1): 0.95}, index_base=100.0) zil = ZeroIndexLeg( effective=dt(2022, 1, 15), termination="3Y", @@ -1767,9 +1767,9 @@ def _set_periods(self): ), ] - def cashflow(self, curve: IndexCurve | None = None): - """Aggregate the cashflows on the *IndexFixedPeriod* and *Cashflow* period using an - *IndexCurve*.""" + def cashflow(self, curve: Curve | None = None): + """Aggregate the cashflows on the *IndexFixedPeriod* and *Cashflow* period using a + *Curve*.""" _ = self.periods[0].cashflow(curve) + self.periods[1].cashflow return _ @@ -2159,7 +2159,7 @@ class IndexFixedLeg(_IndexLegMixin, _FixedLegMixin, BaseLeg): .. ipython:: python curve = Curve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.98}) - index_curve = IndexCurve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.99}, index_base=100.0) + index_curve = Curve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.99}, index_base=100.0) index_leg_exch = IndexFixedLeg( dt(2022, 1, 1), "9M", "Q", notional=1000000, diff --git a/python/rateslib/periods.py b/python/rateslib/periods.py index 13dcb79f..8ab29dd8 100644 --- a/python/rateslib/periods.py +++ b/python/rateslib/periods.py @@ -31,7 +31,7 @@ from rateslib import defaults from rateslib.calendars import CalInput, CalTypes, _get_eom, add_tenor, dcf, get_calendar -from rateslib.curves import CompositeCurve, Curve, IndexCurve, LineCurve, average_rate, index_left +from rateslib.curves import Curve, LineCurve, average_rate, index_left from rateslib.default import NoInput, _drb from rateslib.dual import ( Dual, @@ -1061,7 +1061,7 @@ def rate(self, curve: Curve | dict[str, Curve] | NoInput = NoInput(0)) -> DualTy Parameters ---------- - curve : Curve, LineCurve, IndexCurve, dict of curves, optional + curve : Curve, LineCurve, dict of curves, optional The forecasting curve object. If ``fixings`` are available on the Period may be able to return a value without, otherwise will raise. @@ -1443,7 +1443,7 @@ def fixings_table( Parameters ---------- - curve : Curve, LineCurve, IndexCurve dict of such + curve : Curve, LineCurve, dict of such The forecast needed to calculate rates which affect compounding and dependent notional exposure. approximate : bool, optional @@ -2712,7 +2712,7 @@ class IndexMixin(metaclass=ABCMeta): payment: datetime = datetime(1990, 1, 1) currency: str = "" - def cashflow(self, curve: IndexCurve | NoInput = NoInput(0)) -> DualTypes | None: + def cashflow(self, curve: Curve | NoInput = NoInput(0)) -> DualTypes | None: """ float, Dual or Dual2 : The calculated value from rate, dcf and notional, adjusted for the index. @@ -2730,7 +2730,7 @@ def cashflow(self, curve: IndexCurve | NoInput = NoInput(0)) -> DualTypes | None _ = self.real_cashflow * (index_ratio + _) return _ - def index_ratio(self, curve: IndexCurve | NoInput = NoInput(0)) -> tuple: + def index_ratio(self, curve: Curve | NoInput = NoInput(0)) -> tuple: """ Calculate the index ratio for the end date of the *IndexPeriod*. @@ -2740,8 +2740,8 @@ def index_ratio(self, curve: IndexCurve | NoInput = NoInput(0)) -> tuple: Parameters ---------- - curve : IndexCurve - The index curve from which index values are forecast. + curve : Curve + The curve from which index values are forecast. Returns ------- @@ -2769,17 +2769,17 @@ def index_ratio(self, curve: IndexCurve | NoInput = NoInput(0)) -> tuple: @staticmethod def _index_value_from_curve( i_date: datetime, - i_curve: IndexCurve | NoInput, + i_curve: Curve | NoInput, i_lag: int, i_method: str, ) -> DualTypes | None: - if i_curve is NoInput.blank: + if isinstance(i_curve, NoInput): return None - elif (not isinstance(i_curve, IndexCurve) and not isinstance(i_curve, CompositeCurve)) or ( - isinstance(i_curve, CompositeCurve) and not isinstance(i_curve.curves[0], IndexCurve) - ): - raise TypeError("`index_value` must be forecast from an `IndexCurve`.") elif i_lag != i_curve.index_lag: + warnings.warn( + f"The `index_lag` of the Period ({i_lag}) does not match " + f"the `index_lag` of the Curve ({i_curve.index_lag}): returning None." + ) return None # TODO decide if RolledCurve to correct index lag be attempted else: return i_curve.index_value(i_date, i_method) @@ -2788,7 +2788,7 @@ def _index_value_from_curve( def _index_value( i_fixings: float | Series | NoInput, i_date: datetime, - i_curve: IndexCurve | NoInput, + i_curve: Curve | NoInput, i_lag: int, i_method: str, ) -> DualTypes | NoInput: @@ -2800,7 +2800,7 @@ def _index_value( Parameters ---------- - curve : IndexCurve + curve : Curve Returns ------- @@ -2840,7 +2840,7 @@ def _index_value( def npv( self, - curve: IndexCurve | NoInput = NoInput(0), + curve: Curve | NoInput = NoInput(0), disc_curve: Curve | NoInput = NoInput(0), fx: float | FXRates | FXForwards | NoInput = NoInput(0), base: str | NoInput = NoInput(0), @@ -2933,7 +2933,7 @@ class IndexFixedPeriod(IndexMixin, FixedPeriod): # type: ignore[misc] index_base=100.25, ) ifp.cashflows( - curve=IndexCurve({dt(2022, 1, 1):1.0, dt(2022, 12, 31): 0.99}, index_base=100.0, index_lag=2), + curve=Curve({dt(2022, 1, 1):1.0, dt(2022, 12, 31): 0.99}, index_base=100.0, index_lag=2), disc_curve=Curve({dt(2022, 1, 1):1.0, dt(2022, 12, 31): 0.98}) ) """ # noqa: E501 @@ -2992,7 +2992,7 @@ def real_cashflow(self): def cashflows( self, - curve: IndexCurve | NoInput = NoInput(0), + curve: Curve | NoInput = NoInput(0), disc_curve: Curve | NoInput = NoInput(0), fx: float | FXRates | FXForwards | NoInput = NoInput(0), base: str | NoInput = NoInput(0), @@ -3107,7 +3107,7 @@ class IndexCashflow(IndexMixin, Cashflow): # type: ignore[misc] index_base=100.25 ) icf.cashflows( - curve=IndexCurve({dt(2022, 1, 1): 1.0, dt(2022, 12, 31): 0.99}, index_base=100.0), + curve=Curve({dt(2022, 1, 1): 1.0, dt(2022, 12, 31): 0.99}, index_base=100.0), disc_curve=Curve({dt(2022, 1, 1): 1.0, dt(2022, 12, 31): 0.98}), ) """ diff --git a/python/tests/test_legs.py b/python/tests/test_legs.py index a188105f..00639527 100644 --- a/python/tests/test_legs.py +++ b/python/tests/test_legs.py @@ -867,6 +867,36 @@ def test_zero_fixed_leg_cashflows(self, freq, cash, rate, curve) -> None: rtol=1e-3, ) + def test_zero_fixed_leg_cashflows_cal(self, curve) -> None: + # assert stated cashflows accrual dates are adjusted according to calendar + # GH561/562 + zfl = ZeroFixedLeg( + effective=dt(2024, 12, 15), + termination="5y", + payment_lag=0, + notional=-1e8, + calendar="tgt", + modifier="mf", + convention="ActAct", + frequency="A", + fixed_rate=2.0, + ) + result = zfl.cashflows(curve) + expected = DataFrame( + { + "Type": ["ZeroFixedLeg"], + "Acc Start": [dt(2024, 12, 16)], + "Acc End": [dt(2029, 12, 17)], + "DCF": [5.0], + "Rate": [2.0], + }, + ) + assert_frame_equal( + result[["Type", "Acc Start", "Acc End", "DCF", "Rate"]], + expected, + rtol=1e-3, + ) + def test_zero_fixed_leg_npv(self, curve) -> None: zfl = ZeroFixedLeg( effective=dt(2022, 1, 1), diff --git a/python/tests/test_periods.py b/python/tests/test_periods.py index d93ab5b4..36906196 100644 --- a/python/tests/test_periods.py +++ b/python/tests/test_periods.py @@ -2668,7 +2668,7 @@ def test_bad_curve(self) -> None: index_base=100.0, ) curve = Curve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.99}) - with pytest.raises(TypeError, match="`index_value` must be forecast from"): + with pytest.raises(ValueError, match="Curve must be initialised with an `index_base`"): i_period.index_ratio(curve) # TEST REDUNDANT: function was changed to fallback to forecast from curve @@ -2739,7 +2739,7 @@ def test_composite_curve_raises(self) -> None: nodes={dt(2022, 1, 1): 1.0, dt(2022, 4, 3): 0.995}, ) composite_curve = CompositeCurve([curve]) - with pytest.raises(TypeError, match="`index_value` must be forecast from"): + with pytest.raises(ValueError, match="Curve must be initialised with an `index_base`"): _, result, _ = index_period.index_ratio(composite_curve)