QuantLib: setting up QuantLib-Python and pricing an option

It has been a while since my last post series, today is the first post in a mini-series on the fantastic QuantLib-Python library, where I will present an investigation of various instruments, pricing models and calibration choices, along with the code to generate them yourselves. My thanks to everyone in the QuantLib team who have been supporting and extending this library now for most of 20 years. I’m going to assume a working knowledge of python and that you have a Python 3 setup installed locally. Later posts will also assume some familiarity with the wonderful Pandas library (and possibly numpy and matplotlib…)

First thing you will to do is install the library, which should be as easy as typing pip install QuantLib==1.19 in the command line (this is the current latest version, released July 20th 2020).

QuantLib does a great job of separating out all of the components of a pricing problem, and also of helping to deal with annoying things like interpolation, day count conventions, calibration of discount curves and so on. A typical pricing script will have a few pieces:

  1. Setting up the world state, with asset prices, curves, dates and calendars etc.
  2. Choosing a pricing engine for your product which determines how the pricing is done (eg. Monte Carlo, Analytic…)
  3. Setting up the instrument to price (eg. payment dates, strike, contract type)
  4. Linking the engine and the security, and pricing!

In this first post, I’m going to illustrate the process by showing how to price a fixed coupon bond and a vanilla option in a world with constant rates and a single asset following a constant vol BS process. In future posts we’ll look at some more exciting applications of QuantLib.

First, we set up the world state:

import QuantLib as ql

# Setting up our universe with a single asset and rates curves
spot = 100
vol = 0.1
rate, divi_rate = 0.01, 0.0
today = ql.Date(2, 7, 2020)
day_count = ql.Actual365Fixed()
calendar = ql.NullCalendar()

volatility = ql.BlackConstantVol(today, calendar, vol, day_count)
riskFreeCurve = ql.FlatForward(today, rate, day_count)
diviCurve = ql.FlatForward(today, divi_rate, day_count)

flat_ts = ql.YieldTermStructureHandle(riskFreeCurve)
dividend_ts = ql.YieldTermStructureHandle(riskFreeCurve)
flat_vol = ql.BlackVolTermStructureHandle(volatility)

Hopefully this should all be quite self-explanatory – we use the ql.BlackConstantVol and ql.FlatForward classes to create constant vol and constant rates curves. Perhaps the only slightly mysterious lines are ql.YieldTermStructureHandle and ql.BlackVolTermStructureHandle – we will see classes of the Handle again and again, they act as packaging for the underlying curves which might be of different types depending upon how they were calibrated (eg. from ZCBs? from rates or swap curves?), and give the curves a common interface for the pricer to use later.

Now, let’s set up a bond and bond pricing engine (just an analytic discount engine), and price the bond:

# Create a bond instrument
start_date, end_date = ql.Date(1, 1, 2016), ql.Date(1, 1, 2022)
coupons = [0.02]
coupon_freq = ql.Period(ql.Semiannual)
settlement_days = 0
face_value = 100

bond_instrument = ql.FixedRateBond(settlement_days, calendar, face_value, start_date, end_date, coupon_freq, coupons, day_count)

# Put the yield curve into a curve handler, pass to a bond pricing engine
bond_engine = ql.DiscountingBondEngine(flat_ts)

# Pair the bond engine and the bond, and price!
bond_instrument.setPricingEngine(bond_engine)

print(bond_instrument.NPV())

print()
print("Cashflows remaining: ")
for c in bond_instrument.cashflows():
    print('%20s %12f' % (c.date(), c.amount()))

Here is what I see… the analytic price of the bond’s remaining cashflows (plus accrued interest, if relevant), and also the cashflow termsheet for the bond:

Price and cashflows for a simple bond

And now I calculate the price and delta for a vanilla call option in the same way, this time using an analytic BS pricer:

expiry_date = ql.Date(1, 7, 2021)
strike = 100

# Set up the option payoff
option = ql.EuropeanOption(ql.PlainVanillaPayoff(ql.Option.Call, strike), ql.EuropeanExercise(expiry_date))

# Run pricing
process = ql.BlackScholesProcess(ql.QuoteHandle(ql.SimpleQuote(spot)), flat_ts, flat_vol)
engine = ql.AnalyticEuropeanEngine(process)
option.setPricingEngine(engine)

print(option.NPV())
print(option.delta())

Which should create the following prices:

Price and delta for vanilla call