Una volatilidad para cada strike y fecha
Black-Scholes valora un call con una sola volatilidad, pero el mercado cotiza una volatilidad implícita distinta para cada strike y vencimiento. El modelo de volatilidad local reproduce todos esos precios a la vez dejando que la volatilidad dependa del nivel de precio y del tiempo, sigma(S, t), con una sola fuente de aleatoriedad. Bruno Dupire mostró en 1994 que esta función no es una suposición: la determina la superficie de precios de call C(K, T) mediante una fórmula, de modo que los precios de opciones del mercado se convierten directamente en un modelo para valorar opciones exóticas de forma coherente con ellos.
# What "one volatility" means: a call priced with Black-Scholes at three strikes, all with the same sigma
import math
def norm_cdf(x):
return 0.5 * (1 + math.erf(x / math.sqrt(2)))
def bs_call(s, k, t, r, q, sigma):
d1 = (math.log(s / k) + (r - q + sigma ** 2 / 2) * t) / (sigma * math.sqrt(t))
d2 = d1 - sigma * math.sqrt(t)
return s * math.exp(-q * t) * norm_cdf(d1) - k * math.exp(-r * t) * norm_cdf(d2)
for k in (90, 100, 110):
print(k, round(bs_call(100.0, k, 1.0, 0.03, 0.01, 0.2), 4)) # 90 14.6592 100 8.8273 110 4.8947 one sigma, three different pricesLa fórmula de Dupire
La fórmula de Dupire expresa la varianza local con tres derivadas de la superficie de precios de call: sigma_loc^2(K, T) = (dC/dT + (r - q) * K * dC/dK + q * C) / (0.5 * K^2 * d2C/dK2). El numerador contiene la pendiente temporal de los precios (debe ser no negativa, si no una opción más larga cuesta menos que una más corta: un arbitraje calendario) y el denominador la mariposa d2C/dK2, proporcional a la densidad de probabilidad implícita del mercado para el precio, que debe ser positiva. Con cotizaciones reales, ruidosas y dispersas, las derivadas se calculan por diferencias finitas sobre una superficie suavizada: (f(x+h) - f(x-h)) / (2h) para la primera derivada y (f(x+h) - 2 f(x) + f(x-h)) / h^2 para la segunda. Una superficie ruidosa da volatilidades locales salvajes, así que suavizar primero la superficie de volatilidad implícita es práctica habitual.
# Dupire's formula recovers a constant volatility from Black-Scholes prices (finite differences on the price surface)
import math
def norm_cdf(x):
return 0.5 * (1 + math.erf(x / math.sqrt(2)))
def bs_call(s, k, t, r, q, sigma):
d1 = (math.log(s / k) + (r - q + sigma ** 2 / 2) * t) / (sigma * math.sqrt(t))
return s * math.exp(-q * t) * norm_cdf(d1) - k * math.exp(-r * t) * norm_cdf(d1 - sigma * math.sqrt(t))
def local_variance(call, dc_dt, dc_dk, d2c_dk2, strike, r, q):
return (dc_dt + (r - q) * strike * dc_dk + q * call) / (0.5 * strike ** 2 * d2c_dk2)
s, k, t, r, q, sigma = 100.0, 105.0, 1.0, 0.03, 0.01, 0.2
hk, ht = 0.5, 0.002
c = bs_call(s, k, t, r, q, sigma)
dc_dt = (bs_call(s, k, t + ht, r, q, sigma) - bs_call(s, k, t - ht, r, q, sigma)) / (2 * ht)
dc_dk = (bs_call(s, k + hk, t, r, q, sigma) - bs_call(s, k - hk, t, r, q, sigma)) / (2 * hk)
d2c_dk2 = (bs_call(s, k + hk, t, r, q, sigma) - 2 * c + bs_call(s, k - hk, t, r, q, sigma)) / hk ** 2
print(round(math.sqrt(local_variance(c, dc_dt, dc_dk, d2c_dk2, k, r, q)), 3)) # 0.2 the constant volatility comes backUsar la superficie en una simulación
Para simular, se tabula sigma_loc en una malla de precios (para cada corte de tiempo) y se consulta al precio actual en cada paso, interpolando linealmente entre puntos y manteniendo constantes los valores extremos fuera de la malla. Un paso es entonces el movimiento log-Euler habitual con esa volatilidad, S * exp((mu - sigma^2/2) dt + sigma sqrt(dt) dz). Comparada con Heston, la volatilidad local ajusta exactamente los precios de opciones de hoy por construcción, pero su dinámica futura de la sonrisa es poco realista (el skew se aplana con el tiempo), lo que importa para productos sensibles a la sonrisa futura. Muchas mesas usan por eso volatilidad local-estocástica, una mezcla de ambas.
# A lookup on a strike grid (linear inside, flat outside) and one simulation step with the volatility it returns
import math
def lookup_local_vol(strikes, vols, k):
if k <= strikes[0]:
return vols[0]
if k >= strikes[-1]:
return vols[-1]
for i in range(1, len(strikes)):
if k <= strikes[i]:
weight = (k - strikes[i - 1]) / (strikes[i] - strikes[i - 1])
return vols[i - 1] + weight * (vols[i] - vols[i - 1])
strikes, vols = [90.0, 100.0, 110.0], [0.25, 0.20, 0.18]
print(lookup_local_vol(strikes, vols, 95.0), round(lookup_local_vol(strikes, vols, 105.0), 4), lookup_local_vol(strikes, vols, 200.0)) # 0.225 0.19 0.18
s = 100.0
sigma = lookup_local_vol(strikes, vols, s)
print(round(s * math.exp((0.05 - sigma ** 2 / 2) * (1 / 252) + sigma * math.sqrt(1 / 252) * 0.5), 4)) # 100.6439# A calendar arbitrage shows up as a negative numerator: a longer option priced below a shorter one
def numerator(call, dc_dt, dc_dk, strike, r, q):
return dc_dt + (r - q) * strike * dc_dk + q * call
print(round(numerator(10.0, 1.2, -0.45, 100.0, 0.03, 0.01), 6)) # 0.4 prices rise with maturity: a valid local variance
print(numerator(10.0, -2.0, -0.45, 100.0, 0.03, 0.01) < 0) # True prices fall with maturity: no model can fit this- La volatilidad local
sigma(S, t)reproduce toda la superficie de precios de call con una sola fuente de aleatoriedad; la fórmula de Dupire la da directamente a partir de los precios. - Dupire:
(dC/dT + (r - q) K dC/dK + q C) / (0.5 K^2 d2C/dK2); calcula las derivadas por diferencias finitas sobre una superficie suavizada y sin arbitraje. - Simula consultando
sigma_local precio actual en cada paso; un ajuste perfecto hoy no garantiza una dinámica realista de la sonrisa mañana.
A continuación: el modelo SABR, un modelo de volatilidad estocástica con sonrisa en forma cerrada.
Lab: qué se califica
- Derivar numéricamente con
finite_difference25 pts - Aplicar la fórmula de Dupire en
local_variance25 pts - Leer la superficie con
lookup_local_vol25 pts - Simular un paso con
local_vol_step25 pts