σμ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.10·~1 h 30 minEspecialista

Calibração de modelos

Posição no roteiroMódulo 11 de 11 · Tópico 10 de 13
11.10 Calibração de modelos
—Nota do lab— / 100XP total—Sequência—

Fazer o modelo concordar com o mercado

Um modelo de precificação tem parâmetros, e os valores certos não estão num livro mas no mercado de hoje. A calibração escolhe os parâmetros para que os preços do modelo fiquem o mais perto possível dos observados, normalmente as cotações de opções do dia. A receita tem três partes. Um objetivo que mede o desajuste, quase sempre uma soma de diferenças ao quadrado, possivelmente ponderada para que cotações líquidas contem mais. Uma busca que move os parâmetros para reduzi-lo. E verificações de que o resultado é sensato: estável de um dia para o outro, dentro de limites plausíveis e bom em cotações não usadas no ajuste.

# The objective: how far the model's prices are from the market's, as a (weighted) sum of squares
def sum_squared_error(model, market, weights=None):
    if weights is None:
        weights = [1.0] * len(model)
    return sum(w * (m - y) ** 2 for w, m, y in zip(weights, model, market))

market = [4.20, 8.60, 14.10]
model = [4.00, 8.80, 14.20]
print(round(sum_squared_error(model, market), 4))                    # 0.09   0.2^2 + 0.2^2 + 0.1^2
print(round(sum_squared_error(model, market, [1, 4, 1]), 4))         # 0.21   the middle quote counts four times as much

Duas ferramentas de busca: uma raiz e um mínimo

Duas tarefas surgem constantemente. Primeiro, a volatilidade implícita: dado um preço de mercado, achar a volatilidade com que o modelo o dá, isto é, uma raiz de model(sigma) - price = 0. Como o preço sobe de forma constante com a volatilidade, a bisseção funciona: comece com um intervalo em que a diferença muda de sinal, divida-o ao meio, fique com a metade em que o sinal ainda muda e repita; cada passo dobra a precisão e não pode falhar. Segundo, um mínimo: para uma função com um único vale, a busca da seção áurea reduz o intervalo pelo fator 0,618 a cada passo reutilizando um dos dois valores da função, então precisa de uma avaliação por passo, e não precisa de derivadas. Bibliotecas (scipy.optimize) são mais rápidas e tratam muitos parâmetros, mas estas duas mostram as ideias.

# Implied volatility by bisection: the root of (model price - market price) in sigma
import math

def norm_cdf(x):
    return 0.5 * (1 + math.erf(x / math.sqrt(2)))

def bs_call(s, k, t, r, sigma):
    d1 = (math.log(s / k) + (r + sigma ** 2 / 2) * t) / (sigma * math.sqrt(t))
    return s * norm_cdf(d1) - k * math.exp(-r * t) * norm_cdf(d1 - sigma * math.sqrt(t))

def bisect_root(f, lo, hi, tol=1e-10):
    f_lo = f(lo)
    while hi - lo > tol:
        mid = (lo + hi) / 2
        f_mid = f(mid)
        if f_lo * f_mid <= 0:
            hi = mid
        else:
            lo, f_lo = mid, f_mid
    return (lo + hi) / 2

market_price = bs_call(100.0, 105.0, 1.0, 0.02, 0.27)           # a quote generated with a volatility of 27%
implied = bisect_root(lambda sigma: bs_call(100.0, 105.0, 1.0, 0.02, sigma) - market_price, 0.01, 2.0)
print(round(implied, 6))                                         # 0.27   recovered
# Golden-section search: one function evaluation per step, the interval shrinks by 0.618 each time
import math

def golden_section_minimize(f, lo, hi, tol=1e-8):
    ratio = (math.sqrt(5) - 1) / 2
    a, b = lo, hi
    c, d = b - ratio * (b - a), a + ratio * (b - a)
    fc, fd = f(c), f(d)
    while b - a > tol:
        if fc < fd:
            b, d, fd = d, c, fc
            c = b - ratio * (b - a)
            fc = f(c)
        else:
            a, c, fc = c, d, fd
            d = a + ratio * (b - a)
            fd = f(d)
    return (a + b) / 2

evaluations = []
def valley(x):
    evaluations.append(x)
    return (x - 1.3) ** 2 + 0.5

print(round(golden_section_minimize(valley, 0.0, 5.0), 5), len(evaluations) < 60)       # 1.3 True   about 40 evaluations for 1e-8

Calibrar uma volatilidade e fazê-lo com responsabilidade

Juntando as peças, calibrar uma única volatilidade a vários preços de opções significa minimizar a soma dos erros ao quadrado entre price_fn(strike, sigma) e os preços de mercado sobre sigma, com price_fn passada como argumento para que o mesmo código sirva a qualquer modelo. Para um modelo com vários parâmetros (o Heston tem cinco, o SABR três depois de fixar beta) a busca é multidimensional e pode cair num mínimo local: parta de várias estimativas iniciais, limite os parâmetros e prefira erros relativos ou em volatilidade implícita a erros brutos de preço para que opções baratas fora do dinheiro não sejam ignoradas. Depois verifique que os parâmetros ajustados são estáveis de um dia para o outro e que o modelo precifica bem opções em que não foi ajustado. Um ajuste perfeito mas instável é sobreajuste.

# Calibrate one volatility to three option prices: minimise the sum of squared errors over sigma
import math

def norm_cdf(x):
    return 0.5 * (1 + math.erf(x / math.sqrt(2)))

def bs_call(s, k, t, r, sigma):
    d1 = (math.log(s / k) + (r + sigma ** 2 / 2) * t) / (sigma * math.sqrt(t))
    return s * norm_cdf(d1) - k * math.exp(-r * t) * norm_cdf(d1 - sigma * math.sqrt(t))

def golden_section_minimize(f, lo, hi, tol=1e-8):
    ratio = (math.sqrt(5) - 1) / 2
    a, b = lo, hi
    c, d = b - ratio * (b - a), a + ratio * (b - a)
    fc, fd = f(c), f(d)
    while b - a > tol:
        if fc < fd:
            b, d, fd = d, c, fc
            c = b - ratio * (b - a)
            fc = f(c)
        else:
            a, c, fc = c, d, fd
            d = a + ratio * (b - a)
            fd = f(d)
    return (a + b) / 2

def calibrate_sigma(strikes, market_prices, price_fn, lo, hi):
    def error(sigma):
        return sum((price_fn(k, sigma) - p) ** 2 for k, p in zip(strikes, market_prices))
    return golden_section_minimize(error, lo, hi)

strikes = [90.0, 100.0, 110.0]
quotes = [bs_call(100.0, k, 1.0, 0.02, 0.23) for k in strikes]                  # market quotes made with sigma = 23%
sigma = calibrate_sigma(strikes, quotes, lambda k, s: bs_call(100.0, k, 1.0, 0.02, s), 0.01, 1.5)
print(round(sigma, 4))                                                           # 0.23
A calibração é onde os modelos erram em silêncioUma calibração que converge para um valor absurdo (uma volatilidade de 4.000 %, uma correlação exatamente 1) é um aviso, não um resultado. Imprima sempre os parâmetros ajustados e o erro residual, compare-os com os de ontem e recuse usar um ajuste cujo erro seja maior que o spread bid-ask das cotações. Mantenha os limites apertados o bastante para que o otimizador não vague por regiões onde a fórmula não vale e guarde os dados de mercado ajustados, para que a calibração possa ser reproduzida e auditada depois.
  • A calibração escolhe os parâmetros do modelo que minimizam uma soma (ponderada) de erros de preço ao quadrado; verifique a estabilidade entre dias e a precisão em cotações não usadas no ajuste.
  • A bisseção acha uma raiz (uma volatilidade implícita) dividindo ao meio um intervalo com mudança de sinal; a seção áurea acha o mínimo de uma função de um vale com uma avaliação por passo.
  • Passe a função de precificação como argumento, limite os parâmetros, parta de várias estimativas em modelos multiparâmetro e rejeite ajustes absurdos.

A seguir: redes neurais para previsão, aprender uma previsão a partir dos dados em vez de escrever a fórmula.

Lab: o que é avaliado

  • Medir o desajuste com sum_squared_error25 pts
  • Resolver f(x) = 0 com bisect_root25 pts
  • Achar o ponto mais baixo com golden_section_minimize25 pts
  • Ajustar o mercado com calibrate_sigma25 pts
Explorador · lab-139-1
▾ lab-139-1
…
bash · no navegador
learner@quantcraft:~/lab-139-1$
Python ainda não carregadoPythonLn 1, Col 1Spaces: 4UTF-8

Redefinir este tópico?

Tudo em lab-139-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