Let me start with a brief theory review.
A cap (floor) is a series of caplets (floorlets) which are individual vanilla European call (put) options. The cap (floor) price is calculated as the sum of constituting caplet (floorlet) prices. The price of $i$-th caplet or floorlet at time $t$ in Bachelier's model is given by
$$caplet(t, T_{i-1}, T_i) = \delta P(t, T_i) \sigma \sqrt{T_{i-1}-t} (D\Phi(D)+\phi(D))$$
$$floorlet(t, T_{i-1}, T_i) = \delta P(t, T_i) \sigma \sqrt{T_{i-1}-t} (-D\Phi(-D)+\phi(-D))$$
where $\sigma$ is a year fraction between two consecutive settlement dates, $\Phi$ is the standard normal c.d.f., $\phi$ is the standard normal p.d.f. and
$$D = \frac{F(t, T_{i-1}, T_i) - K}{\sigma\sqrt{T_{i-1}-t}}.$$
I have the following quotes for IR Cap/Floor Options on some 3M interest rate index:
Code: Select all
Normal Volatilities
ATM: 136 for 1Y, 149 for 2Y, 152 for 3Y, 157 for 4Y, 159 for 5Y
floor 4%: 133 for 1Y, 112 for 2Y, 102 for 3Y, 101 for 4Y, 102 for 5Y
cap 6%: 122 for 1Y, 119 for 2Y, 121 for 3Y, 127 for 4Y, 132 for 5Y
cap 8%: 145 for 1Y, 160 for 2Y, 167 for 3Y, 172 for 4Y, 176 for 5Y
cap 10%: 249 for 1Y, 236 for 2Y, 232 for 3Y, 231 for 4Y, 231 for 5Y
Premiums
ATM: 26.6 for 1Y, 92 for 2Y, 174.9 for 3Y, 272.6 for 4Y, 380.6 for 5Y
floor 4%: no quote for 1Y, 0.2 for 2Y, 1.1 for 3Y, 4.1 for 4Y, 10.9 for 5Y
cap 6%: 129.6 for 1Y, 280.6 for 2Y, 412.3 for 3Y, 542.3 for 4Y, 671.7 for 5Y
cap 8%: 22.2 for 1Y, 75.1 for 2Y, 138.1 for 3Y, 213.1 for 4Y, 295.5 for 5Y
cap 10%: 6.7 for 1Y, 31.5 for 2Y, 68.3 for 3Y, 117.4 for 4Y, 174.9 for 5Y
Zero rates for 3M, 6M, 9M, ..., 5Y are
Code: Select all
[0.071663181766941, 0.07421116579842911, 0.07515555944994456, 0.07447034657263228, 0.07347466707239912, 0.07301565735296009, 0.07286963466253056, 0.07292506164754638, 0.07224556983162679, 0.07181546274181379, 0.07157029199702995, 0.07146726443762173, 0.07103903486603182, 0.07075083268643263, 0.07057716013025274, 0.07049901611937062, 0.0701250300167681, 0.06985067068642836, 0.06966203053575848, 0.06954807101703929]
Forwards for 3Mx6M, 6Mx9M, ..., 4Y9Mx5Y are
Code: Select all
[0.07481868191556984, 0.07428785267706228, 0.06855073150781532, 0.06467553924883429, 0.06477175485150344, 0.0648868630781223, 0.06502140056050099, 0.058305736453688084, 0.058444239770849116, 0.05859798163250485, 0.05876745125664318, 0.054265631333743514, 0.054436155515352525, 0.0546202465952117, 0.05481838396833272, 0.05003233173049626, 0.0502195580145548, 0.05041854614044894, 0.050629732723211696]
My results obtained via pricing individual caplets/floorlets with Bachelier formula and summing up resulting prices to get a cap/floor premium leads to results which are pretty different from quoted premiums. My calculated premiums are
Code: Select all
ATM 4% 6% 8% 10%
1 26.899812 0.057921 92.652153 9.677044 3.184801
2 95.231123 1.404018 165.968254 31.535102 14.483809
3 181.575727 8.403642 222.443255 58.762139 31.584824
4 283.844702 25.827419 281.042250 91.925337 54.914178
5 392.927461 57.831034 338.215497 128.584235 82.505987
Please take a look at my code below. I'm wondering whether there are any errors in my calculations causing such discrepancies. Note that I'm using 30/360 market convetion for the sake of simplicity therefore $\delta = T_i - T_{i-1} = 0.25$ in my Bachelier formula, however I doubt that just 30/360 instead of ACT/ACT alone would result in such a big difference between actual market quotes and my calculations. Note also that the first caplet (floorlet) isn't included in a cap (floor) since there is no optionality left, therefore $\sigma = 0$ and the premium is equal to zero.
Code: Select all
import numpy as np
import scipy as sc
from scipy.stats import norm
import pandas as pd
def strikeATM(maturity, ZeroRate3M, ZeroRates, capletMaturities):
DF = [1 / (1 + ZeroRate3M * 0.25)] + [1 / (1 + ZeroRates[i] * capletMaturities[i]) for i in range(len(capletMaturities))]
return (DF[0] - DF[capletMaturities.index(maturity) + 1]) / (0.25 * sum(DF[i] for i in range(1, capletMaturities.index(maturity) + 2)))
bp = 10000.00
strikes = [0.04, 0.06, 0.08, 0.10]
capVols = [[136.00, 149.00, 152.00, 157.00, 159.00], [133.00, 112.00, 102.00, 101.00, 102.00], [122.00, 119.00, 121.00, 127.00, 132.00], [145.00, 160.00, 167.00, 172.00, 176.00], [249.00, 236.00, 232.00, 231.00, 231.00]]
capVols = [[vol/10000.00 for vol in subl] for subl in capVols]
capMaturities = [1, 2, 3, 4, 5]
capletMaturities = [0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 3.25, 3.5, 3.75, 4.0, 4.25, 4.5, 4.75, 5.0]
Forwards = [0.07481868191556984, 0.07428785267706228, 0.06855073150781532, 0.06467553924883429, 0.06477175485150344, 0.0648868630781223, 0.06502140056050099, 0.058305736453688084, 0.058444239770849116, 0.05859798163250485, 0.05876745125664318, 0.054265631333743514, 0.054436155515352525, 0.0546202465952117, 0.05481838396833272, 0.05003233173049626, 0.0502195580145548, 0.05041854614044894, 0.050629732723211696]
ZeroRate3M = 0.071663181766941
ZeroRates = [0.07421116579842911, 0.07515555944994456, 0.07447034657263228, 0.07347466707239912, 0.07301565735296009, 0.07286963466253056, 0.07292506164754638, 0.07224556983162679, 0.07181546274181379, 0.07157029199702995, 0.07146726443762173, 0.07103903486603182, 0.07075083268643263, 0.07057716013025274, 0.07049901611937062, 0.0701250300167681, 0.06985067068642836, 0.06966203053575848, 0.06954807101703929]
def bachelier_formula(k, f, t, v, r, cp='call'):
d1 = (f - k) / (v * (t - 0.25)**0.5)
cp_sign = {'call': 1., 'put': -1.}[cp]
pv = 0.25 * np.exp(-r*t) * (
cp_sign * (f - k) * norm.cdf(cp_sign * d1) +
v * ((t - 0.25) / (2 * np.pi))**0.5 * np.exp(-d1**2 / 2))
return pv
def flatCapPrice(strike, vol, first_settlement, maturity, capletMaturities, ZeroRates, Forwards, style = 'call'):
premium = 0
for settlement in range(capletMaturities.index(first_settlement), capletMaturities.index(maturity) + 1):
premium += bachelier_formula(strike, Forwards[settlement], capletMaturities[settlement], vol, ZeroRates[settlement], style)
return premium
premiums = []
premium_ATM = []
for year in capMaturities:
premium_ATM.append(bp * flatCapPrice(strikeATM(year, ZeroRate3M, ZeroRates, capletMaturities), capVols[0][year-1], 0.5, year, capletMaturities, ZeroRates, Forwards, 'call'))
premiums.append(premium_ATM)
for strike in strikes:
if strike == 0.04:
style = 'put'
else:
style = 'call'
premium_strike = []
for year in capMaturities:
premium_strike.append(bp * flatCapPrice(strike, capVols[strikes.index(strike)+1][year-1], 0.5, year, capletMaturities, ZeroRates, Forwards, style))
premiums.append(premium_strike)
premiums = pd.DataFrame(premiums).transpose()
premiums.columns = ['ATM', '4%', '6%', '8%', '10%']
premiums.index = [year for year in range(1, 5 + 1)]
print(premiums)
Please let me know if I should provide any further clarifications regarding the code or methodology. Any help will be appreciated.