Barrier Call (Binomial Tree)¶
Background¶
Exotic Options: Down-and-Out Barrier Call Pricing via Binomial Tree
This script implements the CRR binomial tree for a down-and-out barrier call option. The barrier condition is enforced at every node: if the stock price falls to or below the barrier H, the option value is set to zero (knocked out).
Mathematical Framework:
- CRR parameters: u = exp(σ√Δt), d = 1/u, q = (exp(rΔt) - d) / (u - d)
- Stock price at node (n,j): S_{n,j} = S₀ · u^j · d^(n-j)
- Backward induction with barrier:
V_{n,j} = exp(-rΔt)[q·V_{n+1,j+1} + (1-q)·V_{n+1,j}] if S_{n,j} > H
V_{n,j} = 0 if S_{n,j} ≤ H
References:
- Cox, Ross, Rubinstein (1979). Option pricing: A simplified approach.
- Rubinstein, Reiner (1991). Breaking down the barriers.
Code¶
```python """ Exotic Options: Down-and-Out Barrier Call Pricing via Binomial Tree
This script implements the CRR binomial tree for a down-and-out barrier call option. The barrier condition is enforced at every node: if the stock price falls to or below the barrier H, the option value is set to zero (knocked out).
Mathematical Framework: - CRR parameters: u = exp(σ√Δt), d = 1/u, q = (exp(rΔt) - d) / (u - d) - Stock price at node (n,j): S_{n,j} = S₀ · u^j · d^(n-j) - Backward induction with barrier: V_{n,j} = exp(-rΔt)[q·V_{n+1,j+1} + (1-q)·V_{n+1,j}] if S_{n,j} > H V_{n,j} = 0 if S_{n,j} ≤ H
References: - Cox, Ross, Rubinstein (1979). Option pricing: A simplified approach. - Rubinstein, Reiner (1991). Breaking down the barriers. """
import numpy as np import matplotlib.pyplot as plt
=============================================================================¶
1. Barrier Call Binomial Pricing Function¶
=============================================================================¶
def barrier_call_binomial(S, K, H, T, r, sigma, N): """ Price a down-and-out barrier call option using a CRR binomial tree.
Parameters
----------
S : float
Current stock price.
K : float
Strike price.
H : float
Down-and-out barrier level (H < S).
T : float
Time to maturity (years).
r : float
Risk-free rate (annualized).
sigma : float
Volatility (annualized).
N : int
Number of time steps.
Returns
-------
float
Option price.
"""
dt = T / N
u = np.exp(sigma * np.sqrt(dt))
d = 1 / u
p = (np.exp(r * dt) - d) / (u - d)
# Build stock price tree
ST = np.zeros((N + 1, N + 1))
for i in range(N + 1):
for j in range(i + 1):
ST[j, i] = S * (u ** j) * (d ** (i - j))
# Terminal payoffs with barrier enforcement
C = np.maximum(ST[:, N] - K, 0)
C[ST[:, N] <= H] = 0 # Knocked out
# Backward induction with barrier
for i in range(N - 1, -1, -1):
for j in range(i + 1):
if ST[j, i] > H:
C[j] = np.exp(-r * dt) * (p * C[j + 1] + (1 - p) * C[j])
else:
C[j] = 0 # Knocked out
return C[0]
=============================================================================¶
2. Vanilla Call Binomial (for comparison)¶
=============================================================================¶
def vanilla_call_binomial(S, K, T, r, sigma, N): """Price a European call using a CRR binomial tree (no barrier).""" dt = T / N u = np.exp(sigma * np.sqrt(dt)) d = 1 / u p = (np.exp(r * dt) - d) / (u - d)
ST = np.zeros((N + 1, N + 1))
for i in range(N + 1):
for j in range(i + 1):
ST[j, i] = S * (u ** j) * (d ** (i - j))
C = np.maximum(ST[:, N] - K, 0)
for i in range(N - 1, -1, -1):
for j in range(i + 1):
C[j] = np.exp(-r * dt) * (p * C[j + 1] + (1 - p) * C[j])
return C[0]
=============================================================================¶
3. Main: Price and Convergence Analysis¶
=============================================================================¶
if name == "main": # Parameters S = 100 # Current stock price K = 100 # Strike price H = 90 # Down-and-out barrier T = 1 # Time to maturity r = 0.05 # Risk-free rate sigma = 0.2 # Volatility
# --- Single Price ---
N = 500
barrier_price = barrier_call_binomial(S, K, H, T, r, sigma, N)
vanilla_price = vanilla_call_binomial(S, K, T, r, sigma, N)
print("=" * 60)
print("DOWN-AND-OUT BARRIER CALL PRICING (Binomial Tree)")
print("=" * 60)
print(f"Parameters: S={S}, K={K}, H={H}, T={T}, r={r}, σ={sigma}")
print(f"Number of steps: N={N}")
print(f"\nVanilla Call Price: {vanilla_price:.4f}")
print(f"Down-and-Out Barrier Call: {barrier_price:.4f}")
print(f"Barrier Discount: {vanilla_price - barrier_price:.4f}")
print(f"Barrier/Vanilla Ratio: {barrier_price/vanilla_price:.4f}")
# --- Convergence Analysis ---
N_values = list(range(50, 501, 10))
barrier_prices = []
vanilla_prices = []
for n in N_values:
barrier_prices.append(barrier_call_binomial(S, K, H, T, r, sigma, n))
vanilla_prices.append(vanilla_call_binomial(S, K, T, r, sigma, n))
# Plot convergence
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Panel 1: Convergence of barrier and vanilla prices
axes[0].plot(N_values, barrier_prices, 'b-', alpha=0.7, label='Barrier Call')
axes[0].plot(N_values, vanilla_prices, 'r-', alpha=0.7, label='Vanilla Call')
axes[0].set_xlabel('Number of Steps (N)')
axes[0].set_ylabel('Option Price')
axes[0].set_title('Convergence: Barrier vs Vanilla Call')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# Panel 2: Oscillation detail for barrier option
axes[1].plot(N_values, barrier_prices, 'b-o', markersize=2, alpha=0.7)
axes[1].axhline(y=barrier_prices[-1], color='k', linestyle='--', alpha=0.5,
label=f'N={N_values[-1]}: {barrier_prices[-1]:.4f}')
axes[1].set_xlabel('Number of Steps (N)')
axes[1].set_ylabel('Barrier Call Price')
axes[1].set_title('Barrier Call Convergence (Oscillation Detail)')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('barrier_call_binomial_convergence.png', dpi=150, bbox_inches='tight')
plt.show()
# --- Sensitivity to Barrier Level ---
H_values = np.arange(70, 100, 2)
prices_by_barrier = [barrier_call_binomial(S, K, h, T, r, sigma, 300) for h in H_values]
plt.figure(figsize=(8, 5))
plt.plot(H_values, prices_by_barrier, 'b-o', markersize=4)
plt.axhline(y=vanilla_price, color='r', linestyle='--', label='Vanilla Call')
plt.xlabel('Barrier Level (H)')
plt.ylabel('Barrier Call Price')
plt.title('Down-and-Out Call Price vs Barrier Level')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('barrier_call_sensitivity.png', dpi=150, bbox_inches='tight')
plt.show()
print("\n--- Barrier Sensitivity ---")
print(f"{'Barrier H':>10} {'Price':>10} {'% of Vanilla':>14}")
print("-" * 36)
for h, p in zip(H_values[::3], prices_by_barrier[::3]):
print(f"{h:10.0f} {p:10.4f} {p/vanilla_price*100:13.1f}%")
```
Exercises¶
Exercise 1. Define a down-and-out barrier call. Write the backward induction formula with barrier enforcement at each node.
Solution to Exercise 1
A down-and-out call pays \(\max(S_T - K, 0)\) if \(S_t > H\) for all \(t\). In the binomial tree, at each node \((n, j)\): if \(S_{n,j} \le H\), set \(V_{n,j} = 0\) (knocked out); otherwise \(V_{n,j} = e^{-r\Delta t}[qV_{n+1,j+1} + (1-q)V_{n+1,j}]\).
Exercise 2. Why does the binomial tree price of a barrier option converge more slowly than for a vanilla option?
Solution to Exercise 2
The barrier \(H\) rarely falls exactly on a node level \(S_0 u^j d^{M-j}\). The effective barrier oscillates between adjacent node levels as \(M\) changes, causing price oscillations with convergence rate \(O(1/\sqrt{M})\) instead of \(O(1/M)\).
Exercise 3. Describe the Broadie-Glasserman barrier correction for improving convergence.
Solution to Exercise 3
Replace the barrier \(H\) with an adjusted barrier \(H_M = H \exp(\beta\sigma\sqrt{\Delta t})\) where \(\beta \approx 0.5826\). This correction accounts for the average displacement of the barrier from the nearest node level, accelerating convergence to \(O(1/M)\).
Exercise 4. Derive the in-out parity for barrier options: \(C_{\text{in}} + C_{\text{out}} = C_{\text{vanilla}}\).
Solution to Exercise 4
A long DI call plus a long DO call always pays \(\max(S_T - K, 0)\): if the barrier is hit, the DI activates; if not, the DO survives. By no-arbitrage, the sum of their prices equals the vanilla call price.