Processing math: 100%

6 votos

Desviación hacia delante generada por el modelo Local Vol

Estoy investigando las propiedades del modelo Local Vol y me confundo con las afirmaciones que hacen los autores en los artículos/libros de texto (sin explicaciones) como: "El sesgo hacia delante en el modelo Local Vol se aplana" o "el Local Vol no es fiable para predecir el sesgo hacia delante".

Denotemos la función vol determinista local σLoct(T,K) y la superficie vol implícita σIVt(K,T) , donde t se refiere al momento en que los precios de las vainas con strike K y la madurez T>t0 se observan en el mercado. Por ejemplo, hoy en t=0 observamos σIV0(K,T) y puede derivar σLoc0(T,K) utilizando la fórmula de Dupire con σIV0 -superficie como entrada.

Entiendo que cada día, dada la superficie de vol. implícita actualizada, se calibra una nueva función de vol. local, es decir, esta última siempre depende de la primera. Como entonces uno usando el modelo calibrado, digamos hoy, puede hacer predicciones sobre el skew implícito a futuro (t>0), σIVt(K,T) ? (por no hablar de cómo podemos validar que esta superficie prevista es más plana en comparación con la que se realizará en el futuro).

Cualquier referencia es muy apreciada.

11voto

Marc Puntos 892

Podemos demostrarlo mediante un experimento de fijación de precios utilizando QuantLib-Python.

He definido varias funciones de utilidad en el bloque de código al final de la respuesta que necesitarás para replicar el trabajo.

En primer lugar, vamos a crear un proceso Heston, y a calibrar un modelo local de vol para que coincida con él. Hasta cuestiones numéricas, ambos deberían tener el mismo precio de las vainillas.

v0, kappa, theta, rho, sigma = 0.015, 1.5, 0.08, -0.4, 0.4

dates, strikes, vols, feller = create_vol_surface_mesh_from_heston_params(today, calendar, spot, v0, kappa, theta, rho, sigma, flat_ts, dividend_ts)

local_vol_surface = ql.BlackVarianceSurface(today, calendar, dates, strikes, vols, day_count)

# Plot the vol surface ...
plot_vol_surface(local_vol_surface, plot_years=np.arange(0.1, 2, 0.1))

Our vol surface

En este caso, he elegido los parámetros de heston para dar un vol bastante rápido, un sesgo moderado hacia abajo, y para mantenernos a salvo de la condición de feller.

Ahora, la forma más elegante de proceder sería utilizar los precios incorporados en ql y los instrumentos de precio del tipo ql.ForwardVanillaOption pero, lamentablemente, el único motor de fijación de precios de opciones a plazo expuesto actualmente en python es ql.ForwardEuropeanEngine whihc pondrá el precio bajo el vol local pero no bajo el modelo heston, así que en su lugar procedo usando monte carlo y poniendo el precio de las opciones explícitamente (es un poco tosco pero demuestra el punto).

El siguiente paso es generar muchas rutas de MC a partir de los procesos que acabo de definir

local_vol = ql.BlackVolTermStructureHandle(local_vol_surface)

bs_process = ql.BlackScholesMertonProcess(ql.QuoteHandle(ql.SimpleQuote(spot)), dividend_ts, flat_ts, local_vol)
heston_process = ql.HestonProcess(flat_ts, dividend_ts, ql.QuoteHandle(ql.SimpleQuote(spot)), v0, kappa, theta, sigma, rho)

bs_paths = generate_multi_paths_df(bs_process, num_paths=100000, timestep=72, length=3)[0]
heston_paths, heston_vols = generate_multi_paths_df(heston_process, num_paths=100000, timestep=72, length=3)

bs_paths.head().transpose().plot()

plt.pause(0.05)

heston_paths.head().transpose().plot()

Sample local vol and Heston paths

Ahora que tenemos trayectorias, queremos cotizar las opciones que comienzan a lo largo de cada una de ellas. A continuación, cotizo las opciones que comienzan en 1Y y expiran en 2Y, y las opciones que comienzan en 2Y y expiran en 3Y, con distintos tipos de dinero (el strike sólo se determina al inicio, por el spot*dinero). Como mis tipos son 0 en todas partes, el precio de estas opciones es simplemente (S(2) - moneyness * S(1)).clip(0).mean() o similar.

También tenemos que retirar los "vols implícitos" de estos precios. Dado que el precio de ejercicio no se determina de antemano, no está del todo claro que utilizar la fórmula normal de BS sea correcto, pero lo he hecho de todos modos (utilizando moneyness * spot como precio de ejercicio), a continuación.

moneynesses = np.linspace(0.6, 1.4, 17)
prices = []

for moneyness in moneynesses:
    lv_price_1y = (bs_paths[2.0] - moneyness * bs_paths[1.0]).clip(0).mean()
    lv_price_2y = (bs_paths[3.0] - moneyness * bs_paths[2.0]).clip(0).mean()

    heston_price_1y = (heston_paths[2.0] - moneyness * heston_paths[1.0]).clip(0).mean()
    heston_price_2y = (heston_paths[3.0] - moneyness * heston_paths[2.0]).clip(0).mean()

    prices.append({'moneyness': moneyness, 'lv_price_1y': lv_price_1y, 'lv_price_2y': lv_price_2y, 'heston_price_1y': heston_price_1y, 'heston_price_2y': heston_price_2y})

price_df = pd.DataFrame(prices)

price_df['lv_iv_1y'] = price_df.apply(lambda x: bs_implied_vol(x['lv_price_1y'], 1.0, 100, 100 * x['moneyness'], 1.0), axis=1)
price_df['lv_iv_2y'] = price_df.apply(lambda x: bs_implied_vol(x['lv_price_2y'], 1.0, 100, 100 * x['moneyness'], 1.0), axis=1)
price_df['heston_iv_1y'] = price_df.apply(lambda x: bs_implied_vol(x['heston_price_1y'], 1.0, 100, 100 * x['moneyness'], 1.0), axis=1)
price_df['heston_iv_2y'] = price_df.apply(lambda x: bs_implied_vol(x['heston_price_2y'], 1.0, 100, 100 * x['moneyness'], 1.0), axis=1)

plt.plot(moneynesses, price_df['lv_iv_1y'], label='lv 1y fwd iv at 1y')
plt.plot(moneynesses, price_df['lv_iv_2y'], label='lv 1y fwd iv at 2y')
plt.plot(moneynesses, price_df['heston_iv_1y'], label='heston 1y fwd iv at 1y')
plt.plot(moneynesses, price_df['heston_iv_2y'], label='heston 1y fwd iv at 2y')

plt.title("Forward IVs in Local Vol and Heston")
plt.legend()

Heston and lv forward-start vols

Como puede ver, los vols a plazo procedentes de lv son mucho más planos y menos sonrientes que los precios del proceso de heston, que es exactamente el efecto que buscábamos.

Funciones de utilidad y código boilerplate de QuantLib:

import warnings
warnings.filterwarnings('ignore')

import QuantLib as ql
import numpy as np
import pandas as pd

from scipy import optimize, stats
from matplotlib import pyplot as plt
import matplotlib.cm as cm
from mpl_toolkits.mplot3d import Axes3D

def plot_vol_surface(vol_surface, plot_years=np.arange(0.1, 3, 0.1), plot_strikes=np.arange(70, 130, 1), funct='blackVol'):
    if type(vol_surface) != list:
        surfaces = [vol_surface]
    else:
        surfaces = vol_surface

    fig = plt.figure(figsize=(8,6))
    ax = fig.gca(projection='3d')
    X, Y = np.meshgrid(plot_strikes, plot_years)

    for surface in surfaces:
        method_to_call = getattr(surface, funct)

        Z = np.array([method_to_call(float(y), float(x)) 
                      for xr, yr in zip(X, Y) 
                          for x, y in zip(xr,yr) ]
                     ).reshape(len(X), len(X[0]))

        surf = ax.plot_surface(X,Y,Z, rstride=1, cstride=1, linewidth=0.1)

    N = Z / Z.max()  # normalize 0 -> 1 for the colormap
    surf = ax.plot_surface(X, Y, Z, rstride=1, cstride=1, linewidth=0.1, facecolors=cm.twilight(N))

    m = cm.ScalarMappable(cmap=cm.twilight)
    m.set_array(Z)
    plt.colorbar(m, shrink=0.8, aspect=20)
    ax.view_init(30, 300)

def generate_multi_paths_df(process, num_paths=1000, timestep=24, length=2):
    """Generates multiple paths from an n-factor process, each factor is returned in a seperate df"""
    times = ql.TimeGrid(length, timestep)
    dimension = process.factors()

    rng = ql.GaussianRandomSequenceGenerator(ql.UniformRandomSequenceGenerator(dimension * timestep, ql.UniformRandomGenerator()))
    seq = ql.GaussianMultiPathGenerator(process, list(times), rng, False)

    paths = [[] for i in range(dimension)]

    for i in range(num_paths):
        sample_path = seq.next()
        values = sample_path.value()
        spot = values[0]

        for j in range(dimension):
            paths[j].append([x for x in values[j]])

    df_paths = [pd.DataFrame(path, columns=[spot.time(x) for x in range(len(spot))]) for path in paths]

    return df_paths

def create_vol_surface_mesh_from_heston_params(today, calendar, spot, v0, kappa, theta, rho, sigma, 
                                               rates_curve_handle, dividend_curve_handle,
                                               strikes = np.linspace(40, 200, 161), tenors = np.linspace(0.1, 3, 60)):
    quote = ql.QuoteHandle(ql.SimpleQuote(spot))

    heston_process = ql.HestonProcess(rates_curve_handle, dividend_curve_handle, quote, v0, kappa, theta, sigma, rho)
    heston_model = ql.HestonModel(heston_process)
    heston_handle = ql.HestonModelHandle(heston_model)
    heston_vol_surface = ql.HestonBlackVolSurface(heston_handle)

    data = []
    for strike in strikes:
        data.append([heston_vol_surface.blackVol(tenor, strike) for tenor in tenors])

    expiration_dates = [calendar.advance(today, ql.Period(int(365*t), ql.Days)) for t in tenors]
    implied_vols = ql.Matrix(data)
    feller = 2 * kappa * theta - sigma ** 2

    return expiration_dates, strikes, implied_vols, feller

def d_plus_minus(forward, strike, tte, vol):
    denominator = vol * np.sqrt(tte)
    inner_term = np.log(forward / strike) + 0.5 * vol * vol * tte
    d_plus = inner_term / denominator
    d_minus = d_plus - denominator

    return d_plus, d_minus

def call_option_price(vol, dcf, forward, strike, tte):
    d_plus, d_minus = d_plus_minus(forward, strike, tte, vol)
    return dcf * (forward * stats.norm.cdf(d_plus) - strike * stats.norm.cdf(d_minus))

def vol_solver_helper(x, price, dcf, forward, strike, tte):
    return call_option_price(x, dcf, forward, strike, tte) - price

def bs_implied_vol(price, dcf, forward, strike, tte):
    return optimize.brentq(vol_solver_helper, 0.0001, 2.0, args=(price, dcf, forward, strike, tte))

# World State for Vanilla Pricing
spot = 100
vol = 0.1
rate = 0.0
dividend = 0.0

today = ql.Date(1, 9, 2020)

day_count = ql.Actual365Fixed()
calendar = ql.NullCalendar()

# Set up the vol and risk-free curves
volatility = ql.BlackConstantVol(today, calendar, vol, day_count)
riskFreeCurve = ql.FlatForward(today, rate, day_count)
dividendCurve = ql.FlatForward(today, rate, day_count)

flat_ts = ql.YieldTermStructureHandle(riskFreeCurve)
dividend_ts = ql.YieldTermStructureHandle(dividendCurve)
flat_vol = ql.BlackVolTermStructureHandle(volatility)

0 votos

Gran explicación, ¡gracias!

3voto

hoge Puntos 281

El sesgo a plazo de un modelo es fácil de ver fijando el precio de las opciones de inicio a plazo de strike flotante en dicho modelo. Si lo haces con vol local, calibrado a una superficie de volatilidad realista (donde los vols y skews de vencimiento cercano son más altos que los vols y skews de vencimiento lejano) verás que el skew forward decae a cero.

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