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 muchDuas 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-8Calibrar 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 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