import QuantLib as ql
The common interface of interest-rate term structures
= ql.Date(2, ql.May, 2019)
today = today ql.Settings.instance().evaluationDate
QuantLib defines different kinds of interest-rate term structures. Depending on your choice of model or the data you have available, they can model discount factors, instantaneous forward rates or zero rates. Their implementation can be, for instance, a simple one returning a constant value for the forward rate…
= ql.FlatForward(today, 0.01, ql.Actual365Fixed()) curve_1
…or one interpolating between a number of precalculated discount factors for a set of given dates.
= [today + ql.Period(i, ql.Years) for i in range(11)]
dates = [
discounts 1.0,
0.99,
0.98,
0.96,
0.95,
0.93,
0.92,
0.90,
0.88,
0.87,
0.86,
]= ql.DiscountCurve(dates, discounts, ql.Actual360()) curve_2
More complex curves are also available (e.g., ones that can be bootstrapped or fitted over a set of quotes) and will be described in more detail in other notebooks.
A common interface
Because of the way they’re defined, it might look like the first curve above is supposed to be used for forecasting and the second curve for discounting, and indeed, they can be used this way. I can pass the first curve to an instance of an interest-rate index, allowing it to forecast its future fixings…
= ql.RelinkableYieldTermStructureHandle(curve_1)
forecast_curve = ql.Euribor6M(forecast_curve)
index_6M print(index_6M.fixing(ql.Date(1, ql.July, 2019)))
0.009887915724457295
…and I can pass the second to an engine that will use it to discount a sequence of bond cashflows.
= ql.Schedule(
schedule 8, ql.February, 2013),
ql.Date(8, ql.February, 2023),
ql.Date(6, ql.Months),
ql.Period(
ql.TARGET(),
ql.Following,
ql.Following,
ql.DateGeneration.Backward,False,
)= ql.FixedRateBond(
bond 3, 100.0, schedule, [0.03], ql.Thirty360(ql.Thirty360.BondBasis)
)
= ql.RelinkableYieldTermStructureHandle(curve_2)
discount_curve
bond.setPricingEngine(ql.DiscountingBondEngine(discount_curve))print(bond.cleanPrice())
106.21125788897203
However, the fact that a curve internally models rates or discount factors doesn’t force us to use it in a specific way. Since instantaneous forwards, zero rates and discount factors are mathematically related and can be deduced from each other, both curves above can be used in either way. We can link the “discount” curve to the handle in the interest-rate index and use it for forecasting…
forecast_curve.linkTo(curve_2)print(index_6M.fixing(ql.Date(1, ql.July, 2019)))
0.00991059243307731
…or we can link the “forward” curve to the handle in the discounting engine:
discount_curve.linkTo(curve_1)print(bond.cleanPrice())
107.32867290510617
This works because all curves inherit from the YieldTermStructure
base class, which defines a common interface and whose implementation can convert between different quantities by means of their mathematical relationships.
print(curve_1.discount(ql.Date(15, ql.June, 2020)))
print(curve_2.discount(ql.Date(15, ql.June, 2020)))
0.9888299764864131
0.9887891320662344
print(
curve_1.forwardRate(15, ql.June, 2020),
ql.Date(15, ql.June, 2021),
ql.Date(
ql.Thirty360(ql.Thirty360.BondBasis),
ql.Compounded,
ql.Semiannual,
)
)print(
curve_2.forwardRate(15, ql.June, 2020),
ql.Date(15, ql.June, 2021),
ql.Date(
ql.Thirty360(ql.Thirty360.BondBasis),
ql.Compounded,
ql.Semiannual,
) )
1.002504 % 30/360 (Bond Basis) Semiannual compounding
1.144677 % 30/360 (Bond Basis) Semiannual compounding
If you’re interested in the details of how YieldTermStructure
manages it, or how the library provides classes to make it easier to write different kinds of curves, you can reach for your copy of Implementing QuantLib; there’s a whole chapter in there about the implementation of interest-rate term structures.
Mixing it up
An interest-rate curve is not even necessarily based on discount factors or zero rates alone. For instance, you could have modeled a risk-free curve based on discount factors (say, curve_2
above) and then you might add credit risk to it by adding a constant spread over its resulting continuously-compounded zero rates:
= ql.SimpleQuote(0.001)
spread
= ql.ZeroSpreadedTermStructure(
curve_3
ql.YieldTermStructureHandle(curve_2), ql.QuoteHandle(spread)
)
print(curve_2.discount(ql.Date(15, ql.June, 2021)))
print(curve_3.discount(ql.Date(15, ql.June, 2021)))
0.9775671243960207
0.9754649032337227
What are we modeling here? You’ll be the judge of that.
Choices, choices
This setup should enable you to choose your curves based on modeling and data issues alone; that is, what kind of constraints and requirements you have in mind for your interest rates (continuity, smoothness, boundary conditions…), what quantity is best suited to model them, and of course what data you have available.
When using Python, this means having a number of curves to choose from, modeling different quantities or interpolating curve nodes in a different way. When using C++, the library also gives you the possibility to implement your own term-structure class by providing different possible base classes to inherit from; they will take care of the more mundane tasks like converting dates into times or rates into discount factors, and allow you to focus on the actual modeling issues.