Options with a trapdoor
A barrier option is a call or put with an on-off switch. A knock-out option ceases to exist if the price touches the barrier; a knock-in option only comes into existence if it does. The barrier can be above the start (up) or below (down). Because the buyer gives up some of the payoff, barrier options are cheaper than plain ones, and they are popular for that reason, and in structured products. Like Asian options they depend on the whole path, so the natural tool is 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)Knock-out, knock-in, and the sum
On each path compute the plain call payoff max(S_T - K, 0) and the barrier flag. The knock-out call keeps the payoff where the barrier was *not* hit; the knock-in call keeps it where it was. Every path is one or the other, so *in + out = plain call*, path by path, a handy identity: price the easier one and get the other for free, and use it as a test of your code. np.where(hit, a, b) selects between arrays element by element.
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)) # TruePricing, and the discrete-monitoring bias
The price is the discounted mean payoff, as before. But watch the barrier check: a simulation looks at the price only on its grid dates, while a real continuous barrier is also breached *between* them. With few steps, simulated paths often cross the barrier unseen, so a knock-out call comes out too expensive and a knock-in too cheap. More steps reduce the bias, and so do corrections that shift the barrier by about 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- A barrier is hit if the path's maximum (up) or minimum (down) reaches it on any date; knock-out dies on a hit, knock-in is born on a hit.
- Per path, knock-in + knock-out = the plain call payoff;
np.where(hit, a, b)selects; barrier options are cheaper than plain ones. - Simulating on a grid misses crossings between dates, so knock-outs look too dear: use more steps or shift the barrier; beware of gap risk near the barrier.
Next up: antithetic variates, a way to get the same Monte Carlo accuracy from fewer simulations.
Lab · what's graded
- Detect the touch with
hit_barrier25 pts - Kill the option with
knock_out_call_payoff25 pts - Bring it to life with
knock_in_call_payoff25 pts - Turn payoffs into a price with
barrier_price25 pts