The BlackScholes model in QuantLib
Hello again.
Today’s post was originally published as an article in the May 2023 issue of Wilmott Magazine, which was dedicated to the 50th anniversary of the BlackScholes model. It is reposted here without modifications, except that the code is here interleaved with the text instead of in a separate box.
The full source file is available on my Tutorial page, together with code from other articles and, when available, either the articles themselves or corresponding blog posts like this one.
Subscribe to my Substack to receive my posts in your inbox, or follow me on Twitter or LinkedIn if you want to be notified of new posts, or subscribe via RSS if you’re the tech type: the buttons for all that are in the footer. Also, I’m available for training, both online and (when possible) onsite: visit my Training page for more information.
The BlackScholes model in QuantLib
I’m usually fine with changing plans, mostly because I don’t believe them in the first place; I’ve been long known to quote the old adage attributed to Eisenhower that plans are worthless but planning is everything. I won’t confirm nor deny that the reason was to try and avoid giving estimates on projects.
Which is a longwinded way to say that I had planned another column for this month, but I happily jumped on the bandwagon when I was told by our esteemed editor that this issue of the magazine would celebrate the 50th anniversary of the BlackScholes model. The model is provided in QuantLib, of course, and after 50 years from publication it is still pretty useful—a fact that bodes well for all those who, like myself, are even older than that.
BlackScholes implementations in the library
The BlackScholes model was one of the first we added to QuantLib because European options were a textbook example we wanted to have when we started; and later on, we kept coming back to it one way or another as a benchmark. Adding a Monte Carlo framework to the library? Let’s price European options under BlackScholes and check that prices are right. Adding a finitedifference framework? The same—twice, because we revamped finite differences at some point. Coding trees? You guessed it, let’s try a BlackScholes binomial tree and check European option prices.
So, how is the BlackScholes model used in the library? Let me count the ways.
And let’s get the includes out of the way first:
#include <ql/exercise.hpp>
#include <ql/instruments/europeanoption.hpp>
#include <ql/pricingengines/vanilla/analyticdividendeuropeanengine.hpp>
#include <ql/pricingengines/vanilla/analyticeuropeanengine.hpp>
#include <ql/pricingengines/vanilla/binomialengine.hpp>
#include <ql/pricingengines/vanilla/fdblackscholesvanillaengine.hpp>
#include <ql/pricingengines/vanilla/mceuropeanengine.hpp>
#include <ql/quotes/simplequote.hpp>
#include <ql/settings.hpp>
#include <ql/termstructures/volatility/equityfx/blackconstantvol.hpp>
#include <ql/termstructures/yield/flatforward.hpp>
#include <ql/time/calendars/target.hpp>
#include <ql/time/daycounters/actual360.hpp>
#include <iostream>
Analytic formulas
Of course, as I mentioned, the library can price European options using the BlackScholes formula.
The setup is a bit more cumbersome than you might expect; for example, we need a fullfledged riskfree rate curve and volatility surface (even if flat, as in this case), from which the engine will pick the values corresponding to the maturity and strike of the option.
int main() {
using namespace QuantLib;
auto valueDate = Date(3, May, 2023);
Settings::instance().evaluationDate() = valueDate;
auto strike = 95.0;
auto maturityDate = Date(3, August, 2023);
EuropeanOption option(
ext::make_shared<PlainVanillaPayoff>(Option::Call,
strike),
ext::make_shared<EuropeanExercise>(maturityDate));
auto underlyingValue =
ext::make_shared<SimpleQuote>(96.4);
auto riskFreeCurve = ext::make_shared<FlatForward>(
valueDate, 0.01, Actual360());
auto volSurface = ext::make_shared<BlackConstantVol>(
valueDate, TARGET(), 0.15, Actual360());
auto bsProcess = ext::make_shared<BlackScholesProcess>(
Handle<Quote>(underlyingValue),
Handle<YieldTermStructure>(riskFreeCurve),
Handle<BlackVolTermStructure>(volSurface));
option.setPricingEngine(
ext::make_shared<AnalyticEuropeanEngine>(bsProcess));
std::cout << option.NPV() << std::endl; // prints 3.7858
As you see, we’re a long way from a simple function taking the inputs
of the BlackScholes formula and returning the resulting value. As I
tried to show in previous columns, though, this setup gives us the
possibility to perform operations such as changing or shifting the
curves and have the value of the option change accordingly. And if
you were to dig into the code of the engine, of course, you would
ultimately find a class, BlackCalculator
, that implements the
formula based on those simpler inputs.
Now, the acute reader—that legendary creature—might have noticed
that we’re using a class called BlackScholesProcess
, not
BlackScholesModel
. This is a historical artifact; I’ll get to it
later. What I’ll quickly note, instead, is that BlackScholesProcess
is just one of a family of classes that share a common underlying
implementation; the library implements the BlackScholes process as
the particular case of a generalized BlackScholesMerton process
where the dividend yield is null. Other specific cases of the same
generic process include, for instance, the Black process and the
GarmanKohlhagen model.
To close this section, I’ll mention that the BlackScholes model is
the base for a number of other analytic formulas implemented in the
library. The same AnalyticEuropeanEngine
class used above can also
manage cashornothing or assetornothing payoffs; the
AnalyticDividendEuropeanEngine
can accommodate discrete dividends,
as also shown in the listing; and the library provides analytic
engines for a baker’s dozen of more or less exotic instruments such as
Asian options of various kinds, barrier and double barrier options,
lookback options, chooser options, exchange options, and others that
you can find in the store of that old friend of Wilmott magazine, the
Collector.
auto dividends =
DividendVector({Date(24, May, 2023)}, {1.5});
option.setPricingEngine(
ext::make_shared<AnalyticDividendEuropeanEngine>(
bsProcess, dividends));
std::cout << option.NPV() << std::endl; // prints 2.94026
Finitedifferences methods
The model was also plugged in our finitedifference frameworks;
plural, since the original implementation is currently being phased
out by a newer one. In the listing, you can see the corresponding
engine being used in its simpler form; it is also possible to
customize parameters such as, for instance, the number of points in
the finitedifference grid, or other finer points of the method. In
this case, the BlackScholes model is coded into classes such as
FdmBlackScholesOp
and FdmBlackScholesMesher
: respectively, the
finitedifferences operator used during the calculation and the helper
object used to set up the grid over which the calculation will be
performed.
option.setPricingEngine(
ext::make_shared<FdBlackScholesVanillaEngine>(
bsProcess));
std::cout << option.NPV() << std::endl; // prints 3.78768
As for the analytic case, this is not the only way the model is used. Just to mention a few extensions, the same engine can price American options; other engines based on the same model modify the calculation so that it can be used to price, for instance, barrier options; and other engines exist that couple the BlackScholes model for the value of the underlying and, say, the HullWhite model for rates.
Monte Carlo simulations
Looking at the next bit of listing, you’ll see next that it
instantiates—possibly with an unfamiliar syntax—a Monte Carlo
engine. Like for finite differences, the library provides a general
framework in which specific models can be used. In this case, the
object providing the specialized behavior for the BlackScholes model
is the same instance of BlackScholesProcess
we’ve been using all
along. As the implementation of a stochastic process, it has methods
that the Monte Carlo framework can call to obtain the information it
needs during path generation, such as the drift and diffusion terms at
any given point in the path.
option.setPricingEngine(
MakeMCEuropeanEngine<LowDiscrepancy>(bsProcess)
.withAntitheticVariate()
.withSteps(1)
.withSamples(32767));
std::cout << option.NPV() << std::endl; // prints 3.78531
Again, this is the most basic usage of the framework. The same process can be used to price other kinds of options (Asian ones, for instance), and multiple BlackScholes processes can be correlated and used in a higherdimensional simulation to price, say, basket options.
Binomial trees
Finally, the next bit of code instantiates an engine based on an underlying
binomial tree. Again, the framework is generic and the model is
plugged in via a specific BlackScholesLattice
class. There are a
few available choices for the way the tree is built; in this listing,
I’m using a method developed by the late Mark Joshi, whom I like to
remember as a contributor to our library.
auto timeSteps = 100;
option.setPricingEngine(
ext::make_shared<BinomialVanillaEngine<Joshi4>>(
bsProcess, timeSteps));
std::cout << option.NPV() << std::endl; // prints 3.75728
}
As you came to expect by now, the BlackScholes process can be used in a few different ways in this framework. The same binomial engine shown in the listing can price American options, for instance; and I’ll also mention that a binomial BlackScholes tree also underlies the implementation of a TsiveriotisFernandes engine for pricing convertible bonds—though that method is known to have drawbacks; I’d be glad to have contributions in that area covering more modern pricing methods.
What do we get out of this?
Of course, the above is just a quick catalog of ways the BlackScholes model is used, and was meant to showcase its lasting impact; I could hardly show any details in 1500 words. For a whole lot of those details, you can have a look at Implementing QuantLib where all those framework are described in full. However, I do have some parting remarks and some small hope that they might be food for thought for a quantitative developer.
According to Bjarne Stroustrup, there are only two kinds of languages: the ones people complain about and the ones nobody uses. Well, si parva licet componere magnis, QuantLib is a library that people, myself included, do complain about. If you get around to read Implementing QuantLib, you’ll see that I criticized plenty of things, and our implementation of the BlackScholes process is one of those. Why, you say?
Geological layers
Good question. As I mentioned at the beginning of the column, the BlackScholes model was one of the first things we implemented, and this might make it prone to technical debt. The early phases of a project, by definition, is when we know the least about it—in the case of a library such as QuantLib, for instance, about how it will be used and in which way it will grow—and this will lead to nonoptimal choices.
Later on, when we understand things better, we should go back to the code and change it so that it reflects our new understanding. If we don’t, we make it more awkward to build further code on top of the existing one. This, and not some sense of purity, is why we talk of paying back technical debt. Of course, this doesn’t always happen, and for all kinds of good reasons.^{1}
An example? We initially wrote the BlackCalculator
class so that it
doesn’t take the usual parameters such as the volatility and the
riskfree rate; instead, it takes equivalent but more unusual ones,
such as the standard deviation of the expected distribution and the
discount factor. It probably looked like a neat idea at the time, but
it made it a bit harder to understand.
Another example? The generalized BlackScholes process is a jack of too many trades. It started as something to use in Monte Carlo simulations; but since it was already laying there, we got lazy and started using it as a convenient way to pass around the bunch of data it contains. The generalization itself has some tricky corner cases: search for “quantlib notebooks rho for the black process” and you’ll find a YouTube video in which I go over one such case.
How would I do it now?
Well, I would write analytic formulas in the way people, myself
included, expect so they are easier to read and verify. Or we could
write a general BlackScholesModel
class, and it could be able to
create a leaner, more efficient process for a given Monte Carlo
simulation.
Now, you could correctly point out that we’re dealing with software, not masonry, so we could change it—and I would agree. The tricky part would be to do it in a way that doesn’t break code that people have written on top of QuantLib. This usually involves writing new functionality as additions, deprecating the old functionality, and waiting a whole bunch of releases before finally removing it. In the case of something as widely used as the BlackScholes process, though, this might cause more work than it’s worth, for QuantLib developers and users alike.
So, as usual, I don’t have a plan. However, you’re welcome to have a look at the library code and try your hand at contributing some changes. I’ll be happy to look at your pull requests. See you next time!

As I write this, in early February 2023, the hot bit of news is that Marie Kondo now has three kids and no longer tidies her home like she used to. Good for her, I say. ↩