Des options avec une trappe
Une option à barrière est un call ou un put avec un interrupteur. Une option désactivante (knock-out) cesse d'exister si le prix touche la barrière ; une option activante (knock-in) n'existe que s'il la touche. La barrière peut être au-dessus du départ (up) ou en dessous (down). Comme l'acheteur renonce à une partie du gain, ces options coûtent moins cher que les options simples, d'où leur popularité, notamment dans les produits structurés. Comme les options asiatiques, elles dépendent de toute la trajectoire, et l'outil naturel est la simulation.
import numpy as np
paths = np.array([[100.0, 105.0, 112.0, 108.0], # stays below 115
[100.0, 98.0, 103.0, 110.0], # stays below 115
[100.0, 120.0, 95.0, 90.0]]) # touches 115 on day 1, then falls
hit_up = paths.max(axis=1) >= 115.0 # the maximum of each path against the barrier
print(hit_up) # [False False True]
print(paths.min(axis=1) <= 96.0) # a down barrier at 96: [False False True]# hit_barrier for both directions, with an error for anything else
import numpy as np
def hit_barrier(paths, barrier, direction):
if direction == "up":
return paths.max(axis=1) >= barrier
if direction == "down":
return paths.min(axis=1) <= barrier
raise ValueError("direction must be 'up' or 'down'")
paths = np.array([[100.0, 110.0, 105.0], [100.0, 109.9, 105.0]])
print(hit_barrier(paths, 110.0, "up")) # [ True False]: touching the barrier exactly counts
try:
hit_barrier(paths, 110.0, "sideways")
except ValueError as error:
print("rejected:", error)Désactivante, activante, et leur somme
Sur chaque trajectoire, calculez le gain du call simple max(S_T - K, 0) et l'indicateur de barrière. Le call désactivant garde le gain là où la barrière n'a *pas* été touchée ; le call activant le garde là où elle l'a été. Chaque trajectoire est dans l'un ou l'autre cas, donc *activant + désactivant = call simple*, trajectoire par trajectoire, une identité pratique : valorisez le plus simple et obtenez l'autre gratuitement, et servez-vous-en pour tester votre code. np.where(hit, a, b) choisit entre tableaux élément par élément.
import numpy as np
paths = np.array([[100.0, 105.0, 112.0, 108.0],
[100.0, 98.0, 103.0, 110.0],
[100.0, 120.0, 95.0, 130.0]])
strike, barrier = 100.0, 115.0
vanilla = np.maximum(paths[:, -1] - strike, 0.0)
hit = paths.max(axis=1) >= barrier
knock_out = np.where(hit, 0.0, vanilla)
knock_in = np.where(hit, vanilla, 0.0)
print(vanilla, knock_out, knock_in) # [ 8. 10. 30.] [ 8. 10. 0.] [ 0. 0. 30.]
print(np.allclose(knock_in + knock_out, vanilla)) # TrueValorisation, et biais de suivi discret
Le prix est le gain moyen actualisé, comme avant. Mais attention au test de barrière : une simulation ne regarde le prix qu'aux dates de sa grille, alors qu'une vraie barrière continue est aussi franchie *entre* elles. Avec peu de pas, les trajectoires simulées franchissent souvent la barrière sans qu'on le voie, si bien qu'un call désactivant paraît trop cher et un activant trop bon marché. Davantage de pas réduisent le biais, de même que des corrections qui décalent la barrière d'environ 0.5826 * sigma * sqrt(dt).
import numpy as np
def simulate_paths(s0, rate, sigma, t, steps, n_paths, rng):
dt = t / steps
z = rng.standard_normal((n_paths, steps))
moves = (rate - 0.5 * sigma ** 2) * dt + sigma * np.sqrt(dt) * z
return s0 * np.exp(np.hstack([np.zeros((n_paths, 1)), np.cumsum(moves, axis=1)]))
def knock_out_price(paths, strike, barrier, rate, t):
vanilla = np.maximum(paths[:, -1] - strike, 0.0)
payoff = np.where(paths.max(axis=1) >= barrier, 0.0, vanilla)
return float(np.exp(-rate * t) * payoff.mean())
rng = np.random.default_rng(1)
for steps in (12, 52, 252):
paths = simulate_paths(100.0, 0.05, 0.25, 1.0, steps, 40000, rng)
print(steps, round(knock_out_price(paths, 100.0, 130.0, 0.05, 1.0), 3)) # the price falls as monitoring gets finer- Une barrière est touchée si le maximum (up) ou le minimum (down) de la trajectoire l'atteint à une date quelconque ; la désactivante meurt au contact, l'activante naît au contact.
- Par trajectoire, activante + désactivante = le gain du call simple ;
np.where(hit, a, b)choisit ; les options à barrière sont moins chères que les simples. - Simuler sur une grille manque les franchissements entre les dates, donc les désactivantes paraissent trop chères : utilisez plus de pas ou décalez la barrière ; attention au risque de saut près de la barrière.
À suivre : les variables antithétiques, une façon d'obtenir la même précision Monte Carlo avec moins de simulations.
Lab : ce qui est noté
- Détecter le contact avec
hit_barrier25 pts - Tuer l'option avec
knock_out_call_payoff25 pts - La faire naître avec
knock_in_call_payoff25 pts - Transformer les gains en prix avec
barrier_price25 pts