Uma ação que só pode subir ou descer
Black–Scholes é uma fórmula; uma árvore binomial é um procedimento, e pode precificar o que a fórmula não consegue. Divide-se o tempo até o vencimento em passos pequenos. A cada passo a ação sobe um fator u ou cai um fator d, nada mais. Após n passos há n + 1 preços possíveis. Os pagamentos são conhecidos no vencimento e cada nó anterior vale a média descontada dos dois nós a que leva, de modo que a árvore se preenche de trás para frente até o presente.
# One step: a stock at 100 goes to 110 or 90. What is a call with strike 100 worth?
import math
spot, strike, rate, dt = 100, 100, 0.05, 1.0
u, d = 1.10, 0.90
p = (math.exp(rate * dt) - d) / (u - d) # the probability that makes the stock grow at the rate
up_value, down_value = max(spot * u - strike, 0), max(spot * d - strike, 0)
price = math.exp(-rate * dt) * (p * up_value + (1 - p) * down_value)
print(round(p, 4), round(up_value, 4), down_value) # 0.7564 10.0 0
print(round(price, 4)) # 7.1947A probabilidade não é uma previsão
O número p é escolhido para que a ação cresça, em média, à taxa livre de risco: p * u + (1 - p) * d = exp(rate * dt), o que dá p = (exp(rate * dt) - d) / (u - d). É a probabilidade neutra ao risco: um artifício que faz uma carteira replicante valer sua esperança descontada, não uma crença sobre para onde a ação vai. Para reproduzir a volatilidade observada, a árvore de Cox–Ross–Rubinstein (CRR) usa u = exp(sigma * sqrt(dt)) e d = 1 / u, de modo que uma alta seguida de uma queda volta ao mesmo preço e a árvore se recombina: n passos dão só n + 1 nós finais.
# The CRR parameters and the prices at expiry of a 3-step tree
import math
def crr_parameters(rate, sigma, t, steps):
dt = t / steps
u = math.exp(sigma * math.sqrt(dt))
d = 1 / u
p = (math.exp(rate * dt) - d) / (u - d)
return u, d, p
def terminal_prices(spot, u, d, steps):
return [spot * u ** j * d ** (steps - j) for j in range(steps + 1)]
u, d, p = crr_parameters(0.05, 0.2, 1, 3)
print(round(u, 4), round(d, 4), round(p, 4)) # 1.1224 0.8909 0.5438
print([round(price, 2) for price in terminal_prices(100, u, d, 3)])Trabalhar de trás para frente
No vencimento cada nó é apenas o pagamento, max(S - K, 0) para uma call. Uma coluna antes, cada valor é exp(-rate * dt) * (p * value_up + (1 - p) * value_down), e a coluna perde um nó. Após n varreduras resta um único valor, o preço de hoje. Em Python uma coluna é uma lista, e uma varredura é uma compreensão de lista sobre pares vizinhos. Com mais passos o preço converge para o de Black–Scholes e a árvore reproduz o que diz a fórmula.
# The tree price of a European call approaches the Black-Scholes price as steps grow
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 european_call_tree(spot, strike, rate, sigma, t, steps):
dt = t / steps
u = math.exp(sigma * math.sqrt(dt))
d = 1 / u
p = (math.exp(rate * dt) - d) / (u - d)
disc = math.exp(-rate * dt)
values = [max(spot * u ** j * d ** (steps - j) - strike, 0) for j in range(steps + 1)]
for _ in range(steps):
values = [disc * (p * values[j + 1] + (1 - p) * values[j]) for j in range(len(values) - 1)]
return values[0]
print("Black-Scholes", round(bs_call(100, 100, 0.05, 0.2, 1), 4))
for steps in (1, 5, 25, 100, 400):
print(steps, round(european_call_tree(100, 100, 0.05, 0.2, 1, steps), 4))O exercício antecipado
A força real da árvore é a opção que a fórmula não consegue precificar: uma opção americana pode ser exercida a qualquer momento. Em cada nó o titular compara dois valores, a esperança descontada de manter e o pagamento de exercer agora, e fica com o maior. Esse único max extra em cada nó é toda a mudança no algoritmo. Para uma call sobre ação sem dividendos o exercício antecipado nunca compensa, mas para uma put pode: uma put bem dentro do dinheiro vale mais em caixa hoje, então a put americana custa mais que a europeia.
# An American put against a European put: the early-exercise premium
import math
def tree(spot, strike, rate, sigma, t, steps, american):
dt = t / steps
u = math.exp(sigma * math.sqrt(dt))
d = 1 / u
p = (math.exp(rate * dt) - d) / (u - d)
disc = math.exp(-rate * dt)
values = [max(strike - spot * u ** j * d ** (steps - j), 0) for j in range(steps + 1)]
for i in range(steps - 1, -1, -1):
values = [disc * (p * values[j + 1] + (1 - p) * values[j]) for j in range(i + 1)]
if american:
values = [max(values[j], strike - spot * u ** j * d ** (i - j)) for j in range(i + 1)]
return values[0]
for spot in (80, 100, 120):
european = tree(spot, 100, 0.05, 0.2, 1, 200, False)
american = tree(spot, 100, 0.05, 0.2, 1, 200, True)
print(spot, round(european, 4), round(american, 4), "premium", round(american - european, 4))n passos custam cerca de n**2 / 2 atualizações de nós, e a convergência não é suave (o erro oscila conforme o strike cai entre os nós), por isso as árvores são verificadas comparando vários números de passos.- A cada passo a ação sobe
uou desced; a escolha CRRu = exp(sigma * sqrt(dt)),d = 1 / urecombina a árvore, entãonpassos têmn + 1nós finais. - Precificar de trás para frente: pagamentos no vencimento e depois
exp(-rate * dt) * (p * up + (1 - p) * down)por nó;pé a probabilidade neutra ao risco, não uma previsão. - Muitos passos convergem para Black–Scholes; uma opção americana acrescenta um
max(continuação, exercício)por nó, e é por isso que as árvores existem.
A seguir: os métodos de diferenças finitas, precificar resolvendo numa grade a equação diferencial parcial da opção.
Lab: o que é avaliado
- Definir os movimentos de alta e baixa com
crr_parameters25 pts - Listar os preços no vencimento com
terminal_prices25 pts - Percorrer a árvore de trás para frente com
european_call_tree25 pts - Permitir o exercício antecipado com
american_put_tree25 pts