Table of Contents
- A note on Python and C++
- Code conventions used in this book
- 1. QuantLib basics
- 2. Instruments and pricing engines
- 3. EONIA curve bootstrapping
- 4. Constructing a yield curve
- 5. Dangerous day-count conventions
- 6. Valuing European and American options
- 7. Duration of floating-rate bonds
- Translating QuantLib Python examples to C++
The authors have used good faith effort in preparation of this book, but make no expressed or implied warranty of any kind and disclaim without limitation all responsibility for errors or omissions. No liability is assumed for incidental or consequential damages in connection with or arising out of the use of the information or programs contained herein. Use of the information and instructions in this book is at your own risk.
The cover image is in the public domain and available from the New York Public Library. The cover font is Open Sans Condensed, released by Steve Matteson under the Apache License version 2.0.
A note on Python and C++
The choice of using the QuantLib Python bindings and Jupyter
was due to their interactivity, which make it easier to demonstrate
features, and to the fact that the platform provides out of the box
excellent modules like matplotlib
for graphing and pandas
for data
analysis.
This choice might seem to leave C++ users out in the cold. However, it’s easy enough to translate the Python code shown here into the corresponding C++ code. An example of such translation is shown in the appendix.
Code conventions used in this book
The recipes in this cookbook are written as Jupyter notebooks, and follow their structure: blocks of explanatory text, like the one you’re reading now, are mixed with cells containing Python code (inputs) and the results of executing it (outputs). The code and its output—if any—are marked by In [N]
and Out [N]
, respectively, with N
being the index of the cell. You can see an example in the computations below:
By default, Jupyter displays the result of the last instruction as the output of a cell, like it did above; however, print
statements can display further results.
Jupyter also knows a few specific data types, such as Pandas data frames, and displays them in a more readable way:
foo | bar | |
---|---|---|
0 | 1 | a |
1 | 2 | b |
2 | 3 | c |
The index of the cells shows the order of their execution. Jupyter doesn’t constrain it; however, in all of the recipes of this book the cells were executed in sequential order as displayed. All cells are executed in the global Python scope; this means that, as we execute the code in the recipes, all variables, functions and classes defined in a cell are available to the ones that follow.
Notebooks can also include plots, as in the following cell:
As you might have noted, the cell above also printed a textual representation of the object returned from the plot, since it’s the result of the last instruction in the cell. To prevent this, cells in the recipes might have a semicolon at the end, as in the next cell. This is just a quirk of the Jupyter display system, and it doesn’t have any particular significance; I’m mentioning it here just so that you dont’t get confused by it.
Finally, the utils
module that I imported above is a short module containing convenience functions, mostly related to plots, for the notebooks in this collection. It’s not necessary to understand its implementation to follow the recipes, and therefore we won’t cover it here; but if you’re interested and want to look at it, it’s included in the zip archive that you can download from Leanpub if you purchased the book.
1. QuantLib basics
In this chapter we will introduce some of the basic concepts such as Date
, Period
, Calendar
and Schedule
. These are QuantLib constructs that are used throughout the library in creation of instruments, models, term structures etc.
Date Class
The Date
object can be created using the constructor as Date(day, month, year)
. It would be worthwhile to pay attention to the fact that day
is the first argument, followed by month
and then the year
. This is different from the Python datetime
object instantiation.
The fields of the Date
object can be accessed using the month()
, dayOfMonth()
and year()
methods. The weekday()
method can be used to fetch the day of the week.
The Date
objects can also be used to perform arithmetic operations such as advancing by days, weeks, months etc. Periods such as weeks or months can be denoted using the Period
class. Period
object constructor signature is Period(num_periods, period_type)
. The num_periods
is an integer and represents the number of periods. The period_type
can be Weeks
, Months
and Years
.
One can also do logical operations using the Date
object.
The Date
object is used in setting valuation dates, issuance and expiry dates of instruments. The Period
object is used in setting tenors, such as that of coupon payments, or in constructing payment schedules.
Calendar Class
The Date
arithmetic above did not take holidays into account. But valuation of different securities would require taking into account the holidays observed in a specific exchange or country. The Calendar
class implements this functionality for all the major exchanges. Let us take a look at a few examples here.
The addHoliday
and removeHoliday
methods in the calendar can be used to add and remove holidays to the calendar respectively. If a calendar has any missing holidays or has a wrong holiday, then these methods come handy in fixing the errors. The businessDaysBetween
method helps find out the number of business days between two dates per a given calendar. Let us use this method on the us_calendar
and italy_calendar
as a sanity check.
In valuation of certain deals, more than one calendar’s holidays are observed. QuantLib has JointCalendar
class that allows you to combine the holidays of two or more calendars. Let us take a look at a working example.
Schedule Class
The Schedule
object is necessary in creating coupon schedules or call schedules. Schedule
object constructors have the following signature:
and
date | |
---|---|
0 | January 2nd, 2015 |
1 | February 2nd, 2015 |
2 | March 2nd, 2015 |
3 | April 1st, 2015 |
4 | May 1st, 2015 |
5 | June 1st, 2015 |
6 | July 1st, 2015 |
7 | August 3rd, 2015 |
8 | September 1st, 2015 |
9 | October 1st, 2015 |
10 | November 2nd, 2015 |
11 | December 1st, 2015 |
12 | January 4th, 2016 |
Here we have generated a Schedule
object that will contain dates between effective_date
and termination_date
with the tenor
specifying the Period
to be Monthly
. The calendar
object is used for determining holidays. Here we have chosen the convention to be the day following holidays. That is why we see that holidays are excluded in the list of dates.
The Schedule
class can handle generation of dates with irregularity in schedule. The two extra parameters firstDate
and nextToLastDate
parameters along with a combination of forward or backward date generation rule can be used to generate short or long stub payments at the front or back end of the schedule. For example, the following combination of firstDate
and backward generation rule creates a short stub in the front on the January 15, 2015.
date | |
---|---|
0 | January 2nd, 2015 |
1 | January 15th, 2015 |
2 | February 2nd, 2015 |
3 | March 2nd, 2015 |
4 | April 1st, 2015 |
5 | May 1st, 2015 |
6 | June 1st, 2015 |
7 | July 1st, 2015 |
8 | August 3rd, 2015 |
9 | September 1st, 2015 |
10 | October 1st, 2015 |
11 | November 2nd, 2015 |
12 | December 1st, 2015 |
13 | January 4th, 2016 |
Using the nextToLastDate
parameter along with the forward date generation rule creates a short stub at the back end of the schedule.
date | |
---|---|
0 | January 2nd, 2015 |
1 | February 2nd, 2015 |
2 | March 2nd, 2015 |
3 | April 1st, 2015 |
4 | May 1st, 2015 |
5 | June 1st, 2015 |
6 | July 1st, 2015 |
7 | August 3rd, 2015 |
8 | September 1st, 2015 |
9 | October 1st, 2015 |
10 | November 2nd, 2015 |
11 | December 1st, 2015 |
12 | December 15th, 2015 |
13 | January 4th, 2016 |
Using the backward generation rule along with the firstDate
allows us to create a long stub in the front. Below the first two dates are longer in duration than the rest of the dates.
date | |
---|---|
0 | December 15th, 2014 |
1 | February 2nd, 2015 |
2 | March 2nd, 2015 |
3 | April 1st, 2015 |
4 | May 1st, 2015 |
5 | June 1st, 2015 |
6 | July 1st, 2015 |
7 | August 3rd, 2015 |
8 | September 1st, 2015 |
9 | October 1st, 2015 |
10 | November 2nd, 2015 |
11 | December 1st, 2015 |
12 | January 4th, 2016 |
Similarly the usage of nextToLastDate
parameter along with forward date generation rule can be used to generate long stub at the back of the schedule.
date | |
---|---|
0 | January 2nd, 2015 |
1 | February 2nd, 2015 |
2 | March 2nd, 2015 |
3 | April 1st, 2015 |
4 | May 1st, 2015 |
5 | June 1st, 2015 |
6 | July 1st, 2015 |
7 | August 3rd, 2015 |
8 | September 1st, 2015 |
9 | October 1st, 2015 |
10 | November 2nd, 2015 |
11 | December 1st, 2015 |
12 | January 15th, 2016 |
Below the Schedule
is generated from a list of dates.
date | |
---|---|
0 | January 2nd, 2015 |
1 | February 2nd, 2015 |
2 | March 2nd, 2015 |
3 | April 1st, 2015 |
4 | May 1st, 2015 |
5 | June 1st, 2015 |
6 | July 1st, 2015 |
7 | August 3rd, 2015 |
8 | September 1st, 2015 |
9 | October 1st, 2015 |
10 | November 2nd, 2015 |
11 | December 1st, 2015 |
12 | January 4th, 2016 |
Interest Rate
The InterestRate
class can be used to store the interest rate with the compounding type, day count and
the frequency of compounding. Below we show how to create an interest rate of 5.0% compounded annually,
using Actual/Actual day count convention.
Lets say if you invest a dollar at the interest rate described by interest_rate
, the
compoundFactor
method in the InterestRate
object gives you how much your investment will be worth after any period.
Below we show that the value returned by compound_factor
for 2 years agrees with
the expected compounding formula.
The discountFactor
method returns the reciprocal of the compoundFactor
method.
The discount factor is useful while calculating the present value of future cashflows.
A given interest rate can be converted into other compounding types and compounding frequency using the equivalentRate
method.
The discount factor for the two InterestRate
objects, interest_rate
and new_interest_rate
are the same, as shown below.
The impliedRate
method in the InterestRate
class
takes compound factor to return the implied rate. The impliedRate
method
is a static method in the InterestRate
class and can be used without an
instance of InterestRate
. Internally the equivalentRate
method invokes
the impliedRate
method in its calculations.
Conclusion
This chapter gave an introduction to the basics of QuantLib. Here we explained the Date
, Schedule
, Calendar
and InterestRate
classes.
2. Instruments and pricing engines
In this notebook, I’ll show how instruments and their available engines can monitor changes in their input data.
Setup
To begin, we import the QuantLib module and set up the global evaluation date.
The instrument
As a sample instrument, we’ll take a textbook example: a European option.
Building the option requires only the specification of its contract, so its payoff (it’s a call option with strike at 100) and its exercise, three months from today’s date. Market data will be selected and passed later, depending on the calculation methods.
First pricing method: analytic Black-Scholes formula
The different pricing methods are implemented as pricing engines holding the required market data. The first we’ll use is the one encapsulating the analytic Black-Scholes formula.
First, we collect the quoted market data. We’ll assume flat risk-free rate and volatility, so they can be expressed by SimpleQuote
instances: those model numbers whose value can change and that can notify observers when this happens. The underlying value is at 100, the risk-free value at 1%, and the volatility at 20%.
In order to build the engine, the market data are encapsulated in a Black-Scholes process object. First we build flat curves for the risk-free rate and the volatility…
…then we instantiate the process with the underlying value and the curves we just built. The inputs are all stored into handles, so that we could change the quotes and curves used if we wanted. I’ll skip over this for the time being.
Once we have the process, we can finally use it to build the engine…
…and once we have the engine, we can set it to the option and evaluate the latter.
Depending on the instrument and the engine, we can also ask for other results; in this case, we can ask for Greeks.
Market changes
As I mentioned, market data are stored in Quote
instances and thus can notify the option when any of them changes. We don’t have to do anything explicitly to tell the option to recalculate: once we set a new value to the underlying, we can simply ask the option for its NPV again and we’ll get the updated value.
Just for showing off, we can use this to graph the option value depending on the underlying asset value. After a bit of graphic setup (don’t pay attention to the man behind the curtains)…
…we can take an array of values from 80 to 120, set the underlying value to each of them, collect the corresponding option values, and plot the results.
Other market data also affect the value, of course.
We can see it when we change the risk-free rate…
…or the volatility.
Date changes
Just as it does when inputs are modified, the value also changes if we advance the evaluation date. Let’s look first at the value of the option when its underlying is worth 105 and there’s still three months to exercise…
…and then move to a date two months before exercise.
Again, we don’t have to do anything explicitly: we just ask the option its value, and as expected it has decreased, as can also be seen by updating the plot.
In the default library configuration, the returned value goes down to 0 when we reach the exercise date.
Other pricing methods
The pricing-engine mechanism allows us to use different pricing methods. For comparison, I’ll first set the input data back to what they were previously and output the Black-Scholes price.
Let’s say that we want to use a Heston model to price the option. What we have to do is to instantiate the corresponding class with the desired inputs…
…pass it to the corresponding engine, and set the new engine to the option.
Asking the option for its NPV will now return the value according to the new model.
Lazy recalculation
One last thing. Up to now, we haven’t really seen evidence of notifications going around. After all, the instrument might just have recalculated its value every time, regardless of notifications. What I’m going to show, instead, is that the option doesn’t just recalculate every time anything changes; it also avoids recalculations when nothing has changed.
We’ll switch to a Monte Carlo engine, which takes a few seconds to run the required simulation.
When we ask for the option value, we have to wait for the calculation to finish…
…but a second call to the NPV
method will be instantaneous when made before anything changes. In this case, the option didn’t calculate its value; it just returned the result that it cached from the previous call.
If we change anything (e.g., the underlying value)…
…the option is notified of the change, and the next call to NPV
will again take a while.
3. EONIA curve bootstrapping
In the next notebooks, I’ll reproduce the results of the paper by F. M. Ametrano and M. Bianchetti, Everything You Always Wanted to Know About Multiple Interest Rate Curve Bootstrapping but Were Afraid to Ask (April 2, 2013). The paper is available at SSRN: http://ssrn.com/abstract=2219548.
First try
We start by instantiating helpers for all the rates used in the bootstrapping process, as reported in figure 25 of the paper.
The first three instruments are three 1-day deposit that give us discounting between today and the day after spot. They are modeled by three instances of the DepositRateHelper
class with a tenor of 1 day and a number of fixing days going from 0 (for the deposit starting today) to 2 (for the deposit starting on the spot date).
Then, we have a series of OIS quotes for the first month. They are modeled by instances of the OISRateHelper
class with varying tenors. They also require an instance of the Eonia
class, which doesn’t need a forecast curve and can be shared between the helpers.
Next, five OIS forwards on ECB dates. For these, we need to instantiate the DatedOISRateHelper
class and specify start and end dates explicitly.
Finally, we add OIS quotes up to 30 years.
The curve is an instance of PiecewiseLogCubicDiscount
(corresponding to the PiecewiseYieldCurve<Discount,LogCubic>
class in C++; I won’t repeat the argument for this choice made in section 4.5 of the paper). We let the reference date of the curve move with the global evaluation date, by specifying it as 0 days after the latter on the TARGET calendar. The day counter chosen is not of much consequence, as it is only used internally to convert dates into times. Also, we enable extrapolation beyond the maturity of the last helper; that is mostly for convenience as we retrieve rates to plot the curve near its far end.
To compare the curve with the one shown in figure 26 of the paper, we can retrieve daily overnight rates over its first two years and plot them:
However, we still have work to do. Out plot above shows a rather large bump at the end of 2012 which is not present in the paper. To remove it, we need to model properly the turn-of-year effect.
Turn-of-year jumps
As explained in section 4.8 of the paper, the turn-of-year effect is a jump in interest rates due to an increased demand for liquidity at the end of the year. The jump is embedded in any quoted rates that straddles the end of the year and must be treated separately; the YieldTermStructure
class allows this by taking any number of jumps, modeled as additional discount factors, and applying them at the specified dates.
Our problem is to estimate the size of the jump. To simplify analysis, we turn to flat forward rates instead of log-cubic discounts; thus, we instantiate a PiecewiseFlatForward
curve (corresponding to PiecewiseYieldCurve<ForwardRate,BackwardFlat>
in C++).
To show the jump more clearly, I’ll restrict the plot to the first 6 months:
As we see, the forward ending at the beginning of January 2013 is out of line. In order to estimate the jump, we need to estimate a “clean” forward that doesn’t include it.
A possible estimate (although not the only one) can be obtained by interpolating the forwards around the one we want to replace. To do so, we extract the values of the forwards rates and their corresponding dates.
If we look at the first few nodes, we can clearly see that the seventh is out of line.
To create a curve that doesn’t include the jump, we replace the relevant forward rate with a simple average of the ones that precede and follow…
…and instantiate a ForwardCurve
with the modified nodes.
For illustration, we can extract daily overnight nodes from the doctored curve and plot them alongside the old ones:
Now we can estimate the size of the jump. As the paper hints, it’s more an art than a science. I’ve been able to reproduce closely the results of the paper by extracting from the two curves the forward rate over the two weeks around the end of the year:
We want to attribute the whole jump to the last day of the year, so we rescale it according to
$$ (F-F_1) \cdot t_{12} = J \cdot t_J $$where \(t_{12}\) is the time between the two dates and \(t_J\) is the time between the start and end date of the end-of-year overnight deposit. This gives us a jump quite close to the value of 10.2 basis points reported in the paper.
As I mentioned previously, the jump can be added to the curve as a corresponding discount factor \(1/(1+J \cdot t_J)\) on the last day of the year. The information can be passed to the curve constructor, giving us a new instance:
Retrieving daily overnight rates from the new curve and plotting them, we can see the jump quite clearly:
We can now go back to log-cubic discounts and add the jump.
As you can see, the large bump is gone now. The two plots in figure 26 can be reproduced as follows (omitting the jump at the end of 2013 for brevity, and the flat forwards for clarity):
A final word of warning: as you saw, the estimate of the jumps is not an exact science, so it’s best to check it manually and not to leave it to an automated procedure.
Moreover, jumps nowadays might be present at the end of each month, as reported for instance in Paolo Mazzocchi’s presentation at the QuantLib User Meeting 2014. This, too, suggests particular care in building the Eonia curve.
4. Constructing a yield curve
In this chapter we will go over the construction of treasury yield curve. Let’s start by importing QuantLib and other necessary libraries.
This is an example based on Exhibit 5-5 given in Frank Fabozzi’s Bond Markets, Analysis and Strategies, Sixth Edition.
Maturities | Curve | |
---|---|---|
6M | 5.25 | |
12M | 5.50 | |
18M | 5.75 | |
24M | 6.00 | |
30M | 6.25 | |
36M | 6.50 | |
42M | 6.75 | |
48M | 6.80 | |
54M | 7.00 | |
60M | 7.10 | |
66M | 7.15 | |
72M | 7.20 | |
78M | 7.30 | |
84M | 7.35 | |
90M | 7.40 | |
96M | 7.50 | |
102M | 7.60 | |
108M | 7.60 | |
114M | 7.70 | |
120M | 7.80 |
Below we declare some constants and conventions used here. For the sake of simplicity, we assume that some of the constants are the same for deposit rates and bond rates.
The basic idea of bootstrapping is to use the deposit rates and bond rates to create individual rate helpers. Then use the combination of the two helpers to construct the yield curve. As a first step, we create the deposit rate helpers as shown below.
The rest of the points are coupon bonds. We assume that the YTM given for the bonds are all par rates. So we have bonds with coupon rate same as the YTM. Using this information, we construct the fixed rate bond helpers below.
The union of the two helpers is what we use in bootstrapping shown below.
The get_spot_rates
is a convenient wrapper function that we will use to get the spot rates on a monthly interval.
The bootstrapping process is fairly generic in QuantLib. You can chose what variable you are bootstrapping, and what is the interpolation method used in the bootstrapping. There are multiple piecewise interpolation methods that can be used for this process. The PiecewiseLogCubicDiscount
will construct a piece wise yield curve using LogCubic
interpolation of the Discount
factor. Similarly PiecewiseLinearZero
will use Linear
interpolation of Zero
rates. PiecewiseCubicZero
will interpolate the Zero
rates using a Cubic
interpolation method.
The zero rates from the tail end of the PiecewiseLogCubicDiscount
bootstrapping is shown below.
Maturities | Curve | |
---|---|---|
9.666667 | 7.981384 | |
9.750000 | 8.005292 | |
9.833333 | 8.028145 | |
9.916667 | 8.050187 | |
10.000000 | 8.071649 |
The yield curves using the PiecewiseLinearZero
and PiecewiseCubicZero
is shown below. The tail end of the zero rates obtained from PiecewiseLinearZero
bootstrapping is also shown below. The numbers can be compared with that of the PiecewiseLogCubicDiscount
shown above.
Maturities | Curve | |
---|---|---|
9.666667 | 7.976804 | |
9.750000 | 8.000511 | |
9.833333 | 8.024221 | |
9.916667 | 8.047934 | |
10.000000 | 8.071649 |
All three are plotted below to give you an overall perspective of the three methods.
Conclusion
In this chapter we saw how to construct yield curves by bootstrapping bond quotes.
5. Dangerous day-count conventions
(Based on a question by Min Gao on the QuantLib mailing list. Thanks!)
The problem
Talking about term structures in Implementing QuantLib, I suggest to use simple day-count conventions such as Actual/360 or Actual/365 to initialize curves. That’s because the convention is used internally to convert dates into times, and we want the conversion to be as regular as possible. For instance, we’d like distances between dates to be additive: given three dates \(d_1\), \(d_2\) and \(d_3\), we would expect that \(T(d_1,d_2) + T(d_2,d_3) = T(d_1,d_3)\), where \(T\) denotes the time between dates.
Unfortunately, that’s not always the case for some day counters. The property holds for most dates…
…but doesn’t for some.
That’s because some day-count conventions were designed to calculate the duration of a coupon, not the distance between any two given dates. They have particular formulas and exceptions that make coupons more regular; but those exceptions also cause some pairs of dates to have strange properties. For instance, there might be no distance at all between some particular distinct dates:
The 30/360 convention is not the worst offender, either. Min Gao’s question came from using for the term structure the same convention used for the bond being priced, that is, ISMA actual/actual. This day counter is supposed to be given a reference period, as well as the two dates whose distance one needs to measure; failing to do so will result in the wrong results…
…and sometimes, in spectacularly wrong results. Here is what happens if we plot the year fraction since January 1st, 2018 as a function of the date over that same year.
Of course, that’s no way to convert dates into times. Using this day-count convention inside a coupon is ok, of course. Using it inside a term structure, which doesn’t have any concept of a reference period, leads to very strange behaviors.
Any solutions?
Not really, at this time. Work is underway to store a schedule inside an ISMA actual/actual day counter and use it to retrieve the correct reference period, but that’s not fully working yet. In the meantime, what I can suggest is to use the specified day-count conventions for coupons; but, unless something prevents it, use a simple day-count convention such as actual/360 or actual/365 for term structures.
6. Valuing European and American options
I have written about option pricing earlier. The introduction to option pricing gave an overview of the theory behind option pricing. The post on introduction to binomial trees outlined the binomial tree method to price options.
In this post, we will use QuantLib and the Python extension to illustrate a simple example. Here we are going to price a European option using the Black-Scholes-Merton formula. We will price them again using the Binomial tree and understand the agreement between the two.
European Option
Let us consider a European call option for AAPL with a strike price of 130 maturing on 15th Jan, 2016. Let the spot price be 127.62. The volatility of the underlying stock is know to be 20%, and has a dividend yield of 1.63%. Let’s value this option as of 8th May, 2015.
We construct the European option here.
The Black-Scholes-Merton process is constructed here.
Lets compute the theoretical price using the AnalyticEuropeanEngine
.
Lets compute the price using the binomial-tree approach.
In the plot below, we show the convergence of binomial-tree approach by comparing its price with the BSM price.
American Option
The above exercise was pedagogical, and introduces one to pricing using the binomial tree approach and compared with Black-Scholes. As a next step, we will use the Binomial pricing to value American options.
The construction of an American option is similar to the construction of European
option discussed above. The one main difference is the use of AmericanExercise
instead of EuropeanExercise
use above.
Once we have constructed the american_option
object, we can price them using the Binomial trees as done above. We use the same function we constructed above.
Above, we plot the price of the American option as a function of steps used in the binomial tree, and compare with that of the Black-Scholes price for the European option with all other variables remaining the same. The binomial tree converges as the number of steps used in pricing increases. American option is valued more than the European BSM price because of the fact that it can be exercised anytime during the course of the option.
Conclusion
In this chapter we learnt about valuing European and American options using the binomial tree method.
7. Duration of floating-rate bonds
(Based on a question by Antonio Savoldi on the QuantLib mailing list. Thanks!)
The problem
We want to calculate the modified duration of a floating-rate bond. First, we need an interest-rate curve to forecast its coupon rates: for illustration’s sake, let’s take a flat curve with a 0.2% rate.
Then, we instantiate the index to be used. The bond has semiannual coupons, so we create a Euribor6M
instance and we pass it the forecast curve. Also, we set a past fixing for the current coupon (which, having fixed in the past, can’t be forecast).
The bond was issued a couple of months before the evaluation date and will run for 5 years with semiannual coupons.
The cash flows are calculated based on the forecast curve. Here they are, together with their dates. As expected, they each pay around 0.1% of the notional.
date | amount | |
---|---|---|
1 | February 9th, 2015 | 0.102778 |
2 | August 10th, 2015 | 0.101112 |
3 | February 8th, 2016 | 0.101112 |
4 | August 8th, 2016 | 0.101112 |
5 | February 8th, 2017 | 0.102223 |
6 | August 8th, 2017 | 0.100556 |
7 | February 8th, 2018 | 0.102223 |
8 | August 8th, 2018 | 0.100556 |
9 | February 8th, 2019 | 0.102223 |
10 | August 8th, 2019 | 0.100556 |
11 | August 8th, 2019 | 100.000000 |
If we try to use the function provided for calculating bond durations, though, we run into a problem. When we pass it the bond and a 0.2% semiannual yield, the result we get is:
which is about the time to maturity. Shouldn’t we get the time to next coupon instead?
What happened?
The function above is too generic. It calculates the modified duration as \(\displaystyle{-\frac{1}{P}\frac{dP}{dy}}\); however, it doesn’t know what kind of bond it has been passed and what kind of cash flows are paid, so it can only consider the yield for discounting and not for forecasting. If you looked into the C++ code, you’d see that the bond price \(P\) above is calculated as the sum of the discounted cash flows, as in the following:
(Incidentally, we can see that this matches the calculation in the dirtyPrice
method of the Bond
class.)
Finally, the derivative \(\displaystyle{\frac{dP}{dy}}\) in the duration formula in approximated as \(\displaystyle{\frac{P(y+dy)-P(y-dy)}{2 dy}}\), so that we get:
which is the same figure returned by BondFunctions.duration
.
The problem is that the above doesn’t use the yield curve for forecasting, so it’s not really considering the bond as a floating-rate bond. It’s using it as a fixed-rate bond, whose coupon rates happen to equal the current forecasts for the Euribor 6M fixings. This is clear if we look at the coupon amounts and discounts we stored during the calculation:
date | amount | discounts | amount (+) | discounts (+) | amount (-) | discounts (-) | |
---|---|---|---|---|---|---|---|
1 | February 9th, 2015 | 0.102778 | 0.999339 | 0.102778 | 0.999336 | 0.102778 | 0.999343 |
2 | August 10th, 2015 | 0.101112 | 0.998330 | 0.101112 | 0.998322 | 0.101112 | 0.998338 |
3 | February 8th, 2016 | 0.101112 | 0.997322 | 0.101112 | 0.997308 | 0.101112 | 0.997335 |
4 | August 8th, 2016 | 0.101112 | 0.996314 | 0.101112 | 0.996296 | 0.101112 | 0.996333 |
5 | February 8th, 2017 | 0.102223 | 0.995297 | 0.102223 | 0.995273 | 0.102223 | 0.995320 |
6 | August 8th, 2017 | 0.100556 | 0.994297 | 0.100556 | 0.994269 | 0.100556 | 0.994325 |
7 | February 8th, 2018 | 0.102223 | 0.993282 | 0.102223 | 0.993248 | 0.102223 | 0.993315 |
8 | August 8th, 2018 | 0.100556 | 0.992284 | 0.100556 | 0.992245 | 0.100556 | 0.992322 |
9 | February 8th, 2019 | 0.102223 | 0.991270 | 0.102223 | 0.991227 | 0.102223 | 0.991314 |
10 | August 8th, 2019 | 0.100556 | 0.990275 | 0.100556 | 0.990226 | 0.100556 | 0.990323 |
11 | August 8th, 2019 | 100.000000 | 0.990275 | 100.000000 | 0.990226 | 100.000000 | 0.990323 |
where you can see how the discount factors changed when the yield was modified, but the coupon amounts stayed the same.
The solution
Unfortunately, there’s no easy way to fix the BondFunctions.duration
method so that it does the right thing. What we can do, instead, is to repeat the calculation above while setting up the bond and the curves so that the yield is used correctly. In particular, we have to link the forecast curve to the flat yield curve being modified…
…so that changing the yield will also affect the forecast rate of the coupons.
Now the coupon amounts change with the yield (except, of course, the first coupon, whose amount was already fixed)…
date | amount | discounts | amount (+) | discounts (+) | amount (-) | discounts (-) | |
---|---|---|---|---|---|---|---|
1 | February 9th, 2015 | 0.102778 | 0.999339 | 0.102778 | 0.999336 | 0.102778 | 0.999343 |
2 | August 10th, 2015 | 0.101112 | 0.998330 | 0.101617 | 0.998322 | 0.100606 | 0.998338 |
3 | February 8th, 2016 | 0.101112 | 0.997322 | 0.101617 | 0.997308 | 0.100606 | 0.997335 |
4 | August 8th, 2016 | 0.101112 | 0.996314 | 0.101617 | 0.996296 | 0.100606 | 0.996333 |
5 | February 8th, 2017 | 0.102223 | 0.995297 | 0.102734 | 0.995273 | 0.101712 | 0.995320 |
6 | August 8th, 2017 | 0.100556 | 0.994297 | 0.101059 | 0.994269 | 0.100053 | 0.994325 |
7 | February 8th, 2018 | 0.102223 | 0.993282 | 0.102734 | 0.993248 | 0.101712 | 0.993315 |
8 | August 8th, 2018 | 0.100556 | 0.992284 | 0.101059 | 0.992245 | 0.100053 | 0.992322 |
9 | February 8th, 2019 | 0.102223 | 0.991270 | 0.102734 | 0.991227 | 0.101712 | 0.991314 |
10 | August 8th, 2019 | 0.100556 | 0.990275 | 0.101059 | 0.990226 | 0.100053 | 0.990323 |
11 | August 8th, 2019 | 100.000000 | 0.990275 | 100.000000 | 0.990226 | 100.000000 | 0.990323 |
…and the duration is calculated correctly, thus approximating the four months to the next coupon.
This also holds if the discounting curve is dependent, but not the same as the forecast curve; e.g., as in the case of an added credit spread:
This causes the price to decrease due to the increased discount factors…
…but the coupon amounts are still the same.
date | amount | discount | |
---|---|---|---|
1 | February 9th, 2015 | 0.102778 | 0.999009 |
2 | August 10th, 2015 | 0.101112 | 0.997496 |
3 | February 8th, 2016 | 0.101112 | 0.995984 |
4 | August 8th, 2016 | 0.101112 | 0.994475 |
5 | February 8th, 2017 | 0.102223 | 0.992952 |
6 | August 8th, 2017 | 0.100556 | 0.991456 |
7 | February 8th, 2018 | 0.102223 | 0.989938 |
8 | August 8th, 2018 | 0.100556 | 0.988446 |
9 | February 8th, 2019 | 0.102223 | 0.986932 |
10 | August 8th, 2019 | 0.100556 | 0.985445 |
11 | August 8th, 2019 | 100.000000 | 0.985445 |
The price derivative is calculated in the same way as above…
…and yields a similar result.
Translating QuantLib Python examples to C++
It’s easy enough to translate the Python code shown in this book into the corresponding C++ code. As an example, I’ll go through a bit of code from the notebook on instruments and pricing engines.
This line imports the QuantLib
module and provides a shorter alias
that can be used to qualify the classes and functions it contains.
The C++ equivalent would be:
In C++, however, I wouldn’t include the global quantlib.hpp
header,
which would inflate your compilation times; instead, you can include
the specific headers for the classes you’ll use.
Moreover, in C++ is not as discouraged as in Python to import the whole contents of a namespace in a source file, that is, to use
However, the above should not be used in header files; ask the nearest C++ guru if you’re not sure why.
The code above has a couple of caveats. The first line is easy enough
to translate; you’ll have to declare the type to the variable (or use
auto
if you’re compiling in C++11 mode). The second line is
trickier. To begin with, the syntax to call static methods differs in
Python and C++, so you’ll have to replace the dot before instance
by
a double colon (the same goes for the namespace qualifications).
Then, evaluationDate
is a property in Python but a method in C++; it
was changed in the Python module to be more idiomatic, since it’s not
that usual in Python to assign to the result of a method. Luckily, you
won’t find many such cases. The translated code is:
Next:
Again, you’ll have to declare the type of the variable. Furthermore,
the constructor of EuropeanOption
takes its arguments by pointer, or
more precisely, by boost::shared_ptr
. This is hidden in Python,
since there’s no concept of pointer in the language; the SWIG wrappers
take care of exporting boost::shared_ptr<T>
simply as T
.
The corresponding C++ code (note also the double colon in Option::Call
):
(A note: in the remainder of the example, I’ll omit the boost::
and
ql::
namespaces for brevity.)
Quotes, too, are stored and passed around as shared_ptr
instances;
this is the case for most polymorphic classes (when in doubt, you can
look at the C++ headers and check the signatures of the functions you
want to call). The above becomes:
Depending on what you need to do with them, the variables might also
be declared as shared_ptr<Quote>
. I used the above, since I’ll need
to call a method of SimpleQuote
in a later part of the code.
The Handle
template class couldn’t be exported as such, because
Python doesn’t have templates. Thus, the SWIG wrappers have to declare
separate classes QuoteHandle
, YieldTermStructureHandle
and so on.
In C++, you can go back to the original syntax.
Next,
turns into
and
into
So far, we’ve been calling constructors. Method invocation works the same in Python and C++, except that in C++ we might be calling methods through a pointer. Therefore,
where option
is an object in C++, becomes
whereas
since u
is a (smart) pointer, turns into
Of course, the direct translation I’ve been doing only applies to the
QuantLib code; I’m not able to point you to libraries that replace the
graphing functionality in matplotlib
, or the data-analysis
facilities in pandas
, or the parallel math functions in
numpy
. However, I hope that the above can still enable you to
extract value from this cookbook, even if you’re programming in C++.