import QuantLib as ql
A taste of QuantLib
(Originally published as an article in Wilmott Magazine, January 2023.)
Why don’t I skip the usual introductions and show you some code instead? Let’s see if you find something interesting in it. I’ll try to keep this a quick tour and show the forest rather than the trees; so I’ll gloss over, or completely ignore, a lot of details. Here we go.
Dates and calendars and conventions, oh my
After setting the evaluation date to be used, the first few lines create a sequence of regular dates between an initial and a final one and store them in a Schedule
object. As you can see, we can specify parameters such as their frequency, what convention to use to roll a date when it falls on a holiday, and the calendar to use to determine holidays. We also specify an explicit first date; we’ll use the schedule to specify coupon dates for a bond, and this allows us to have a long first coupon from August 2020 to May 2021, followed by regular semiannual coupons resetting in November and May until 2031.
= ql.Date(8, ql.December, 2022)
value_date = value_date
ql.Settings.instance().evaluationDate
= ql.MakeSchedule(
schedule =ql.Date(26, ql.August, 2020),
effectiveDate=ql.Date(26, ql.May, 2031),
terminationDate=ql.Date(26, ql.May, 2021),
firstDate=ql.Semiannual,
frequency=ql.TARGET(),
calendar=ql.Following,
convention=True,
backwards
)
for d in schedule:
print(d)
August 26th, 2020
May 26th, 2021
November 26th, 2021
May 26th, 2022
November 28th, 2022
May 26th, 2023
November 27th, 2023
May 27th, 2024
November 26th, 2024
May 26th, 2025
November 26th, 2025
May 26th, 2026
November 26th, 2026
May 26th, 2027
November 26th, 2027
May 26th, 2028
November 27th, 2028
May 28th, 2029
November 26th, 2029
May 27th, 2030
November 26th, 2030
May 26th, 2031
Some simple bond calculations
The next few lines create a fixed-rate bond. Besides the schedule that we just defined (used, of course, for the coupon dates), its constructor takes a few more parameters: the number of settlement days, the face amount, the coupon rate, and the day-count convention to use for accrual. Again, I’m not showing all the possibilities here. For instance, the list of coupon rates might contain multiple values, resulting in a step-up bond.
Once instantiated, the bond can start to provide useful figures. Here I’m asking it for its price given a quoted yield and for the amount accrued so far; that is, up to the evaluation date set at the beginning of the program. We could get other results, such as its duration or the amount accrued at some other date; or we could extract its coupons and ask each of them for their amounts, rates, accrual times and so on.
= 3
settlement_days = 1000000.0
face_amount = [0.02]
coupons = ql.Thirty360(ql.Thirty360.BondBasis)
day_counter
= ql.FixedRateBond(
bond
settlement_days,
face_amount,
schedule,
coupons,
day_counter,
)
= 0.025
bond_yield = bond.cleanPrice(bond_yield, day_counter, ql.Compounded, ql.Annual)
price = bond.accruedAmount()
accrual
print(f"Price: {price:.6}")
print(f"Accrual: {accrual:.4}")
Price: 96.3256
Accrual: 0.08333
Discount curves
Let’s say that you have a discount curve available for the bond issuer. In this listing, I’m assuming that it’s specified as a set of zero rates corresponding to a set of dates, and for the sake of illustration I’m hard-coding them; in a real use case, they would come from a database or some API. The ZeroCurve
class turns them into a curve object that can return rates or discount factors at any date in between by using linear interpolation. Other classes can be used if, for instance, the curve is specified as a set of discount factors, or if you want to bootstrap or fit the curve based on a set of market quotes.
Whatever the curve, it can be used to calculate a theoretical price for the bond; instead of relying on a quoted yield, we can create a pricing engine that first uses the curve to discount each bond coupon to the settlement date and then accumulates the results. Once created, we pass the engine to the bond and ask it for its price.
= [
curve_nodes
value_date,+ ql.Period(1, ql.Years),
value_date + ql.Period(2, ql.Years),
value_date + ql.Period(5, ql.Years),
value_date + ql.Period(10, ql.Years),
value_date + ql.Period(15, ql.Years),
value_date
]= [
curve_rates 0.015,
0.015,
0.018,
0.022,
0.025,
0.028,
]= ql.ZeroCurve(curve_nodes, curve_rates, ql.Actual360())
issuer_curve
bond.setPricingEngine(
ql.DiscountingBondEngine(ql.YieldTermStructureHandle(issuer_curve))
)
print(f"Price: {bond.cleanPrice():.6}")
Price: 96.6641
Pricing at a spread over a curve
For a variation on the last calculation, we can also price the bond at a z-spread over a curve; for instance, a government curve, which again I’m assuming to be tabulated on a set of dates. To account for the spread, I used the ZeroSpreadedTermStructure
class, which adds it to the government curve and creates a new curve; the latter can be passed to a new pricing engine.
The spread (modeled as an instance of the SimpleQuote
class) is initially set to zero; but it can be set afterwards to any other value, and the bond price will react accordingly. Adding a constant spread is not the only way we can modify an existing curve: other classes provide the means to model, say, flattening or steepening scenarios.
= [
curve_rates_2 -0.005,
-0.005,
0.001,
0.004,
0.009,
0.012,
]= ql.ZeroCurve(curve_nodes, curve_rates_2, ql.Actual360())
govies_curve
= ql.SimpleQuote(0.0)
spread = ql.ZeroSpreadedTermStructure(
discount_curve
ql.YieldTermStructureHandle(govies_curve), ql.QuoteHandle(spread)
)
bond.setPricingEngine(
ql.DiscountingBondEngine(ql.YieldTermStructureHandle(discount_curve))
)
print(f"Price: {bond.cleanPrice():.6}")
0.01)
spread.setValue(
print(f"Price: {bond.cleanPrice():.6}")
Price: 110.34
Price: 101.913
Finding the z-spread given a price
Finally, what if you have a price—for instance, the one we calculated given the quoted yield—and you want to find the corresponding z-spread over the government curve? The objects we already created can be reused to create a function (discrepancy
in the code) that takes a value, sets it to the spread, and returns the difference between the resulting price and the target price. Once created, the function can be passed to a one-dimensional solver that finds its root; that is, the value of the spread for which the discrepancy is null and the calculated price equals the desired one.
def discrepancy(s):
spread.setValue(s)return bond.cleanPrice() - price
= ql.Brent()
solver = 1e-5
accuracy = 0.015
guess = 1e-4
step
= solver.solve(discrepancy, accuracy, guess, step)
spread_over_govies
print(f"Spread: {spread_over_govies:.4}")
Spread: 0.01712
Not only Python
All the features showcased above are, of course, available in the underlying C++ library. There are a few differences, due in part to the different syntax and in part to the need in C++ for memory management which makes it necessary to use smart pointers, hidden in Python by the wrapper code. For comparison, here is the original C++ code from which the Python code above was translated (I also have a note on smart pointers, but it can wait until after the code.)
#include <ql/instruments/bonds/fixedratebond.hpp>
#include <ql/math/solvers1d/brent.hpp>
#include <ql/pricingengines/bond/discountingbondengine.hpp>
#include <ql/quotes/simplequote.hpp>
#include <ql/settings.hpp>
#include <ql/termstructures/yield/zerocurve.hpp>
#include <ql/termstructures/yield/zerospreadedtermstructure.hpp>
#include <ql/time/calendars/target.hpp>
#include <ql/time/daycounters/actual360.hpp>
#include <ql/time/daycounters/thirty360.hpp>
#include <ql/time/schedule.hpp>
#include <iostream>
int main() {
using namespace QuantLib;
auto valueDate = Date(8, December, 2022);
::instance().evaluationDate() = valueDate;
Settings
// Dates and calendars and conventions, oh my
=
Schedule schedule ()
MakeSchedule.from(Date(26, August, 2020))
.to(Date(26, May, 2031))
.withFirstDate(Date(26, May, 2021))
.withFrequency(Semiannual)
.withCalendar(TARGET())
.withConvention(Following)
.backwards();
for (auto d : schedule) {
std::cout << d << "\n";
}
std::cout << std::endl;
// Some simple bond calculations
unsigned settlementDays = 3;
double faceAmount = 1000000.0;
std::vector<Rate> coupons = {0.02};
auto dayCounter = Thirty360(Thirty360::BondBasis);
auto bond =
(settlementDays, faceAmount, schedule,
FixedRateBond, dayCounter);
coupons
= 0.025;
Rate yield double price = bond.cleanPrice(yield, dayCounter,
, Annual);
Compoundeddouble accrual = bond.accruedAmount();
std::cout << price << "\n"
<< accrual << std::endl;
// Discount curves
std::vector<Date> curveNodes = {valueDate,
+ 1 * Years,
valueDate + 2 * Years,
valueDate + 5 * Years,
valueDate + 10 * Years,
valueDate + 15 * Years};
valueDate std::vector<Rate> curveRates = {0.015, 0.015, 0.018,
0.022, 0.025, 0.028};
auto issuerCurve =
::make_shared<InterpolatedZeroCurve<Linear>>(
ext, curveRates, Actual360());
curveNodes
.setPricingEngine(
bond::make_shared<DiscountingBondEngine>(
ext<YieldTermStructure>(issuerCurve)));
Handlestd::cout << bond.cleanPrice() << std::endl;
// Pricing at a spread over a curve
std::vector<Rate> curveRates2 = {-0.005, -0.005, 0.001,
0.004, 0.009, 0.012};
auto goviesCurve =
::make_shared<InterpolatedZeroCurve<Linear>>(
ext, curveRates2, Actual360());
curveNodes
auto spread = ext::make_shared<SimpleQuote>(0.0);
auto discountCurve =
::make_shared<ZeroSpreadedTermStructure>(
ext<YieldTermStructure>(goviesCurve),
Handle<Quote>(spread));
Handle
.setPricingEngine(
bond::make_shared<DiscountingBondEngine>(
ext<YieldTermStructure>(discountCurve)));
Handlestd::cout << bond.cleanPrice() << std::endl;
->setValue(0.01);
spreadstd::cout << bond.cleanPrice() << std::endl;
// Finding the z-spread given a price
auto discrepancy = [&spread, &bond, price](Spread s) {
->setValue(s);
spreadreturn bond.cleanPrice() - price;
};
auto solver = Brent();
double accuracy = 1e-5, guess = 0.015, step = 1e-4;
=
Spread spreadOverGovies .solve(discrepancy, accuracy, guess, step);
solver
std::cout << spreadOverGovies << std::endl;
}
Smart pointers and other standard classes
And now, the note on smart pointers I promised before. They were added the the C++ standard in 2011, so I won’t bore you by explaining what they are and why they are used; I assume you know all that. What you might be wondering, however, is why they seem to be in namespace ext
in the code above.
In short: we have a compilation flag that allows us to define ext::shared_ptr
as either boost::shared_ptr
(the current default) or std::shared_ptr
; the same flag also controls related functions such as make_shared
or dynamic_pointer_cast
. We did the same for other classes such as std::function
or std::tuple
, each with its own compilation flag.
This gives us a migration path from using the Boost classes (the only alternative for the first decade of QuantLib) to using the ones in the C++ standard, without breaking other people’s code in the switch.
Boost is still the default for some of these classes; beside the smart pointers, also any
and optional
. A while ago, we changed the default for function
and tuple
, and recently we removed the switch so that the std
implementation is always used. The long-term plan is to do the same for the other classes.
For shared_ptr
, however, the decision will be a bit more complicated. There’s a significant difference in behavior between boost::shared_ptr
and std::shared_ptr
; the former can be configured so that trying to access a null shared_ptr
raises an exception, while the latter doesn’t check for null pointers and lets you access them and crash the program.
Therefore, it’s possible that we’ll keep boost::shared_ptr
around as an alternative for quite a while, for instance when generating the Python wrappers for QuantLib. Unlike the C++ community, Python programmers tend to frown upon segmentation faults.