σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademyRoteiros
Ctrl K
AO
+1,2 mil alunos
EntrarParticipar ↗
Roteiros
AO
+1,2 mil alunos
Entrar
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Roteiro de Python›Módulo 11 · Sistemas de produção e modelos de ponta›Tópico 11.8·~1 h 30 minEspecialista

Volatilidade local

Posição no roteiroMódulo 11 de 11 · Tópico 8 de 13
11.8 Volatilidade local
—Nota do lab— / 100XP total—Sequência—

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 prices

A 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 back

Usando 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
Um ajuste perfeito não é um modelo perfeitoComo a volatilidade local é lida dos preços, ela os reproduz por construção, inclusive o ruído e qualquer arbitragem nas cotações. Verifique que a superfície é livre de arbitragem antes de usar Dupire (borboletas e spreads de calendário não negativos), limite por cima e por baixo as volatilidades resultantes e teste os preços simulados contra as opções vanilla de que partiu. Um modelo que se ajusta perfeitamente a hoje ainda pode fazer mal o hedge de amanhã, pois sua ideia de como o smile se move é simplista.
  • 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_loc ao 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
Explorador · lab-137-1
▾ lab-137-1
…
bash · no navegador
learner@quantcraft:~/lab-137-1$
Python ainda não carregadoPythonLn 1, Col 1Spaces: 4UTF-8

Redefinir este tópico?

Tudo em lab-137-1 volta aos arquivos iniciais e o terminal é limpo. Sua solução atual será perdida — isso não pode ser desfeito.

Será redefinido
  • main.py → código inicial
  • Histórico do terminal e saída dos testes
  • Alterações não enviadas
Será mantido
  • Melhor nota do lab
  • Seu XP e sua sequência
  • Progresso da aula e anotações

Entre para executar o seu código.

Todas as aulas são gratuitas para ler. Crie uma conta ou entre para executar código, rodar os testes e enviar este lab.

  • IDE no navegador com terminal bash — nada para instalar
  • Correção instantânea, XP e o seu lugar no roteiro
Criar uma contaEntrar
OU
Continuar com o GitHub