Pricing Caps
In this example we will explore how to: 1. Price caps by settings a constant volatility and only using a single curve for discounting and forecasting. 2. Price caps by using two curves, one for forecasting and one for discounting. 3. Price caps by having an input volatility surface.
[1]:
import QuantLib as ql
import pandas as pd
We initialize by setting the valuation date and defining our forecasting and discounting curve to be used in pricing.
[2]:
# Begin by setting the valuation date of which the cap and the floor should be priced at
ql.Settings.instance().evaluationDate = ql.Date(1, 1, 2022)
# Then we initialize the curve we want to use for discounting and forecasting
discount_factors = [1, 0.965, 0.94] # discount factors
dates = [
ql.Date(1, 1, 2022),
ql.Date(1, 1, 2023),
ql.Date(1, 1, 2024),
] # maturity dates of the discount factors
day_counter = ql.Actual360()
# Note that we will not strip a curve here, but simply use the discount factors and the dates defined above
# By default QuantLib DiscountCurve will log linearly interpolate between the points.
discount_curve = ql.DiscountCurve(dates, discount_factors, day_counter)
# The curve will note be linked in case we want to update the quotes later on
discount_handle = ql.YieldTermStructureHandle(discount_curve)
The next step involves creating an schedule of dates for which the optionlets of the caps will mature at.
[3]:
start_date = ql.Date(1, 1, 2022)
end_date = start_date + ql.Period(12, ql.Months)
# We define the schedule of the cap and floor
schedule = ql.Schedule(
start_date, # Start date of payments
end_date, # End date of payments
ql.Period(3, ql.Months), # frequency of payments
ql.Sweden(), # Calendar for adjusting for holidays
ql.ModifiedFollowing, # Business convention for adjusting for holidays
ql.ModifiedFollowing, # Business convention for adjusting for holidays
ql.DateGeneration.Backward, # Date generation rule for generating the schedule
False, # End of month rule
)
# Create a custom index to track the payments correctly, specifically fixing days.
custom_discount_index= ql.IborIndex(
"MyIndex",
ql.Period("3m"),
0,
ql.SEKCurrency(),
ql.Sweden(),
ql.ModifiedFollowing,
False,
ql.Actual360(),
discount_handle,
)
The last step is to define the pricing engine to use for pricing. We can choose between:
BlackCapFloorEngine
BachelierEngine
AnalyticCapFloorEngine
TreeCapFloorEngine
In this example we will precede with BlackCapFloorEngine.
[4]:
# As you have noted by now, the pricing of caps and floors involves creating a floating leg
ibor_leg_discount = ql.IborLeg([1e6], schedule, custom_discount_index)
strike = [0.025]
cap_discount = ql.Cap(ibor_leg_discount, strike)
# The final step is to define a volatility surface, we will use a constant volatility for simplicity
volatility = ql.QuoteHandle(ql.SimpleQuote(0.5))
# Input our discounting and forecasting curve together with our volatility surface to the engine
engine = ql.BlackCapFloorEngine(discount_handle, volatility)
cap_discount.setPricingEngine(engine)
print(cap_discount.NPV())
10831.583434218297
At last we want to show our results of the seperate optionlets.
[5]:
schedule_dates = schedule.dates()
display_result = lambda _ : pd.DataFrame({
'price': _.optionletsPrice(),
'discount_factor': _.optionletsDiscountFactor(),
'cap_rate': _.capRates(),
'atm_forward': _.optionletsAtmForward(),
'std_dev': _.optionletsStdDev(),
'accrual_start': schedule_dates[:-1],
'accrual_end' : schedule_dates[1:]
})
display_result(cap_discount)
[5]:
price | discount_factor | cap_rate | atm_forward | std_dev | accrual_start | accrual_end | |
---|---|---|---|---|---|---|---|
0 | 2493.450264 | 0.991254 | 0.025 | 0.035290 | 0.037012 | January 3rd, 2022 | April 1st, 2022 |
1 | 2625.359083 | 0.982488 | 0.025 | 0.035296 | 0.248282 | April 1st, 2022 | July 1st, 2022 |
2 | 2846.309041 | 0.973515 | 0.025 | 0.035301 | 0.352097 | July 1st, 2022 | October 3rd, 2022 |
3 | 2866.465047 | 0.964931 | 0.025 | 0.035193 | 0.434000 | October 3rd, 2022 | January 2nd, 2023 |
Considering that we have used only one curve as discounting and forecasting, we will now add a second curve for forecasting.
[6]:
ql.Settings.instance().evaluationDate = ql.Date(1, 1, 2022)
# Similiar to the discount curve we declared previously
forward_rates = [0.04, 0.05, 0.06]
forward_curve = ql.ForwardCurve(dates, forward_rates, day_counter)
forward_handle = ql.YieldTermStructureHandle(forward_curve)
[7]:
# Create a new index that uses the forward curve for forecasting
custom_forward_index= ql.IborIndex(
"MyIndex",
ql.Period("3m"),
0,
ql.SEKCurrency(),
ql.Sweden(),
ql.ModifiedFollowing,
False,
ql.Actual360(),
forward_handle, # Previously was set to discount_handle
)
[8]:
# Define a new ibor_leg & cap that uses the new index with forward estimation
ibor_leg_forward = ql.IborLeg([1e6], schedule, custom_forward_index)
cap_forward = ql.Cap(ibor_leg_forward, strike)
# Input our discounting and forecasting curve together with our volatility surface to the engine
engine_forward = ql.BlackCapFloorEngine(discount_handle, volatility)
cap_forward.setPricingEngine(engine_forward)
print(cap_forward.NPV())
25171.79621353972
[9]:
schedule_dates = schedule.dates()
display_result(cap_forward)
[9]:
price | discount_factor | cap_rate | atm_forward | std_dev | accrual_start | accrual_end | |
---|---|---|---|---|---|---|---|
0 | 6132.002083 | 0.991254 | 0.025 | 0.050307 | 0.037012 | January 3rd, 2022 | April 1st, 2022 |
1 | 6289.142138 | 0.982488 | 0.025 | 0.050317 | 0.248282 | April 1st, 2022 | July 1st, 2022 |
2 | 6465.774497 | 0.973515 | 0.025 | 0.050328 | 0.352097 | July 1st, 2022 | October 3rd, 2022 |
3 | 6284.877495 | 0.964931 | 0.025 | 0.050429 | 0.434000 | October 3rd, 2022 | January 2nd, 2023 |
The last step is to consider to have a full volatility surface instead of a constant one.
[38]:
# Set the settlement day of the volatility surface
settlementDays = 0
# Define the expiries for the volatility surface
expiries = [ql.Period("3M"), ql.Period("6M"), ql.Period("9M"), ql.Period("1Y")]
# Define the strikes for the volatility surface
strikes = [0.010, 0.025, 0.03]
# Define the market quotes for the volatility surface
black_volatility = [[0.98, 0.792, 0.6873], [0.9301, 0.7401, 0.6403], [0.7926, 0.6424, 0.5602], [0.7126, 0.6024, 0.4902]]
# Create a new volatility surface
volatility_surface = ql.CapFloorTermVolSurface(
settlementDays,
ql.Sweden(),
ql.ModifiedFollowing,
expiries,
strikes,
black_volatility,
day_counter,
)
# Strip the volatility surface for optionlets (caplets) as the input is based on caps
optionlet_surf = ql.OptionletStripper1(volatility_surface, custom_forward_index)
# Call strippedOptionletAdapter to create a handle for the volatility surface
ovs_handle = ql.OptionletVolatilityStructureHandle(
ql.StrippedOptionletAdapter(optionlet_surf)
)
cap_volatility = ql.Cap(ibor_leg_forward, strike)
# Input our discounting and forecasting curve together with our volatility surface to the engine
engine_volatility = ql.BlackCapFloorEngine(discount_handle, ovs_handle)
cap_volatility.setPricingEngine(engine_volatility)
print(cap_volatility.NPV())
25340.288918668186
[39]:
display_result(cap_volatility)
[39]:
price | discount_factor | cap_rate | atm_forward | std_dev | accrual_start | accrual_end | |
---|---|---|---|---|---|---|---|
0 | 6132.002083 | 0.991254 | 0.025 | 0.050307 | 0.000000 | January 3rd, 2022 | April 1st, 2022 |
1 | 6325.268247 | 0.982488 | 0.025 | 0.050317 | 0.372127 | April 1st, 2022 | July 1st, 2022 |
2 | 6526.008974 | 0.973515 | 0.025 | 0.050328 | 0.434983 | July 1st, 2022 | October 3rd, 2022 |
3 | 6357.009614 | 0.964931 | 0.025 | 0.050429 | 0.500385 | October 3rd, 2022 | January 2nd, 2023 |