Skip to content
All library documents

Reconstructing Bootstrapped QuantLib Curves with Matching Nodes and Interpolation

Article Quant Q&A · Author: opt

Summary

The document explains why a curve rebuilt from zero rates sampled at arbitrary tenors may differ from a QuantLib curve bootstrapped from deposits, futures, and swaps. Matching a few sampled rates does not preserve the values between those dates because the rebuilt curve interpolates according to its own nodes and interpolation rule.

To reproduce a piecewise curve, retrieve its underlying date-value pairs with the curve’s nodes method and rebuild using the corresponding curve type and interpolation. For a piecewise linear zero curve, the nodes contain zero rates and can be used to create a linear ZeroCurve. For a piecewise flat forward curve, nodes contain instantaneous forward rates; for a log-cubic discount curve, they contain discount factors. The document notes a practical limitation: the Python-exported discount curve described uses log-linear interpolation, not log-cubic, so reproducing the latter may require extending and recompiling the bindings. Its demonstration compares curves sampled across dates, but applies to the specific interpolation and wrapper availability discussed.

Key ideas

  • Reconstruction requires matching the original curve’s nodes as well as its interpolation method.
  • Arbitrarily sampled zero rates can match at selected dates while differing between them.
  • Piecewise linear zero rates can be reproduced from the original dates and rates using a linear zero curve.
  • Flat forward and log-cubic discount curves require node values in their respective rate or discount-factor forms.
  • The documented Python bindings do not expose the needed log-cubic discount curve directly.

Tags

Full text
# Quantlib-Python: use zero rates to get the originally bootstrapped curve


# Quantlib-Python: use zero rates to get the originally bootstrapped curve












Let's say I am trying to build a curve using deposits, future and swaps with one of the three Quantlib methods in Python as below:

```
crv = ql.PiecewiseLogCubicDiscount(2, ql.TARGET(), deposits + futures + swaps, ql.Actual365Fixed())
```

or

```
crv = ql.PiecewiseLinearZero(2, ql.TARGET(), deposits + futures + swaps, ql.Actual365Fixed())
```

or

```
crv = ql.PiecewiseFlatForward(2, ql.TARGET(), deposits + futures + swaps, ql.Actual365Fixed())
```

and that then I get the zero rates on a set of key tenors as below:

```
spotDate = crv.referenceDate()
dates = [ql.TARGET().advance(spotDate, t, ql.Days) for t in keytenors ]
rates = [ crv.zeroRate(t, ql.Continuous).rate() for t in keytenors ]
```

If I pass these zero rates to get the original crv object in the three cases above, how can I do it? I tried with:

```
zero_curve = ql.ZeroCurve(dates, rates,  ql.Actual365Fixed())
```

but the curve I get is not the same as the one I bootstrapped. In general the zero curve I obtain is much less smooth and seems linearly interpolating somehow. How can I get the PiecewiseLogCubicDiscount, PiecewiseLinearZero and PiecewiseFlatForward instead?

## Answer by Luigi Ballabio (score 7, accepted)

https://quant.stackexchange.com/a/44779

To retrieve the original curve, you need to use the same key tenors of the original curve and with the same interpolation. For instance, when you create the original curve as:

```
crv = ql.PiecewiseLinearZero(2, ql.TARGET(), deposits + futures + swaps, ql.Actual365Fixed())
```

the curve linearly interpolates zero rates between nodes given by the maturities of the passed deposits, futures and swaps. You can retrieve the set of underlying dates and the corresponding rates by calling `crv.nodes()`, which returns a sequence of (date, rate) pairs; for instance, if I call it on a curve defined as in this example, I get:

```
((Date(8,11,2001), 0.038716178576382605),
 (Date(15,11,2001), 0.038716178576382605),
 (Date(10,12,2001), 0.037654445569665344),
 (Date(8,2,2002), 0.03663450512870074),
 (Date(8,5,2002), 0.03704480712236303),
 (Date(8,8,2002), 0.037185800177110054),
 (Date(8,11,2002), 0.03725571728097072),
 (Date(10,11,2003), 0.03633800161641973),
 (Date(8,11,2004), 0.039086101826569714),
 (Date(8,11,2006), 0.04547303923680055),
 (Date(8,11,2011), 0.051542294488560084),
 (Date(8,11,2016), 0.055797299887186284))
```

(The evaluation date used in the example is November 6th, 2001).

Since the curve is a `PiecewiseLinearZero` instance, the rates returned above are zero rates; and if you use them to create an instance of `ZeroCurve` (which also interpolates linearly)...

```
dates, rates = zip(*crv.nodes())
crv2 = ql.ZeroCurve(dates, rates, ql.Actual365Fixed())
```

...you'll get the same curve as the original:

```
spot = crv.referenceDate()
sample_dates = [ spot + ql.Period(i, ql.Weeks) for i in range(15*52) ]
z1 = [ crv.zeroRate(d, ql.Actual365Fixed(), ql.Continuous).rate() for d in sample_dates ]
z2 = [ crv2.zeroRate(d, ql.Actual365Fixed(), ql.Continuous).rate() for d in sample_dates ]

fig = plt.figure(figsize=(12,6))
ax = fig.add_subplot(1,1,1)
ax.plot_date([d.to_date() for d in sample_dates], z1, '.')
ax.plot_date([d.to_date() for d in sample_dates], z2, '-')
```

The problem is that, if you sample the zero rates at different nodes, you'll get points on the curve; but interpolating between them, you'll get different values.

```
sample_nodes = [ spot + ql.Period(3*i, ql.Years) for i in range(6) ]
sample_rates = [ crv.zeroRate(d, ql.Actual365Fixed(), ql.Continuous).rate() for d in sample_nodes ]
crv3 = ql.ZeroCurve(sample_nodes, sample_rates, ql.Actual365Fixed())

z3 = [ crv3.zeroRate(d, ql.Actual365Fixed(), ql.Continuous).rate() for d in sample_dates ]

fig = plt.figure(figsize=(12,6))
ax = fig.add_subplot(1,1,1)
ax.plot_date([d.to_date() for d in sample_dates], z1, '.')
p, = ax.plot_date([d.to_date() for d in sample_dates], z3, '-')
ax.plot_date([d.to_date() for d in sample_nodes], sample_rates, 'o', markersize=8, color=p.get_color())
```

In short: you need to use the same nodes and interpolation. You can retrieve the former from the original curve as `curve.nodes()`, and you'll have to choose a class that provides the latter. For `PiecewiseLinearZero`, you'll have to use `ZeroCurve`; for `PiecewiseFlatForward`, the `nodes` method will return pairs of dates and instantaneous forward rates, which you can use to create an instance of `ForwardCurve`.

For `PiecewiseLogCubicDiscount`, the `nodes` method will return pairs of dates and discount factors, and you'd have to pass them to a corresponding interpolated discount curve; however, the one currently exported to Python (`DiscountCurve`) uses log-linear interpolation, and not log-cubic. If you want to use the latter, you'll have to modify `QuantLib-SWIG/SWIG/discountcurve.i` so that it also exports the desired curve and recompile the wrappers. The same goes if you want to use a different interpolation for zero or forward rates; the corresponding files to edit are `zerocurve.i` and `forwardcurve.i`.

Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.