Soy nuevo en Quantlib en Python y me estoy encontrando con una situación bastante incómoda.
Tengo una superficie vol del mercado en el índice SPX. No todos los strikes/maturidades están poblados. Además, potencialmente podría haber arbitrajes en vencimientos cortos ya que tenemos muchas opciones diarias y todavía no estoy corrigiendo los arbitrajes.
Sin embargo, cuando se construye un BlackVarianceSurface con estos datos y el precio de una opción vainilla, espero que la opción que se cotiza con una constante vol interpolada de la red.
Cuando paso el objeto superficie al pricer estoy obteniendo un precio muy diferente al que obtengo simplemente pasando la constante vol tomada de la superficie. El precio con la superficie parece tener en cuenta vols a muy corto plazo y arbitrajes. He jugado con la superficie eliminando vols a muy corto plazo y/o strikes lejanos y el precio de la opción realmente cambia (?!?). Cuando digo que el precio es diferente, me refiero a que en vez de obtener el precio de una opción con 22 vol implícitos, obtengo el de una opción con 38 vol implícitos.
El precio con el vol constante es exactamente lo que espero y me da el vol de equilibrio adecuado. Así que claramente podría interpolar desde la cuadrícula para cada precio y obtener el precio adecuado.
Nótese que la opción (strike,maturity) existe en la rejilla dada en input por lo que no es ni un problema de interpolación ni de extrapolación (aunque jugando con estos ajustes también cambia ligeramente el precio pero en mucha menor medida y compatible con el ruido de interpolación).
Parece muy contraproducente tener que interpolar manualmente a partir de la superficie antes de cada tarificación en lugar de pasar el objeto superficie y dejar que QLIB obtenga el vol adecuado bajo la cubierta.
¿Podría ayudarme a entender exactamente cómo QLIB maneja la superficie y qué proceso de difusión se construye a partir de esa superficie? Sabemos que no es un proceso vol local el que se construye. En consecuencia, ¿por qué no es un proceso determinista basado en el vol interpolado?
Muchas gracias.
Aquí hay un código de ejemplo que compara un vol constante con un precio de vol superficial. Observe cómo cambia theta entre los 2 casos, lo que lleva a un vol. de equilibrio muy diferente.
import pandas as pd, QuantLib as ql, math, numpy as np
from 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()
Obtenemos los siguientes resultados:
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
Si se cambia la superficie vol por algo que no sea arbitrable pero que siga teniendo un vol alto a muy corto plazo, el problema persiste e incluso se acentúa. En todos los casos la diferencia aparece en theta. El precio de la opción no cambia.
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.])
Obtenemos lo siguiente:
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.6258437988242298
Break 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