Voy a mostrar un ejemplo codificado para mostrar cómo se puede intentar esto, utilizando el puerto de python del QuantLib biblioteca. Todo parecerá un poco mecánico, pero espero que sea instructivo. Hay un poco de código de configuración requerida (especificando los tipos de interés, spot, etc., y también una función de utilidad que utilizo para trazar las superficies), he empujado todo esto a la parte inferior de mi respuesta, pero lo necesitará en la parte superior de su script/notebook.
Para construir un modelo de vol local, voy a necesitar una superficie de vol implícita de los datos del mercado. No sé qué datos estás utilizando, así que como ejemplo muy trivial, voy a simular una secuencia de sonrisas SABR a diferentes vencimientos, y asumir que es la superficie de vol implícita del mercado. El código para hacer eso es:
strikes = [70.0, 80.0, 90.0, 100.0, 110.0, 120.0, 130.0]
expirations = [ql.Date(1, 7, 2021), ql.Date(1, 9, 2021), ql.Date(1, 12, 2021), ql.Date(1, 6, 2022)]
vol_matrix = ql.Matrix(len(strikes), len(expirations))
# params are sigma_0, beta, vol_vol, rho
sabr_params = [[0.4, 0.6, 0.4, -0.6],
[0.4, 0.6, 0.4, -0.6],
[0.4, 0.6, 0.4, -0.6],
[0.4, 0.6, 0.4, -0.6]]
for j, expiration in enumerate(expirations):
for i, strike in enumerate(strikes):
tte = day_count.yearFraction(today, expiration)
vol_matrix[i][j] = ql.sabrVolatility(strike, spot, tte, *sabr_params[j])
implied_surface = ql.BlackVarianceSurface(today, calendar, expirations, strikes, vol_matrix, day_count)
implied_surface.setInterpolation('bicubic')
vol_ts = ql.BlackVolTermStructureHandle(implied_surface)
process = ql.BlackScholesMertonProcess(ql.QuoteHandle(ql.SimpleQuote(spot)), dividend_ts, flat_ts, vol_ts)
plot_vol_surface(implied_surface, plot_years=np.arange(0.1, 1, 0.1))
plt.title("Implied Vol")
Esto también traza la superficie del vol para que podamos verlo:
Ahora voy a intentar fijar el precio de una opción vainilla a 6 meses de vencimiento, utilizaré un motor de fijación de precios por diferencias finitas y un motor de fijación de precios monte carlo. Primero, vamos a consultar la superficie y ver qué vol esperamos:
implied_surface.blackVol(0.5, 90)
devuelve 0,0787116605540102, es decir, un 7,87% de IV
En QuantLib, configuramos los motores de opciones y de precios de forma independiente. Primero, la opción:
strike = 90.0
option_type = ql.Option.Call
maturity = today + ql.Period(6, ql.Months)
europeanExercise = ql.EuropeanExercise(maturity)
payoff = ql.PlainVanillaPayoff(option_type, strike)
european_option = ql.VanillaOption(payoff, europeanExercise)
y ahora configuraré y fijaré el precio utilizando cada uno de los dos motores de fijación de precios (se puede encontrar un recurso inestimable sobre ellos en el ReadTheDocs página mantenida por uno de los otros carteles aquí).
En primer lugar, el FD pricer (tenga en cuenta que TENEMOS que activar el parámetro final aquí, que especifica el uso de Dupire local vol...):
tGrid, xGrid = 3000, 400
fd_engine = ql.FdBlackScholesVanillaEngine(process, tGrid, xGrid, 0, ql.FdmSchemeDesc.Douglas(), True)
european_option.setPricingEngine(fd_engine)
fd_price = european_option.NPV()
fd_price
el precio es de 7,699526511916783
Y ahora el MC pricer (para el MC en vol local, un número suficiente de pasos es vital, experimente con la reducción de esto y vea lo que sucede con el precio):
steps = 36
rng = "lowdiscrepancy" # could use "pseudorandom"
numPaths = 2**15
mc_engine = ql.MCEuropeanEngine(process, rng, steps, requiredSamples=numPaths)
european_option.setPricingEngine(mc_engine)
mc_price = european_option.NPV()
mc_price
el precio es de 7,684257656799339
Y como comparación final, calcularemos los vols que implica cada uno de estos precios, para comparar con el vol de la superficie anterior:
# Compare calculated volatility to vol from the surface
european_option.impliedVolatility(fd_price, process), european_option.impliedVolatility(mc_price, process)
(0,07872046080861067, 0,077079492498713), ambos bastante cercanos al vol que esperábamos.
Espero que esto sea útil. Hay cientos de pequeñas suposiciones que he hecho en la respuesta, sin embargo, que podrían no ser apropiadas para tu situación... hazme saber si quieres que cambie algo.
Código de caldera necesario para ejecutar los fragmentos anteriores:
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
import matplotlib.cm as cm
from mpl_toolkits.mplot3d import Axes3D
import QuantLib as ql
calc_date = ql.Date(21, ql.December, 2020)
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]
functs = [funct]
else:
surfaces = vol_surface
if type(funct) != list:
functs = [funct] * len(surfaces)
else:
functs = funct
fig = plt.figure(figsize=(10, 6))
ax = fig.gca(projection='3d')
X, Y = np.meshgrid(plot_strikes, plot_years)
Z_array, Z_min, Z_max = [], 100, 0
for surface, funct in zip(surfaces, functs):
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]))
Z_array.append(Z)
Z_min, Z_max = min(Z_min, Z.min()), max(Z_max, Z.max())
# In case of multiple surfaces, need to find universal max and min first for colourmap
for Z in Z_array:
N = (Z - Z_min) / (Z_max - Z_min) # normalize 0 -> 1 for the colormap
surf = ax.plot_surface(X, Y, Z, rstride=1, cstride=1, linewidth=0.1, facecolors=cm.coolwarm(N))
m = cm.ScalarMappable(cmap=cm.coolwarm)
m.set_array(Z)
plt.colorbar(m, shrink=0.8, aspect=20)
ax.view_init(30, 300)
# Simple World State
spot = 100
rate_dom = 0.02
rate_for = 0.05
today = ql.Date(1, 6, 2021)
calendar = ql.NullCalendar()
day_count = ql.Actual365Fixed()
# Set up some risk-free curves
riskFreeCurveDom = ql.FlatForward(today, rate_dom, day_count)
riskFreeCurveFor = ql.FlatForward(today, rate_for, day_count)
flat_ts = ql.YieldTermStructureHandle(riskFreeCurveDom)
dividend_ts = ql.YieldTermStructureHandle(riskFreeCurveFor)
Actualización - Interpolación de Andraesen-Huge (nb. He cambiado ligeramente la función gráfica de arriba):
strikes = np.linspace(70, 130, 10)
expirations = [ql.Date(1, 7, 2021), ql.Date(1, 8, 2021), ql.Date(1, 9, 2021), ql.Date(1, 12, 2021), ql.Date(1, 6, 2022)]
# params are sigma_0, beta, vol_vol, rho
sabr_params = [[0.4, 0.6, 0.4, -0.6],
[0.4, 0.6, 0.4, -0.6],
[0.4, 0.6, 0.4, -0.6],
[0.4, 0.6, 0.4, -0.6],
[0.4, 0.6, 0.4, -0.6]]
# Now try Andraeson Huge calibration
calibration_set = ql.CalibrationSet()
for i, strike in enumerate(strikes):
for j, expiration in enumerate(expirations):
tte = day_count.yearFraction(today, expiration)
payoff = ql.PlainVanillaPayoff(ql.Option.Call, strike)
exercise = ql.EuropeanExercise(expiration)
vol = ql.sabrVolatility(strike, spot, tte, *sabr_params[j])
calibration_set.push_back((ql.VanillaOption(payoff, exercise), ql.SimpleQuote(vol)))
ah_interpolation = ql.AndreasenHugeVolatilityInterpl(calibration_set, \
ql.QuoteHandle(ql.SimpleQuote(spot)), flat_ts, dividend_ts)
ah_surface = ql.AndreasenHugeVolatilityAdapter(ah_interpolation)
plot_vol_surface([implied_surface, ah_surface], plot_years=np.arange(0.2, 1, 0.05), plot_strikes=np.arange(80, 120, 2))
3 votos
¿Qué opciones está tratando de valorar? QuantLib-Python tiene muchos métodos para valorar opciones bajo volatilidad local. Si puedes contarnos un poco más sobre tu problema, te proporcionaré un código de ejemplo. A partir de ahí, puedes profundizar en la implementación y entender lo que sucede "bajo el capó".
0 votos
Hola @StackG. Estoy tratando de ponerle precio a las opciones CDX que son opciones vainilla. He seguido un algoritmo que ayuda a interpolar los precios a través del strike y el tenor para ayudar a encontrar la volatilidad local. Esta interpolación se hizo para que las derivadas que definen la volatilidad local según la ecuación dupire tengan sentido. Hágame saber si necesita aprender más