Une action qui ne peut que monter ou descendre
Black–Scholes est une formule ; un arbre binomial est une procédure, et elle peut valoriser ce que la formule ne peut pas. On découpe la durée jusqu'à l'échéance en petits pas. À chaque pas, l'action monte d'un facteur u ou baisse d'un facteur d, rien d'autre. Après n pas, il y a n + 1 prix possibles. Les gains sont connus à l'échéance, et chaque nœud antérieur vaut la moyenne actualisée des deux nœuds qu'il prolonge, ce qui permet de remplir l'arbre à rebours jusqu'au présent.
# 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 probabilité n'est pas une prévision
Le nombre p est choisi pour que l'action croisse en moyenne au taux sans risque : p * u + (1 - p) * d = exp(rate * dt), d'où p = (exp(rate * dt) - d) / (u - d). C'est la probabilité risque-neutre : un artifice qui rend un portefeuille de réplication égal à son espérance actualisée, non une opinion sur l'évolution de l'action. Pour reproduire la volatilité observée, l'arbre de Cox–Ross–Rubinstein (CRR) prend u = exp(sigma * sqrt(dt)) et d = 1 / u ; une hausse suivie d'une baisse ramène au même prix et l'arbre se recombine : n pas ne donnent que n + 1 nœuds finaux.
# 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)])Remonter à rebours
À l'échéance, chaque nœud n'est que le gain, max(S - K, 0) pour un call. Une colonne plus tôt, chaque valeur est exp(-rate * dt) * (p * value_up + (1 - p) * value_down), et la colonne perd un nœud. Après n balayages, il reste une seule valeur, le prix aujourd'hui. En Python une colonne est une liste, et un balayage est une compréhension de liste sur les paires voisines. Quand le nombre de pas augmente, le prix converge vers celui de Black–Scholes : l'arbre retrouve ce que dit la formule.
# 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))L'exercice anticipé
Le vrai atout de l'arbre est l'option que la formule ne sait pas valoriser : une option américaine peut être exercée à tout moment. À chaque nœud, le détenteur compare deux valeurs, l'espérance actualisée de la détention et le gain de l'exercice immédiat, et prend la plus grande. Ce seul max supplémentaire à chaque nœud est tout le changement de l'algorithme. Pour un call sur une action sans dividende, l'exercice anticipé ne rapporte jamais, mais pour un put il le peut : un put très dans la monnaie vaut plus en espèces aujourd'hui, donc le put américain coûte plus que l'européen.
# 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 pas coûtent environ n**2 / 2 mises à jour de nœuds, et la convergence n'est pas régulière (l'erreur oscille selon la position du prix d'exercice entre les nœuds) ; on vérifie donc un arbre en comparant plusieurs nombres de pas.- À chaque pas l'action monte de
uou baisse ded; le choix CRRu = exp(sigma * sqrt(dt)),d = 1 / urecombine l'arbre, doncnpas ontn + 1nœuds finaux. - Valoriser à rebours : gains à l'échéance, puis
exp(-rate * dt) * (p * up + (1 - p) * down)par nœud ;pest la probabilité risque-neutre, pas une prévision. - Beaucoup de pas convergent vers Black–Scholes ; une option américaine ajoute un
max(continuation, exercice)par nœud, et c'est la raison d'être des arbres.
À suivre : les méthodes de différences finies, valoriser en résolvant sur une grille l'équation aux dérivées partielles de l'option.
Lab : ce qui est noté
- Fixer les mouvements haussier et baissier avec
crr_parameters25 pts - Lister les prix à l'échéance avec
terminal_prices25 pts - Remonter l'arbre à rebours avec
european_call_tree25 pts - Autoriser l'exercice anticipé avec
american_put_tree25 pts