Usar a fórmula ao contrário
A fórmula de Black–Scholes transforma uma volatilidade em preço. No mercado ocorre o oposto: o preço é cotado e a volatilidade é desconhecida. A volatilidade implícita é o sigma para o qual a fórmula reproduz o preço de mercado. Não há inversa algébrica, então se busca: calcula-se o preço para um sigma de teste, olha-se o erro de preço (modelo menos mercado) e move-se sigma até zerá-lo. A busca mais simples testa uma grade de valores e fica com o melhor.
import math
def norm_cdf(x):
return 0.5 * (1 + math.erf(x / math.sqrt(2)))
def bs_call(spot, strike, rate, sigma, t):
d1 = (math.log(spot / strike) + (rate + 0.5 * sigma ** 2) * t) / (sigma * math.sqrt(t))
return spot * norm_cdf(d1) - strike * math.exp(-rate * t) * norm_cdf(d1 - sigma * math.sqrt(t))
market = 8.0 # the quoted price of the 100-strike, 6-month call on a stock at 100
for sigma in (0.10, 0.15, 0.20, 0.25, 0.30):
model = bs_call(100, 100, 0.05, sigma, 0.5)
print(sigma, round(model, 4), "error", round(model - market, 4))Por que há apenas uma resposta
O preço da call sobe de forma contínua com sigma: o vega é positivo, então o erro cruza zero uma só vez. A resposta é única e indica para onde mover: se o modelo está caro demais, reduza sigma. Só há solução se o preço de mercado estiver estritamente entre o *piso intrínseco* max(S - K * exp(-rT), 0) (volatilidade zero) e o spot S (volatilidade infinita). Fora desses limites a cotação contradiz a ausência de arbitragem, e a resposta certa é um erro, não um número.
# The bounds: no volatility can explain a price outside them
import math
def norm_cdf(x):
return 0.5 * (1 + math.erf(x / math.sqrt(2)))
def bs_call(spot, strike, rate, sigma, t):
d1 = (math.log(spot / strike) + (rate + 0.5 * sigma ** 2) * t) / (sigma * math.sqrt(t))
return spot * norm_cdf(d1) - strike * math.exp(-rate * t) * norm_cdf(d1 - sigma * math.sqrt(t))
spot, strike, rate, t = 100, 90, 0.05, 1
floor = max(spot - strike * math.exp(-rate * t), 0)
print(round(floor, 4), spot) # 14.3894 100
print(round(bs_call(spot, strike, rate, 0.0001, t), 4)) # 14.3894 tiny volatility: the floor
print(round(bs_call(spot, strike, rate, 5.0, t), 4)) # 98.8513 huge volatility: close to the spotDividir o intervalo ao meio
Uma grade é grosseira: a resposta é tão fina quanto a grade. Melhor usar o sinal do erro. Parte-se de sigma entre um valor muito baixo e um muito alto, toma-se o ponto médio e olha-se o erro: positivo, o meio está alto demais, fica-se com a metade de baixo; negativo, a de cima. Cada passo divide o intervalo por dois, então cerca de trinta passos dão oito dígitos. O próximo tópico dá nome a isso e o generaliza; aqui é a ferramenta que lê o mercado.
# Implied volatility by halving the interval, then reprice to check it
import math
def norm_cdf(x):
return 0.5 * (1 + math.erf(x / math.sqrt(2)))
def bs_call(spot, strike, rate, sigma, t):
d1 = (math.log(spot / strike) + (rate + 0.5 * sigma ** 2) * t) / (sigma * math.sqrt(t))
return spot * norm_cdf(d1) - strike * math.exp(-rate * t) * norm_cdf(d1 - sigma * math.sqrt(t))
def implied_vol(price, spot, strike, rate, t, tol=1e-8):
low, high = 0.0001, 5.0
while high - low > tol:
mid = (low + high) / 2
if bs_call(spot, strike, rate, mid, t) > price:
high = mid
else:
low = mid
return (low + high) / 2
iv = implied_vol(8.0, 100, 100, 0.05, 0.5)
print(round(iv, 6)) # 0.240534
print(round(bs_call(100, 100, 0.05, iv, 0.5), 6)) # 8.0O sorriso
Se Black–Scholes fosse exato, todas as opções sobre a mesma ação e data implicariam a mesma volatilidade. Não implicam: strikes baixos costumam implicar mais que os altos (o *skew*), ou as duas pontas mais que o centro (o *sorriso*). O padrão mostra o que o modelo deixa de captar (saltos, caudas pesadas, demanda por proteção), e por isso as opções são cotadas em volatilidade implícita: um número único e comparável entre strikes e vencimentos, do qual o preço decorre.
# A smile: market prices at five strikes, converted to implied volatilities
import math
def norm_cdf(x):
return 0.5 * (1 + math.erf(x / math.sqrt(2)))
def bs_call(spot, strike, rate, sigma, t):
d1 = (math.log(spot / strike) + (rate + 0.5 * sigma ** 2) * t) / (sigma * math.sqrt(t))
return spot * norm_cdf(d1) - strike * math.exp(-rate * t) * norm_cdf(d1 - sigma * math.sqrt(t))
def implied_vol(price, spot, strike, rate, t, tol=1e-8):
low, high = 0.0001, 5.0
while high - low > tol:
mid = (low + high) / 2
low, high = (low, mid) if bs_call(spot, strike, rate, mid, t) > price else (mid, high)
return (low + high) / 2
strikes = [90, 95, 100, 105, 110]
quotes = [bs_call(100, k, 0.05, v, 0.5) for k, v in zip(strikes, [0.28, 0.24, 0.21, 0.22, 0.25])] # prices from a made-up smile
for strike, quote in zip(strikes, quotes):
print(strike, round(quote, 4), round(implied_vol(quote, 100, strike, 0.05, 0.5), 4))- A volatilidade implícita é o
sigmaque igualabs_callao preço de mercado: encontra-se por busca, pois não há fórmula inversa. - O preço sobe com
sigma: há uma resposta e o sinal do erro diz para onde mover; fora dos limites não há nenhuma. - Dividir o intervalo converge rápido (cerca de 30 passos para 8 dígitos); o sorriso é a volatilidade implícita contra o strike.
A seguir: a busca de raízes por bisseção e Newton, o método geral por trás desta busca e outro bem mais rápido.
Lab: o que é avaliado
- Comparar modelo e mercado com
call_price_error25 pts - Percorrer uma grade de volatilidades com
implied_vol_scan25 pts - Inverter a fórmula com
implied_vol25 pts - Ler um sorriso inteiro com
implied_vol_smile25 pts