Una acción que solo puede subir o bajar
Black–Scholes es una fórmula; un árbol binomial es un procedimiento, y puede valorar lo que la fórmula no puede. Se divide el tiempo hasta el vencimiento en pasos pequeños. En cada paso la acción sube un factor u o baja un factor d, nada más. Tras n pasos hay n + 1 precios posibles. Los pagos se conocen al vencimiento y cada nodo anterior vale el promedio descontado de los dos nodos a los que lleva, así que el árbol se rellena hacia atrás hasta el 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.1947La probabilidad no es un pronóstico
El número p se elige para que la acción crezca en promedio al tipo libre de riesgo: p * u + (1 - p) * d = exp(rate * dt), de donde p = (exp(rate * dt) - d) / (u - d). Es la probabilidad neutral al riesgo: un artificio que hace que una cartera replicante valga su esperanza descontada, no una creencia sobre adónde irá la acción. Para reproducir la volatilidad observada, el árbol de Cox–Ross–Rubinstein (CRR) usa u = exp(sigma * sqrt(dt)) y d = 1 / u, así una subida seguida de una bajada vuelve al mismo precio y el árbol se recombina: n pasos solo dan n + 1 nodos finales.
# 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)])Trabajar hacia atrás
Al vencimiento cada nodo es solo el pago, max(S - K, 0) para un call. Una columna antes, cada valor es exp(-rate * dt) * (p * value_up + (1 - p) * value_down), y la columna pierde un nodo. Tras n barridos queda un solo valor, el precio de hoy. En Python una columna es una lista, y un barrido es una comprensión de lista sobre pares vecinos. Al crecer el número de pasos, el precio converge al de Black–Scholes y el árbol reproduce lo que dice la 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))El ejercicio anticipado
La verdadera fuerza del árbol es la opción que la fórmula no sabe valorar: una opción americana puede ejercerse en cualquier momento. En cada nodo el titular compara dos valores, la esperanza descontada de mantener y el pago de ejercer ahora, y toma el mayor. Ese único max adicional en cada nodo es todo el cambio del algoritmo. En un call sobre una acción sin dividendos el ejercicio anticipado nunca compensa, pero en un put sí puede: un put muy en el dinero vale más en efectivo hoy, de modo que el put americano cuesta más que el europeo.
# 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 pasos cuestan unas n**2 / 2 actualizaciones de nodos, y la convergencia no es suave (el error oscila según el strike caiga entre nodos), por lo que los árboles se comprueban comparando varios números de pasos.- En cada paso la acción sube
uo bajad; la elección CRRu = exp(sigma * sqrt(dt)),d = 1 / urecombina el árbol, asínpasos tienenn + 1nodos finales. - Valorar hacia atrás: pagos al vencimiento, luego
exp(-rate * dt) * (p * up + (1 - p) * down)por nodo;pes la probabilidad neutral al riesgo, no un pronóstico. - Muchos pasos convergen a Black–Scholes; una opción americana añade un
max(continuación, ejercicio)por nodo, y para eso existen los árboles.
A continuación: los métodos de diferencias finitas, valorar resolviendo en una malla la ecuación en derivadas parciales de la opción.
Lab: qué se califica
- Fijar los movimientos al alza y a la baja con
crr_parameters25 pts - Listar los precios al vencimiento con
terminal_prices25 pts - Recorrer el árbol hacia atrás con
european_call_tree25 pts - Permitir el ejercicio anticipado con
american_put_tree25 pts