Serving the Quantitative Finance Community

 
Hasek
Topic Author
Posts: 9
Joined: October 2nd, 2021, 9:53 am

Interest Rates Options: can't reproduce market premiums given volatilities and discount curve

November 15th, 2021, 3:49 pm

I have a range of premiums and normal (Bachelier) volatilites quotes across several strikes and maturities for caps and floors and trying to figure out why I cannot reprice them given the discount curve data.

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:
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

[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

[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
          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.

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.
 
User avatar
bearish
Posts: 5188
Joined: February 3rd, 2011, 2:19 pm

Re: Interest Rates Options: can't reproduce market premiums given volatilities and discount curve

November 15th, 2021, 10:36 pm

As best as I can tell, your forward rates are badly wrong. That’s good news, since that should be easy to fix.
 
Hasek
Topic Author
Posts: 9
Joined: October 2nd, 2021, 9:53 am

Re: Interest Rates Options: can't reproduce market premiums given volatilities and discount curve

November 16th, 2021, 9:36 am

As best as I can tell, your forward rates are badly wrong. That’s good news, since that should be easy to fix.
What made you think so? These forwards were calculated from the given zero rates via the following formula
$$F(t_1, t_2) = \frac{1}{t_2-t_1}\left(\frac{1+R(t_2)t_2}{1+R(t_1)t_1}-1\right)$$
I may attach the code if needed.

The curve is indeed inverted -- this is one of the emerging markets right now.
 
User avatar
bearish
Posts: 5188
Joined: February 3rd, 2011, 2:19 pm

Re: Interest Rates Options: can't reproduce market premiums given volatilities and discount curve

November 18th, 2021, 11:22 pm

Unless your market is a very peculiar one, that is not the right formula. You are implicitly assuming that your input rates are all uncompounded, or equivalently, that they each have a compounding frequency equal to their tenor. That is a money market convention which, to the best of my knowledge, is never used for maturities beyond a year.
 
Hasek
Topic Author
Posts: 9
Joined: October 2nd, 2021, 9:53 am

Re: Interest Rates Options: can't reproduce market premiums given volatilities and discount curve

November 19th, 2021, 11:14 am

Which formula would you say is a standard market convention? Should one use the continuous compounding?
$$F(t_1,t_2) = \frac{1}{t_2-t_1}\left(\ln P(t_1) -\ln P(t_2)\right) = \frac{R(t_2)t_2 - R(t_1)t_1}{t_2 - t_1}$$
 
User avatar
bearish
Posts: 5188
Joined: February 3rd, 2011, 2:19 pm

Re: Interest Rates Options: can't reproduce market premiums given volatilities and discount curve

November 19th, 2021, 5:07 pm

There are two parts to the answer, both having to do with compounding conventions. First, if you have a set of discount factors, you convert them into forward 3M money market (in the spirit of Libor) by [$] F(t,T_{i-1}, T_i) =( \frac{ P(t,T_{i-1})}{P(t,T_i)} -1 )/\delta [$], using your original notation. The second part consists of converting your zero rates into discount factors. In order to do this, you need to know the compounding frequency and daycount convention in which they are given. E.g., if they are continuously compounded Act/Act you will have [$] P(t,T)=e^{-r_{t,T} (T-t)} [$].