A tutorial on valuing caps and floors using QuantLib Python.
In this post, I will walk you through a simple example of valuing caps. I want to talk about two specific cases:
Caps, as you might know, can be valued as a sum of caplets. The value of each caplet is determined by the Black formula. In practice, each caplet would have a different volatility. Meaning, a caplet that is in the near term can have a different volotility profile compared to the caplet that is far away in tenor. Similarly caplet volatilities differ with the strike as well.
import QuantLib as ql
calc_date = ql.Date(14, 6, 2016) ql.Settings.instance().evaluationDate = calc_date
Let us start by constructing different componets required in valuing the caps. The components that we would need are:
For simplicity, we will construct only one interest rate term structure here, and assume that the discounting and the floating leg is referenced by the same. Below the term structure of interest rates is constructed from a set of zero rates.
dates = [ql.Date(14,6,2016), ql.Date(14,9,2016), ql.Date(14,12,2016), ql.Date(14,6,2017), ql.Date(14,6,2019), ql.Date(14,6,2021), ql.Date(15,6,2026), ql.Date(16,6,2031), ql.Date(16,6,2036), ql.Date(14,6,2046) ] yields = [0.000000, 0.006616, 0.007049, 0.007795, 0.009599, 0.011203, 0.015068, 0.017583, 0.018998, 0.020080] day_count = ql.ActualActual() calendar = ql.UnitedStates() interpolation = ql.Linear() compounding = ql.Compounded compounding_frequency = ql.Annual term_structure = ql.ZeroCurve(dates, yields, day_count, calendar, interpolation, compounding, compounding_frequency) ts_handle = ql.YieldTermStructureHandle(term_structure)
As a next step, lets construct the cap itself. In order to do that, we start by constructing the
Schedule object to project the cashflow dates.
start_date = ql.Date(14, 6, 2016) end_date = ql.Date(14, 6 , 2026) period = ql.Period(3, ql.Months) calendar = ql.UnitedStates() buss_convention = ql.ModifiedFollowing rule = ql.DateGeneration.Forward end_of_month = False schedule = ql.Schedule(start_date, end_date, period, calendar, buss_convention, buss_convention, rule, end_of_month)
Now that we have the schedule, we construct the
USDLibor index. Below, you can see that I use
addFixing method to provide a fixing date for June 10, 2016. According the schedule constructed, the start date of the cap is June 14, 2016, and there is a 2 bussiness day settlement lag (meaning June 10 reference date) embedded in the
USDLibor definition. So in order to set the rate for the accrual period, the rate is obtained from the fixing data provided. For all future dates, the libor rates are automatically inferred using the forward rates provided by the given interest rate term structure.
ibor_index = ql.USDLibor(ql.Period(3, ql.Months), ts_handle) ibor_index.addFixing(ql.Date(10,6,2016), 0.0065560) ibor_leg = ql.IborLeg(, schedule, ibor_index)
Now that we have all the required pieces, the
Cap can be constructed by passing the
ibor_leg and the
strike information. Constructing a floot is done through the
Floor class. The
BlackCapFloorEngine can be used to price the cap with constant volatility as shown below.
strike = 0.02 cap = ql.Cap(ibor_leg, [strike]) vols = ql.QuoteHandle(ql.SimpleQuote(0.547295)) engine = ql.BlackCapFloorEngine(ts_handle, vols) cap.setPricingEngine(engine) print cap.NPV()
In the above exercise, we used a constant volatility value. In practice, one needs to strip the market quoted capfloor volatilities to infer the volatility of each and every caplet.
QuantLib provides excellent tools in order to do that. Let us assume the following dummy data represents the volatility surface quoted by the market. I have the various
expiries, and the volatility quotes in percentage format. I take the raw data and create a
Matrix in order to construct the volatility surface.
strikes = [0.01,0.015, 0.02] expiries = [ql.Period(i, ql.Years) for i in range(1,11)] + [ql.Period(12, ql.Years)] vols = ql.Matrix(len(expiries), len(strikes)) data = [[47.27, 55.47, 64.07, 70.14, 72.13, 69.41, 72.15, 67.28, 66.08, 68.64, 65.83], [46.65,54.15,61.47,65.53,66.28,62.83,64.42,60.05,58.71,60.35,55.91], [46.6,52.65,59.32,62.05,62.0,58.09,59.03,55.0,53.59,54.74,49.54] ] for i in range(vols.rows()): for j in range(vols.columns()): vols[i][j] = data[j][i]/100.0
CapFloorTermVolSurface offers a way to store the capfloor volatilities. These are however
CapFloor volatilities, and not the volatilities of the individual options.
calendar = ql.UnitedStates() bdc = ql.ModifiedFollowing daycount = ql.Actual365Fixed() settlement_days = 2 capfloor_vol = ql.CapFloorTermVolSurface(settlement_days, calendar, bdc, expiries, strikes, vols, daycount)
OptionletStripper1 class lets you to strip the individual caplet/floorlet volatilities from the capfloor volatilities. We have to 'jump' some hoops here to make it useful for pricing. The
OptionletStripper1 class does not allow you to be consumed directly by a pricing engine. The
StrippedOptionletAdapter takes the stripped optionlet volatilities, and creates a term structure of optionlet volatilities. We then wrap that into a handle using
optionlet_surf = ql.OptionletStripper1(capfloor_vol, ibor_index) ovs_handle = ql.OptionletVolatilityStructureHandle( ql.StrippedOptionletAdapter(optionlet_surf) )
Below, we visulaize the capfloor volatility surface, and the optionlet volatility suface for a fixed strike.
import matplotlib.pyplot as plt import numpy as np %matplotlib inline
tenors = np.arange(0,10,0.25) strike = 0.015 capfloor_vols = [capfloor_vol.volatility(t, strike) for t in tenors] opionlet_vols = [ovs_handle.volatility(t, strike) for t in tenors] plt.plot(tenors, capfloor_vols, "--", label="CapFloor Vols") plt.plot(tenors, opionlet_vols,"-", label="Optionlet Vols") plt.legend(bbox_to_anchor=(0.5, 0.25))
<matplotlib.legend.Legend at 0x894efd0>
BlackCapFloorEngine can accept the optionlet volatility surface in order to price the caps or floors.
engine2 = ql.BlackCapFloorEngine(ts_handle, ovs_handle) cap.setPricingEngine(engine2) print cap.NPV()
QuantLib C++ class allow for one to view the projected cashflows in terms of individual caplets. I just realized that the python extension does not have this feature added to it. Will give a PR one of these days and update this post.
Hope you find this useful.