Curves#

The rateslib.curves module allows the fundamental Curve, LineCurve, or IndexCurve class to be defined with parameters (for the purpose of the user guide an IndexCurve can be considered a Curve with minor enhancements). These curve objects are slightly different in what they represent and how they operate.

This module relies on the ultility modules splines and dual.

rateslib.curves.Curve(nodes, *[, ...])

Curve based on DF parametrisation at given node dates with interpolation.

rateslib.curves.LineCurve(*args, **kwargs)

Curve based on value parametrisation at given node dates with interpolation.

rateslib.curves.IndexCurve(*args[, ...])

A subclass of Curve with an index_base value for index calculations.

rateslib.curves.CompositeCurve(curves[, id])

A dynamic composition of a sequence of other curves.

rateslib.curves.ProxyCurve(cashflow, ...[, ...])

A subclass of Curve which returns dynamic DFs based on other curves related via FXForwards parity.

rateslib.curves.MultiCsaCurve(curves[, id, ...])

A dynamic composition of a sequence of other curves.

rateslib.curves.interpolate(x, x_1, y_1, ...)

Perform local interpolation between two data points.

rateslib.curves.index_left(list_input, ...)

Return the interval index of a value from an ordered input list on the left side.

Each fundamental curve type has rate(), plot(), shift(), roll() and translate() methods. IndexCurve can also calculate future index_value().

rateslib.curves.Curve.rate(effective, ...[, ...])

Calculate the rate on the Curve using DFs.

rateslib.curves.Curve.plot(tenor[, right, ...])

Plot given forward tenor rates from the curve.

rateslib.curves.Curve.shift(spread[, id, ...])

Create a new curve by vertically adjusting the curve by a set number of basis points.

rateslib.curves.Curve.roll(tenor)

Create a new curve with its shape translated in time but an identical initial node date.

rateslib.curves.Curve.translate(start[, t])

Create a new curve with an initial node date moved forward keeping all else constant.

rateslib.curves.LineCurve.rate(effective, *args)

Return the curve value for a given date.

rateslib.curves.LineCurve.plot(tenor[, ...])

Plot given forward tenor rates from the curve.

rateslib.curves.LineCurve.shift(spread[, ...])

Raise or lower the curve in parallel by a set number of basis points.

rateslib.curves.LineCurve.roll(tenor)

Create a new curve with its shape translated in time

rateslib.curves.LineCurve.translate(start[, t])

Create a new curve with an initial node date moved forward keeping all else constant.

rateslib.curves.IndexCurve.index_value(date)

Calculate the accrued value of the index from the index_base.

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 Curve these values are discount factors (DFs), and in the case of a LineCurve these are specific values, usually rates associated with that curve.

Curve#

A 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 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 Curve should always have value 1.0, and it will not be varied by a Solver. Curve s must be used with FXForwards since FX forwards calculation rely on the existence of DFs.

LineCurves#

A LineCurve is a more general object which can be used to represent other forms of datetime indexed values. The values maintain interpolation techniques where the most common are likely to be linear and splines. These are generally quite inefficient, and more difficult to parametrise, when dealing with RFR rates, but may be superior when dealing with legacy IBOR rates or inflation etc. The initial node on a LineCurve can take any value and it will be varied by a Solver.

Introduction#

To create a simple curve, with localised interpolation, minimal configuration is required.

In [1]: from rateslib import dt

In [2]: curve = Curve(
   ...:     nodes={
   ...:         dt(2022,1,1): 1.0,  # <- initial DF should always be 1.0
   ...:         dt(2023,1,1): 0.99,
   ...:         dt(2024,1,1): 0.979,
   ...:         dt(2025,1,1): 0.967,
   ...:         dt(2026,1,1): 0.956,
   ...:         dt(2027,1,1): 0.946,
   ...:     },
   ...:     interpolation="log_linear",
   ...: )
   ...: 

We can also use a similar configuration for a generalised curve constructed from connecting lines between values.

In [3]: linecurve = LineCurve(
   ...:     nodes={
   ...:         dt(2022,1,1): 0.975,  # <- initial value is general
   ...:         dt(2023,1,1): 1.10,
   ...:         dt(2024,1,1): 1.22,
   ...:         dt(2025,1,1): 1.14,
   ...:         dt(2026,1,1): 1.03,
   ...:         dt(2027,1,1): 1.03,
   ...:     },
   ...:     interpolation="linear",
   ...: )
   ...: 

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 be exactly 1.0 on a Curve.

Get Item#

Curves have a get item method so that DFs from a Curve or values from a LineCurve can easily be extracted under the curve’s specified interpolation scheme.

Note

Curve DFs (and LineCurve values), before the curve’s initial node date return zero, in order to value historical cashflows at zero.

Warning

Curve DFs, and LineCurve values, after the curve’s final node date will return a value that is an extrapolation. This may not be a sensible or well constrained value depending upon the interpolation.

In [4]: curve[dt(2022, 9, 26)]
Out[4]: 0.9926477364206718

In [5]: curve[dt(1999, 12, 31)]  # <- before the curve initial node date
Out[5]: 0

In [6]: curve[dt(2032, 1, 1)]  # <- extrapolated after the curve final node date
Out[6]: 0.8975214680350941
In [7]: linecurve[dt(2022, 9, 26)]
Out[7]: 1.0667808219178083

In [8]: linecurve[dt(1999, 12, 31)]  # <- before the curve initial node date
Out[8]: 0

In [9]: linecurve[dt(2032, 1, 1)]  # <- extrapolated after the curve final node date
Out[9]: 1.03

Visualization#

Visualization methods are also available via Curve.plot() and LineCurve.plot(). This allows the easy inspection of curves directly. Below we demonstrate a plot highlighting the differences between our parametrised Curve and LineCurve.

In [10]: curve.plot(
   ....:     "1D",
   ....:     comparators=[linecurve],
   ....:     labels=["Curve", "LineCurve"]
   ....: )
   ....: 
Out[10]: 
(<Figure size 640x480 with 1 Axes>,
 <Axes: >,
 [<matplotlib.lines.Line2D at 0x7f0aa0c3d850>,
  <matplotlib.lines.Line2D at 0x7f0aa08d7f10>])

(Source code, png, hires.png, pdf)

_images/c_curves-1.png

Interpolation#

The available basic local interpolation options are:

  • “linear”: this is most suitable, and the default, for LineCurve. Linear interpolation for DF based curves usually produces spurious underlying curves.

  • “log_linear”: this is most suitable, and the default, for Curve. It produces overnight rates that are constant between nodes. This is not usually suitable for LineCurve.

  • “linear_zero_rate”: this is a legacy option for linearly interpolating continuously compounded zero rates, and is only suitable for Curve, but it is not recommended and tends also to produce spurious underlying curves.

  • “flat_forward”: this is only suitable for LineCurve, and it maintains the previous value between nodes. It will produce a stepped curve similar to a Curve with “log_linear” interpolation.

  • “flat_backward”: same as above but in reverse.

In [11]: linecurve.interpolation = "flat_forward"

In [12]: curve.plot("1D", comparators=[linecurve], labels=["Curve", "LineCurve"])
Out[12]: 
(<Figure size 640x480 with 1 Axes>,
 <Axes: >,
 [<matplotlib.lines.Line2D at 0x7f0aa178bc90>,
  <matplotlib.lines.Line2D at 0x7f0aa117c510>])

(Source code, png, hires.png, pdf)

_images/c_curves-2.png

interpolation can also be specified as a user defined function. It must have the argument signature (date, nodes) where nodes are passed internally as those copied from the curve.

In [13]: from rateslib.curves import index_left

In [14]: def flat_backward(x, nodes):
   ....:     """Project the rightmost node value as opposed to leftmost."""
   ....:     node_dates = [key for key in nodes.keys()]
   ....:     if x < node_dates[0]:
   ....:         return 0  # then date is in the past and DF is zero
   ....:     l_index = index_left(node_dates, len(node_dates), x)
   ....:     return nodes[node_dates[l_index + 1]]
   ....: 

In [15]: linecurve.interpolation = flat_backward

In [16]: curve.plot("1D", comparators=[linecurve], labels=["Curve", "LineCurve"])
Out[16]: 
(<Figure size 640x480 with 1 Axes>,
 <Axes: >,
 [<matplotlib.lines.Line2D at 0x7f0aa01226d0>,
  <matplotlib.lines.Line2D at 0x7f0aa00f3f50>])

(Source code, png, hires.png, pdf)

_images/c_curves-3.png

Spline Interpolation#

There is also an option to interpolate with a cubic polynomial spline.

If applying spline interpolation to a Curve then it is applied logarithmically resulting in a log-cubic spline over DFs.

If it is applied to a LineCurve then it results in a standard cubic spline over values.

In order to instruct this mode of interpolation a knot sequence is required as the t argument. This is a list of datetimes and follows the appropriate mathematical convention for such sequences (see pp splines).

Mixed Interpolation#

Prior to the initial knot in the sequence the local interpolation method is used. This allows curves to be constructed with a mixed interpolation in two parts of the curve. This is common practice for interest rate curves usually with a log-linear short end and a log-cubic spline longer end.

In [17]: mixed_curve = Curve(
   ....:     nodes={
   ....:         dt(2022,1,1): 1.0,
   ....:         dt(2023,1,1): 0.99,
   ....:         dt(2024,1,1): 0.979,
   ....:         dt(2025,1,1): 0.967,
   ....:         dt(2026,1,1): 0.956,
   ....:         dt(2027,1,1): 0.946,
   ....:     },
   ....:     interpolation="log_linear",
   ....:     t = [dt(2024,1,1), dt(2024,1,1), dt(2024,1,1), dt(2024,1,1),
   ....:          dt(2025,1,1),
   ....:          dt(2026,1,1),
   ....:          dt(2027,1,1), dt(2027,1,1), dt(2027,1,1), dt(2027,1,1)]
   ....: )
   ....: 

In [18]: curve.plot("1D", comparators=[mixed_curve], labels=["log-linear", "log-cubic-mix"])
Out[18]: 
(<Figure size 640x480 with 1 Axes>,
 <Axes: >,
 [<matplotlib.lines.Line2D at 0x7f0a9ffbcfd0>,
  <matplotlib.lines.Line2D at 0x7f0a9ff6ded0>])

(Source code, png, hires.png, pdf)

_images/c_curves-4.png

IBOR or RFR#

The different Instruments in rateslib may require different interest rate index types, be it IBOR or RFR based. These are fundamentally different and require care dependent on which curve type: Curve or LineCurve is used. This is also similar to fixing input for FloatPeriod (see here).

Curve Type

RFR Based

IBOR Based

Curve

DFs are value date based. For an RFR rate applicable between a start and end date, the start and end date DFs will reflect this rate, regardless of the publication timeframe of the rate.

DFs are value date based. For an IBOR rate applicable between a start and end date, the start and end date DFs will reflect this rate, regardless of the publication timeframe of the rate.

LineCurve

Rates are labelled by reference value date, not publication date.

Rates are labelled by publication date, not reference value date.

Since DF based curves behave similarly for each index type we will give an example of constructing an IRS under the different methods.

For an RFR curve the nodes values are by reference date. The 3.0% value which is applicable between the reference date of 2nd Jan ‘22 and end date 3rd Jan ‘22, is indexed according to the 2nd Jan ‘22.

In [19]: rfr_curve = LineCurve(
   ....:     nodes={
   ....:         dt(2022, 1, 1): 2.0,
   ....:         dt(2022, 1, 2): 3.0,
   ....:         dt(2022, 1, 3): 4.0
   ....:     }
   ....: )
   ....: 

In [20]: irs = IRS(
   ....:     dt(2022, 1, 2),
   ....:     "1d",
   ....:     "A",
   ....:     leg2_fixing_method="rfr_payment_delay"
   ....: )
   ....: 

In [21]: irs.rate(rfr_curve)
Out[21]: 3.0000000000036664

For an IBOR curve the nodes values are by publication date. The curve below has a lag of 2 business days. and the publication on 1st Jan ‘22 is applicable to the reference value date of 3rd Jan.

In [22]: ibor_curve = LineCurve(
   ....:     nodes={
   ....:         dt(2022, 1, 1): 2.5,
   ....:         dt(2022, 1, 2): 3.5,
   ....:         dt(2022, 1, 3): 4.5
   ....:     }
   ....: )
   ....: 

In [23]: irs = IRS(
   ....:     dt(2022, 1, 3),
   ....:     "3m",
   ....:     "A",
   ....:     leg2_fixing_method="ibor",
   ....:     leg2_method_param=2
   ....: )
   ....: 

In [24]: irs.rate(ibor_curve)
Out[24]: 2.5