|
From: Dan V. <dan...@gm...> - 2022-10-06 19:05:04
|
Hi,
I am running into an issue where quantlib outputs different thetas whether
i price with a blackvariancesurface or with the constant vol extracted from
the surface.
I have a vol surface from the market on the SPX index. Not all
strikes/maturities are populated. In addition, there could potentially be
arbitrages in short maturities since we have lots of daily options and I am
not correcting yet for arbitrages.
Yet when building a BlackVarianceSurface with this data and price a vanilla
option, I expect that option to be priced with a constant vol interpolated
from the grid.
When I pass the surface object to the pricer I am getting a very different
theta (and therefore breakeven vol) from simply passing the constant vol
taken from the surface.
Could you help me understand exactly how QLIB handles the surface and how
is theta computed ?
Thank you very very much.
Here's some sample code that compares a constant vol with a surface vol
pricing. Notice how the theta changes between the 2 cases leading to a very
different break even vol.
import pandas as pd, QuantLib as ql, math, numpy as npfrom datetime
import datetime
def from_YYYYMMDD_to_ql_date(ymd):
ymd = int(ymd)
year = math.floor(ymd / 10000)
month = int(ymd / 100) % 100
day = ymd % 100
ql_date = ql.Date(day, month, year)
return ql_date
def run_test():
####################################################################
#### Global Variables
####################################################################
pricing_date = datetime.strptime('20220916', '%Y%m%d')
ql_pricing_date = ql.Date(16, 9, 2022)
calendar = ql.UnitedStates(ql.UnitedStates.Settlement)
day_counter = ql.ActualActual()
spot = 3876.00
spot_handle = ql.QuoteHandle(ql.SimpleQuote(spot))
flat_ts = ql.YieldTermStructureHandle(ql.FlatForward(ql_pricing_date,
0, day_counter))
dividend_yield =
ql.YieldTermStructureHandle(ql.FlatForward(ql_pricing_date, 0,
day_counter))
K = 3800
ql_date_expiry = ql.Date(16, 12, 2022)
exercise = ql.EuropeanExercise(ql_date_expiry)
payoff = ql.PlainVanillaPayoff(ql.Option.Put, strike=K)
####################################################################
#### Build Vol surface
####################################################################
list_opts = []
expirations = [datetime.strptime('20220921', '%Y%m%d'),
datetime.strptime('20221118', '%Y%m%d'), datetime.strptime('20221216',
'%Y%m%d'), ]
list_opts.append([pricing_date, expirations[0], 3800., 45.])
list_opts.append([pricing_date, expirations[0], 3900., 40.])
list_opts.append([pricing_date, expirations[1], 3800., 27.])
list_opts.append([pricing_date, expirations[1], 3900., 25.])
list_opts.append([pricing_date, expirations[2], 3800., 37.])
list_opts.append([pricing_date, expirations[2], 3900., 31.])
df = pd.DataFrame(list_opts, columns=['date', 'maturity', 'strike', 'vol'])
df['ymd'] = df['maturity'].dt.strftime('%Y%m%d')
df['q_exp_dates'] = df['ymd'].apply(from_YYYYMMDD_to_ql_date)
strikes = sorted(df.strike.unique())
expirations = sorted(df['q_exp_dates'].unique())
df['strike_idx'] = df['strike'].apply(lambda x: strikes.index(x))
df['expiry_idx'] = df['q_exp_dates'].apply(lambda x: expirations.index(x))
volMat_np = np.full((len(strikes), len(expirations)), np.nan)
for strk, exp, vol in zip(df['strike_idx'], df['expiry_idx'], df['vol']):
volMat_np[strk][exp] = vol / 100
df_vals = pd.DataFrame(volMat_np, index=strikes, columns=expirations)
print(df_vals)
ql_Matrix = ql.Matrix(len(strikes), len(expirations), np.nan)
for i in range(len(strikes)):
for j in range(len(expirations)):
ql_Matrix[i][j] = volMat_np[i, j]
####################################################################
#### We build 1 vol surface and a constant vol =
BlackVol(surface)
####################################################################
vol_process = ql.BlackVarianceSurface(ql_pricing_date, calendar,
expirations, strikes,
ql_Matrix, day_counter)
option_vol = vol_process.blackVol(ql_date_expiry,K)
constant_vol = ql.BlackConstantVol(ql_pricing_date, calendar,
option_vol, day_counter)
####################################################################
#### Build Process
####################################################################
process_surface = ql.BlackScholesMertonProcess(spot_handle,
dividendTS=dividend_yield, riskFreeTS=flat_ts,
volTS=ql.BlackVolTermStructureHandle(vol_process))
process_constant = ql.BlackScholesMertonProcess(spot_handle,
dividendTS=dividend_yield, riskFreeTS=flat_ts,
volTS=ql.BlackVolTermStructureHandle(constant_vol))
####################################################################
#### Build Option to price
####################################################################
option_surface = ql.DividendVanillaOption(payoff, exercise, [], [])
engine_surface = ql.FdBlackScholesVanillaEngine(process_surface, 200, 200)
option_surface.setPricingEngine(engine_surface)
option_constant = ql.DividendVanillaOption(payoff, exercise, [], [])
engine_constant = ql.FdBlackScholesVanillaEngine(process_constant, 200, 200)
option_constant.setPricingEngine(engine_constant)
prc_cst = option_constant.NPV()
gamma_cst = option_constant.gamma()
theta_cst = option_constant.theta() / 252. # biz day theta
vol_be_sqrt252_cst = round(math.sqrt(-2. * theta_cst / (gamma_cst
* spot ** 2)) * math.sqrt(252),4) # calculate theta-gamma break
even vol to estimate what vol is being used by quantlib
print(f'Option details')
print(f'-------------------------------------------------')
print(f'Maturity: {ql_date_expiry}')
print(f'Strike: {K}')
print(f'Vol from surface: {option_vol}')
print(f'')
print(f'-------------------------------------------------')
print(f'Constant vol case')
print(f'-----------------')
print(f'Option price: {prc_cst}')
print(f'Option gamma: {gamma_cst}')
print(f'Option theta: {theta_cst}')
print(f'Break even Vol = {vol_be_sqrt252_cst} should be {option_vol}')
prc_surface = option_surface.NPV()
gamma_surface = option_surface.gamma()
theta_surface = option_surface.theta() / 252. # biz day theta
vol_be_sqrt252_surface = round(math.sqrt(-2. * theta_surface /
(gamma_surface * spot ** 2)) * math.sqrt(252),4) # calculate
theta-gamma break even vol to estimate what vol is being used by
quantlib
print(f'-------------------------------------------------')
print(f'Vol Surface case')
print(f'----------------')
print(f'Option price: {prc_surface}')
print(f'Option gamma: {gamma_surface}')
print(f'Option theta: {theta_surface}')
print(f'Break even Vol = {vol_be_sqrt252_surface} should be {option_vol}')
if __name__ == '__main__':
run_test()
We get the following results:
September 21st, 2022 November 18th, 2022 December 16th, 2022
3800.0 0.45 0.27 0.37
3900.0 0.40 0.25 0.31
Option details
-------------------------------------------------
Maturity: December 16th, 2022
Strike: 3800
Vol from surface: 0.37
-------------------------------------------------
Constant vol case
-----------------
Option price: 246.10555275972283
Option gamma: 0.0005462073912642451
Option theta: -2.2348817578128877
Break even Vol = 0.3705 should be 0.37
-------------------------------------------------
Vol Surface case
----------------
Option price: 246.10595637993455
Option gamma: 0.0005462035022605052
Option theta: -3.3101198905503284
Break even Vol = 0.4509 should be 0.37
If you change the vol surface to something which is not arbitrable but
still has high very short term vol, the problem persists and even
accentuates. In all cases the difference appears on the theta. The price of
the option does not change.
list_opts.append([pricing_date, expirations[0], 3800., 45.])
list_opts.append([pricing_date, expirations[0], 3900., 40.])
list_opts.append([pricing_date, expirations[1], 3800., 27.])
list_opts.append([pricing_date, expirations[1], 3900., 25.])
list_opts.append([pricing_date, expirations[2], 3800., 27.])
list_opts.append([pricing_date, expirations[2], 3900., 25.])
We get the following:
Option details
-------------------------------------------------
Maturity: December 16th, 2022
Strike: 3800
Vol from surface: 0.27
-------------------------------------------------Constant vol case
-----------------
Option price: 170.495477037466
Option gamma: 0.0007462620578669159
Option theta: -1.6258437988242298Break even Vol = 0.2703 should be 0.27
-------------------------------------------------Vol Surface case
----------------
Option price: 170.49560335893932
Option gamma: 0.0007462597949764497
Option theta: -4.538109308076177
Break even Vol = 0.4517 should be 0.27
Thank you very much
|