1 votos

Problema de Quantlib con BlackVarianceSurface difundiendo con el vol incorrecto cuando hay agujeros o arbitrajes en vencimientos tempranos.

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

2voto

Christian Zorn Puntos 1

He ejecutado su código anterior y puedo confirmar los mismos resultados. También he reproducido en C ++ y viendo lo mismo. Al principio pensé que podría ser un problema de recuento de días (365 frente a 252), pero no creo que sea el caso, aunque esa proporción estaría cerca de resolver la discrepancia en este caso específico - creo que es sólo una coincidencia. También pensé que podría haber un problema con el número de pasos de la cuadrícula FD, pero el resultado es relativamente insensible al número de puntos utilizados. Si le preguntas a la superficie de la varianza de la varianza en una huelga determinada, que te da de vuelta el valor correcto. He adjuntado un depurador y creo que el problema radica en algún lugar con la interacción entre el BlackScholesMertonProcess, el FdmBlackScholesMesher, y el solucionador. Es más profundo en QuantLib que mi conocimiento se extiende.

Si hay algún experto en QuantLib dispuesto a echar un vistazo, puedo limpiar el caso de prueba de C++ y publicarlo aquí si resulta útil.

@volPMNYC -- si tu post del 4 de octubre es un duplicado de este, puedes borrarlo para que centremos la atención/conversación aquí.

1voto

Christian Zorn Puntos 1

Creo que lo que está pasando es que el operador de diferencia, FdmBlackScholesOp, está llamando blackForwardVariance(t1, t2, huelga) en su BlackVarianceSurface para obtener la varianza de aplicar a cada paso de tiempo, ya que evoluciona la solución de nuevo a t = 0 en la cuadrícula FD. Debido a que su superficie tiene una desviación de plazo tan pronunciada, con el vencimiento de septiembre cotizando 18 vols por encima de noviembre, el decaimiento del valor es mucho mayor en los primeros días de la vida de la opción hasta después del vencimiento de 21sep22. Así que aunque la bandera localVol en el FdBlackScholesVanillaEngine se establece en falso, todavía es consciente de la estructura de plazos de vol para una huelga dada, y es la asignación de la varianza sobre la vida de la opción de una manera no uniforme, que se suma a la misma varianza total, y por lo tanto el precio de la opción correcta, pero desplaza las griegas para que sean coherentes con la estructura de plazos del mercado.

Dicho de otra manera, basado en tu superficie, el 16dec22 p3800 es ~245 y el 20sep22 p3800 es ~40, el precio de calendario te está diciendo que tu theta será mucho mayor que el theta analítico BSM y el modelo FD lo está reflejando.

Una observación interesante, que me dejó perplejo durante algún tiempo, es que si se adelanta la fecha de evaluación 1 día y se vuelve a ejecutar el modelo FD, se obtendrá el mismo cambio de valor que el predicho por la theta analítica BSM, lo que nos lleva a pensar que la theta FD es incorrecta. Sin embargo, lo que probablemente debería ocurrir es que los vols de diciembre bajaran para reflejar la pérdida de valor como resultado de la caída de un día de alta varianza al inicio de la vida de la opción.

Quizá algún experto en QuantLib pueda corroborar esta tesis.

A continuación se muestra la evolución de theta en las primeras etapas de la vida de la opción tomada de la rejilla FD. En 9 días la theta ha caído por debajo de la theta analítica BSM.

Theta Evolution from the FD grid ...

Finanhelp.com

FinanHelp es una comunidad para personas con conocimientos de economía y finanzas, o quiere aprender. Puedes hacer tus propias preguntas o resolver las de los demás.

Powered by:

X