Uma volatilidade para cada strike e data
O Black-Scholes precifica um call com uma só volatilidade, mas o mercado cota uma volatilidade implícita diferente para cada strike e vencimento. O modelo de volatilidade local reproduz todos esses preços de uma vez deixando a volatilidade depender do nível de preço e do tempo, sigma(S, t), com uma única fonte de aleatoriedade. Bruno Dupire mostrou em 1994 que essa função não é um palpite: ela é determinada pela superfície de preços de call C(K, T) por uma fórmula, de modo que os preços de opções do mercado se transformam diretamente num modelo para precificar opções exóticas de forma coerente com eles.
# 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 pricesA fórmula de Dupire
A fórmula de Dupire expressa a variância local com três derivadas da superfície de preços de call: sigma_loc^2(K, T) = (dC/dT + (r - q) * K * dC/dK + q * C) / (0.5 * K^2 * d2C/dK2). O numerador contém a inclinação temporal dos preços (deve ser não negativa, senão uma opção mais longa custa menos que uma mais curta: uma arbitragem de calendário) e o denominador a borboleta d2C/dK2, proporcional à densidade de probabilidade implícita do mercado para o preço, que deve ser positiva. Com cotações reais, ruidosas e esparsas, as derivadas são tomadas por diferenças finitas numa superfície suavizada: (f(x+h) - f(x-h)) / (2h) para a primeira derivada e (f(x+h) - 2 f(x) + f(x-h)) / h^2 para a segunda. Uma superfície ruidosa dá volatilidades locais selvagens, então suavizar primeiro a superfície de volatilidade implícita é prática padrão.
# 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 backUsando a superfície numa simulação
Para simular, tabula-se sigma_loc numa grade de preços (para cada fatia de tempo) e consulta-se ao preço atual a cada passo, interpolando linearmente entre pontos e mantendo constantes os valores das pontas fora da grade. Um passo é então o movimento log-Euler habitual com essa volatilidade, S * exp((mu - sigma^2/2) dt + sigma sqrt(dt) dz). Comparada ao Heston, a volatilidade local ajusta exatamente os preços de opções de hoje por construção, mas a dinâmica futura do seu smile é irrealista (o skew achata com o tempo), o que importa para produtos sensíveis ao smile futuro. Muitas mesas usam por isso volatilidade local-estocástica, uma mistura das duas.
# 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- A volatilidade local
sigma(S, t)reproduz toda a superfície de preços de call com uma só fonte de aleatoriedade; a fórmula de Dupire a dá diretamente a partir dos preços. - Dupire:
(dC/dT + (r - q) K dC/dK + q C) / (0.5 K^2 d2C/dK2); calcule as derivadas por diferenças finitas numa superfície suavizada e livre de arbitragem. - Simule consultando
sigma_locao preço atual a cada passo; um ajuste perfeito hoje não garante uma dinâmica realista do smile amanhã.
A seguir: o modelo SABR, um modelo de volatilidade estocástica com smile em forma fechada.
Lab: o que é avaliado
- Derivar numericamente com
finite_difference25 pts - Aplicar a fórmula de Dupire em
local_variance25 pts - Ler a superfície com
lookup_local_vol25 pts - Simular um passo com
local_vol_step25 pts