European Option Pricing with Binomial Trees¶
This notebook covers put-call parity and European option pricing with binomial trees, including ATM calls and puts, delta hedging, and related exercises in derivative pricing.
import numpy as np
import pandas as pd
# Given Parameters
S0 = 100
K = 100
r = 0.05
sigma = 0.20
T = 0.25
N = 100
dt = T/N
u = np.exp(sigma*np.sqrt(dt))
d = 1/u
p = (np.exp(r*dt)-d)/(u-d)
disc = np.exp(-r*dt)
Step 1: Put-Call Parity¶
Q5 – European Call & Put (Binomial Tree)
# Terminal stock prices
ST = np.array([S0*(u**j)*(d**(N-j)) for j in range(N+1)])
# Payoffs
call = np.maximum(ST-K,0)
put = np.maximum(K-ST,0)
# Backward induction
for i in range(N):
call = disc*(p*call[1:] + (1-p)*call[:-1])
put = disc*(p*put[1:] + (1-p)*put[:-1])
C_eur = round(call[0],2)
P_eur = round(put[0],2)
pd.DataFrame({
"Option":["European Call","European Put"],
"Price":[C_eur,P_eur]
})
| Option | Price | |
|---|---|---|
| 0 | European Call | 4.61 |
| 1 | European Put | 3.36 |
Q5 (a) Since (S_0 = 100), the ATM strike is (K = 100). Process: build binomial tree, terminal payoffs (max(S_T - K, 0)) (call) and (max(K - S_T, 0)) (put), then backward induction with risk-neutral probability (p).
(b) Number of steps: A sufficiently large number of steps ((N = 100)) was chosen to ensure numerical convergence of the binomial tree while keeping computational complexity manageable. Increasing (N) improves approximation accuracy toward the continuous-time Black–Scholes value.
Q6 – Delta (European Options)
# Delta from tree: (V_up - V_down)/(S_up - S_down) at first time step
ST = np.array([S0*(u**j)*(d**(N-j)) for j in range(N+1)])
call_vals = np.maximum(ST-K, 0)
put_vals = np.maximum(K-ST, 0)
for i in range(N-1): # stop one step early → 2 nodes at t = dt
call_vals = disc*(p*call_vals[1:] + (1-p)*call_vals[:-1])
put_vals = disc*(p*put_vals[1:] + (1-p)*put_vals[:-1])
# call_vals[0]=V_down, call_vals[1]=V_up at t=dt
S_up, S_down = S0*u, S0*d
delta_call = round((call_vals[1]-call_vals[0])/(S_up-S_down), 4)
delta_put = round((put_vals[1]-put_vals[0])/(S_up-S_down), 4)
pd.DataFrame({
"Option":["European Call","European Put"],
"Delta":[delta_call,delta_put]
})
| Option | Delta | |
|---|---|---|
| 0 | European Call | 0.5693 |
| 1 | European Put | -0.4307 |
Q7 – Volatility Sensitivity (σ = 25%)
sigma_new = 0.25
u_new = np.exp(sigma_new*np.sqrt(dt))
d_new = 1/u_new
p_new = (np.exp(r*dt)-d_new)/(u_new-d_new)
ST_new = np.array([S0*(u_new**j)*(d_new**(N-j)) for j in range(N+1)])
call_new = np.maximum(ST_new-K,0)
put_new = np.maximum(K-ST_new,0)
for i in range(N):
call_new = disc*(p_new*call_new[1:] + (1-p_new)*call_new[:-1])
put_new = disc*(p_new*put_new[1:] + (1-p_new)*put_new[:-1])
C_new = round(call_new[0],2)
P_new = round(put_new[0],2)
pd.DataFrame({
"Option":["Call","Put"],
"Price (20%)":[C_eur,P_eur],
"Price (25%)":[C_new,P_new],
"Change":[round(C_new-C_eur,2),round(P_new-P_eur,2)]
})
| Option | Price (20%) | Price (25%) | Change | |
|---|---|---|---|---|
| 0 | Call | 4.61 | 5.59 | 0.98 |
| 1 | Put | 3.36 | 4.34 | 0.98 |
Q8 – American Call & Put
# Rebuild tree
ST = np.array([S0*(u**j)*(d**(N-j)) for j in range(N+1)])
call = np.maximum(ST-K,0)
put = np.maximum(K-ST,0)
for i in range(N):
ST = np.array([S0*(u**j)*(d**(N-1-i-j)) for j in range(N-i)])
call = np.maximum(disc*(p*call[1:] + (1-p)*call[:-1]), ST-K)
put = np.maximum(disc*(p*put[1:] + (1-p)*put[:-1]), K-ST)
C_am = round(call[0],2)
P_am = round(put[0],2)
pd.DataFrame({
"Option":["American Call","American Put"],
"Price":[C_am,P_am]
})
| Option | Price | |
|---|---|---|
| 0 | American Call | 4.61 |
| 1 | American Put | 3.47 |
Q9 – Delta (American Options)
# American Delta from tree: option values at first time step (2 nodes)
ST_am = np.array([S0*(u**j)*(d**(N-j)) for j in range(N+1)])
call_am = np.maximum(ST_am-K, 0)
put_am = np.maximum(K-ST_am, 0)
for i in range(N-1):
ST_step = np.array([S0*(u**j)*(d**(N-1-i-j)) for j in range(N-i)])
call_am = np.maximum(disc*(p*call_am[1:] + (1-p)*call_am[:-1]), ST_step-K)
put_am = np.maximum(disc*(p*put_am[1:] + (1-p)*put_am[:-1]), K-ST_step)
# call_am[0]=V_down, call_am[1]=V_up at t=dt
S_up_am, S_down_am = S0*u, S0*d
delta_call_am = round((call_am[1]-call_am[0])/(S_up_am-S_down_am), 4)
delta_put_am = round((put_am[1]-put_am[0])/(S_up_am-S_down_am), 4)
pd.DataFrame({
"Option":["American Call","American Put"],
"Delta":[delta_call_am,delta_put_am]
})
| Option | Delta | |
|---|---|---|
| 0 | American Call | 0.5693 |
| 1 | American Put | -0.4498 |
Q10 – Volatility Sensitivity (American)
# American pricing with new volatility
ST_new = np.array([S0*(u_new**j)*(d_new**(N-j)) for j in range(N+1)])
call_new = np.maximum(ST_new-K,0)
put_new = np.maximum(K-ST_new,0)
for i in range(N):
ST_new = np.array([S0*(u_new**j)*(d_new**(N-1-i-j)) for j in range(N-i)])
call_new = np.maximum(disc*(p_new*call_new[1:] + (1-p_new)*call_new[:-1]), ST_new-K)
put_new = np.maximum(disc*(p_new*put_new[1:] + (1-p_new)*put_new[:-1]), K-ST_new)
C_am_new = round(call_new[0],2)
P_am_new = round(put_new[0],2)
pd.DataFrame({
"Option":["American Call","American Put"],
"Price (20%)":[C_am,P_am],
"Price (25%)":[C_am_new,P_am_new],
"Change":[round(C_am_new-C_am,2),round(P_am_new-P_am,2)]
})
| Option | Price (20%) | Price (25%) | Change | |
|---|---|---|---|---|
| 0 | American Call | 4.61 | 5.59 | 0.98 |
| 1 | American Put | 3.47 | 4.45 | 0.98 |
Step 2: Trinomial Tree¶
(same parameters: S0=100, r=5%, σ=20%, T=3 months)
Strike selection: Five strike prices were selected using moneyness levels 90%, 95%, 100%, 105%, and 110% of spot (S_0 = 100), giving (K = 90, 95, 100, 105, 110). For calls: Deep ITM to Deep OTM; for puts: Deep OTM to Deep ITM.
# Trinomial tree: parameters and pricing functions (N steps, recombining)
# dx = sigma*sqrt(2*dt), u = exp(dx), d = 1/u; match risk-neutral mean and variance
def trinomial_params(r, sigma, T, N):
dt = T / N
dx = sigma * np.sqrt(2 * dt)
u = np.exp(dx)
d = 1 / u
nu = r - 0.5 * sigma**2
p_u = 0.5 * ((sigma**2 * dt + nu**2 * dt**2) / (dx**2) + nu * dt / dx)
p_d = 0.5 * ((sigma**2 * dt + nu**2 * dt**2) / (dx**2) - nu * dt / dx)
p_m = 1 - p_u - p_d
disc = np.exp(-r * dt)
return u, d, p_u, p_m, p_d, disc, N
def price_trinomial(S0, K, r, sigma, T, N, option='call', style='european'):
u, d, p_u, p_m, p_d, disc, _ = trinomial_params(r, sigma, T, N)
# Terminal nodes: j = 0..2*N, stock = S0 * u^(j-N)
j_nodes = np.arange(2*N + 1)
S_T = S0 * (u ** (j_nodes - N))
if option == 'call':
V = np.maximum(S_T - K, 0.0)
else:
V = np.maximum(K - S_T, 0.0)
# Backward induction
for n in range(N - 1, -1, -1):
n_nodes = 2 * n + 1
S_n = S0 * (u ** (np.arange(n_nodes) - n))
cont = disc * (p_d * V[:n_nodes] + p_m * V[1:n_nodes+1] + p_u * V[2:n_nodes+2])
if style == 'american':
if option == 'call':
intrinsic = np.maximum(S_n - K, 0.0)
else:
intrinsic = np.maximum(K - S_n, 0.0)
V = np.maximum(cont, intrinsic)
else:
V = cont.copy()
return V[0]
# 5 strikes by moneyness: 90%, 95%, 100%, 105%, 110% (K/S0)
strikes = np.array([90, 95, 100, 105, 110]) # K = S0 * moneyness
moneyness_pct = strikes / S0 * 100
Q15 – European Call options (5 strikes)
# (a) Price European calls for 5 strikes (Deep OTM, OTM, ATM, ITM, Deep ITM)
eur_calls = np.array([price_trinomial(S0, K, r, sigma, T, N, option='call', style='european') for K in strikes])
# For a call: low K = ITM (S>K), high K = OTM. So K=90 Deep ITM, K=110 Deep OTM.
q15_table = pd.DataFrame({
"Strike K": strikes,
"Moneyness (K/S0)": moneyness_pct,
"Type": ["Deep ITM", "ITM", "ATM", "OTM", "Deep OTM"],
"European Call": np.round(eur_calls, 2)
})
q15_table
| Strike K | Moneyness (K/S0) | Type | European Call | |
|---|---|---|---|---|
| 0 | 90 | 90.0 | Deep ITM | 11.67 |
| 1 | 95 | 95.0 | ITM | 7.72 |
| 2 | 100 | 100.0 | ATM | 4.61 |
| 3 | 105 | 105.0 | OTM | 2.48 |
| 4 | 110 | 110.0 | Deep OTM | 1.19 |
Q16 – European Put options (5 strikes)
# (a) Price European puts for the same 5 strikes
eur_puts = np.array([price_trinomial(S0, K, r, sigma, T, N, option='put', style='european') for K in strikes])
q16_table = pd.DataFrame({
"Strike K": strikes,
"Moneyness (K/S0)": moneyness_pct,
"Type": ["Deep OTM", "OTM", "ATM", "ITM", "Deep ITM"],
"European Put": np.round(eur_puts, 2)
})
q16_table
| Strike K | Moneyness (K/S0) | Type | European Put | |
|---|---|---|---|---|
| 0 | 90 | 90.0 | Deep OTM | 0.55 |
| 1 | 95 | 95.0 | OTM | 1.54 |
| 2 | 100 | 100.0 | ATM | 3.37 |
| 3 | 105 | 105.0 | ITM | 6.18 |
| 4 | 110 | 110.0 | Deep ITM | 9.83 |
Q17 – American Call options (5 strikes)
# (a) Price American calls for the same 5 strikes
am_calls = np.array([price_trinomial(S0, K, r, sigma, T, N, option='call', style='american') for K in strikes])
q17_table = pd.DataFrame({
"Strike K": strikes,
"Moneyness (K/S0)": moneyness_pct,
"Type": ["Deep ITM", "ITM", "ATM", "OTM", "Deep OTM"],
"American Call": np.round(am_calls, 2),
"European Call": np.round(eur_calls, 2),
"Diff (Am - Eur)": np.round(am_calls - eur_calls, 2)
})
q17_table
| Strike K | Moneyness (K/S0) | Type | American Call | European Call | Diff (Am - Eur) | |
|---|---|---|---|---|---|---|
| 0 | 90 | 90.0 | Deep ITM | 11.67 | 11.67 | 0.0 |
| 1 | 95 | 95.0 | ITM | 7.72 | 7.72 | 0.0 |
| 2 | 100 | 100.0 | ATM | 4.61 | 4.61 | 0.0 |
| 3 | 105 | 105.0 | OTM | 2.48 | 2.48 | 0.0 |
| 4 | 110 | 110.0 | Deep OTM | 1.19 | 1.19 | 0.0 |
Q18 – American Put options (5 strikes)
# (a) Price American puts for the same 5 strikes
am_puts = np.array([price_trinomial(S0, K, r, sigma, T, N, option='put', style='american') for K in strikes])
q18_table = pd.DataFrame({
"Strike K": strikes,
"Moneyness (K/S0)": moneyness_pct,
"Type": ["Deep OTM", "OTM", "ATM", "ITM", "Deep ITM"],
"American Put": np.round(am_puts, 2),
"European Put": np.round(eur_puts, 2),
"Diff (Am - Eur)": np.round(am_puts - eur_puts, 2)
})
q18_table
| Strike K | Moneyness (K/S0) | Type | American Put | European Put | Diff (Am - Eur) | |
|---|---|---|---|---|---|---|
| 0 | 90 | 90.0 | Deep OTM | 0.56 | 0.55 | 0.01 |
| 1 | 95 | 95.0 | OTM | 1.58 | 1.54 | 0.04 |
| 2 | 100 | 100.0 | ATM | 3.48 | 3.37 | 0.11 |
| 3 | 105 | 105.0 | ITM | 6.43 | 6.18 | 0.25 |
| 4 | 110 | 110.0 | Deep ITM | 10.33 | 9.83 | 0.50 |
Step 3: Dynamic Delta Hedging and Asian Option¶
Data: (S_0 = 180), (r = 2%), (sigma = 25%), (T = 6) months, (K = 182) (Q25–Q26); Asian ATM uses (K = S_0 = 180).
Q25 – European Put, 3-step binomial tree
S0_q25, r_q25, sigma_q25, T_q25, K_q25 = 180, 0.02, 0.25, 0.5, 182
N3 = 3
dt3 = T_q25 / N3
u3 = np.exp(sigma_q25 * np.sqrt(dt3))
d3 = 1 / u3
p3 = (np.exp(r_q25 * dt3) - d3) / (u3 - d3)
disc3 = np.exp(-r_q25 * dt3)
# 3-step tree: S[n][j] = stock price at step n, node j (j = 0..n for binomial: j ups)
def build_binomial_3(S0, u, d, N):
S = [[S0 * (u**j) * (d**(n-j)) for j in range(n+1)] for n in range(N+1)]
return S
S_tree = build_binomial_3(S0_q25, u3, d3, N3)
# Put payoffs at maturity (step 3)
put_T = [max(K_q25 - S_tree[N3][j], 0) for j in range(N3+1)]
# Backward induction (European)
put_vals = put_T.copy()
for n in range(N3-1, -1, -1):
put_vals = [disc3 * (p3 * put_vals[j+1] + (1-p3) * put_vals[j]) for j in range(n+1)]
P_eur_3step = round(put_vals[0], 2)
from IPython.display import display # pyright: ignore[reportMissingImports]
display(pd.DataFrame({"Option": ["European Put (3-step)"], "Price": [P_eur_3step]}))
# Deltas at each node (for hedging): Delta[n][j] = (V_up - V_down) / (S_up - S_down) from step n
put_grid = [[0.0]*(n+1) for n in range(N3+1)]
for j in range(N3+1):
put_grid[N3][j] = max(K_q25 - S_tree[N3][j], 0)
for n in range(N3-1, -1, -1):
for j in range(n+1):
put_grid[n][j] = disc3 * (p3 * put_grid[n+1][j+1] + (1-p3) * put_grid[n+1][j])
delta_grid = [[0.0]*(n+1) for n in range(N3+1)]
for n in range(N3):
for j in range(n+1):
S_up, S_down = S_tree[n+1][j+1], S_tree[n+1][j]
V_up, V_down = put_grid[n+1][j+1], put_grid[n+1][j]
delta_grid[n][j] = (V_up - V_down) / (S_up - S_down) if S_up != S_down else 0.0
# One path: e.g. Down -> Down -> Up (so we end at node j=1 at step 3). Path indices: (0,0)->(1,0)->(2,0)->(3,1)
path_j = [0, 0, 0, 1] # node index at steps 0,1,2,3
path_S = [S_tree[n][path_j[n]] for n in range(N3+1)]
path_delta = [delta_grid[n][path_j[n]] for n in range(N3)]
# Cash account (seller: receives premium, shorts Delta shares so position = -Delta; rebalance each step)
# at t=0 receive P_eur_3step, hold Delta_0 shares (short if put: Delta_0 < 0). Cash_0 = Premium - Delta_0 * S_0
cash = P_eur_3step - path_delta[0] * path_S[0]
rows = [{"Step": 0, "S": path_S[0], "Delta": path_delta[0], "Cash (after rebal)": round(cash, 2), "Payoff (seller pays)": np.nan}]
for n in range(1, N3):
cash = cash * np.exp(r_q25 * dt3) # interest
cash -= (path_delta[n] - path_delta[n-1]) * path_S[n]
rows.append({"Step": n, "S": round(path_S[n], 4), "Delta": round(path_delta[n], 4), "Cash (after rebal)": round(cash, 2), "Payoff (seller pays)": np.nan})
cash = cash * np.exp(r_q25 * dt3)
payoff = max(K_q25 - path_S[N3], 0)
cash -= payoff
rows.append({"Step": N3, "S": round(path_S[N3], 4), "Delta": "-", "Cash (after rebal)": round(cash, 2), "Payoff (seller pays)": payoff})
q25_table = pd.DataFrame(rows)
q25_table
| Option | Price | |
|---|---|---|
| 0 | European Put (3-step) | 13.82 |
| Step | S | Delta | Cash (after rebal) | Payoff (seller pays) | |
|---|---|---|---|---|---|
| 0 | 0 | 180.0000 | -0.472554 | 98.88 | NaN |
| 1 | 1 | 162.5352 | -0.7447 | 143.45 | NaN |
| 2 | 2 | 146.7650 | -1.0 | 181.39 | NaN |
| 3 | 3 | 162.5352 | - | 162.53 | 19.46477 |
Q26 – American Put, 25-step binomial (seller; delta hedging and cash account)
# Same data as Q25; 25 steps
N25 = 25
dt25 = T_q25 / N25
u25 = np.exp(sigma_q25 * np.sqrt(dt25))
d25 = 1 / u25
p25 = (np.exp(r_q25 * dt25) - d25) / (u25 - d25)
disc25 = np.exp(-r_q25 * dt25)
# Build full tree: S[n][j], American put value V[n][j], Delta[n][j]
S_am = [[S0_q25 * (u25**j) * (d25**(n-j)) for j in range(n+1)] for n in range(N25+1)]
V_am = [[0.0]*(n+1) for n in range(N25+1)]
for j in range(N25+1):
V_am[N25][j] = max(K_q25 - S_am[N25][j], 0)
for n in range(N25-1, -1, -1):
for j in range(n+1):
cont = disc25 * (p25 * V_am[n+1][j+1] + (1-p25) * V_am[n+1][j])
intrinsic = max(K_q25 - S_am[n][j], 0)
V_am[n][j] = max(cont, intrinsic)
# Deltas at each node (a)
Delta_am = [[0.0]*(n+1) for n in range(N25+1)]
for n in range(N25):
for j in range(n+1):
Su, Sd = S_am[n+1][j+1], S_am[n+1][j]
Vu, Vd = V_am[n+1][j+1], V_am[n+1][j]
Delta_am[n][j] = (Vu - Vd) / (Su - Sd) if Su != Sd else 0.0
# (a) Show delta at each node for a subset (e.g. steps 0,1,2,3 and last few)
delta_subset = []
for n in [0, 1, 2, 3, N25-2, N25-1]:
for j in range(min(n+1, 5)):
delta_subset.append({"Step": n, "Node j": j, "S": round(S_am[n][j], 2), "Delta": round(Delta_am[n][j], 4)})
print("(a) Delta at selected nodes (subset):")
display(pd.DataFrame(delta_subset))
American_put_price_25 = round(V_am[0][0], 2)
display(pd.DataFrame({"Option": ["American Put (25-step)"], "Price": [American_put_price_25]}))
(a) Delta at selected nodes (subset):
| Step | Node j | S | Delta | |
|---|---|---|---|---|
| 0 | 0 | 0 | 180.00 | -0.4756 |
| 1 | 1 | 0 | 173.75 | -0.5608 |
| 2 | 1 | 1 | 186.48 | -0.3951 |
| 3 | 2 | 0 | 167.71 | -0.6479 |
| 4 | 2 | 1 | 180.00 | -0.4786 |
| 5 | 2 | 2 | 193.19 | -0.3163 |
| 6 | 3 | 0 | 161.89 | -0.7331 |
| 7 | 3 | 1 | 173.75 | -0.5675 |
| 8 | 3 | 2 | 186.48 | -0.3947 |
| 9 | 3 | 3 | 200.14 | -0.2423 |
| 10 | 23 | 0 | 79.82 | -1.0000 |
| 11 | 23 | 1 | 85.67 | -1.0000 |
| 12 | 23 | 2 | 91.95 | -1.0000 |
| 13 | 23 | 3 | 98.68 | -1.0000 |
| 14 | 23 | 4 | 105.91 | -1.0000 |
| 15 | 24 | 0 | 77.05 | -1.0000 |
| 16 | 24 | 1 | 82.69 | -1.0000 |
| 17 | 24 | 2 | 88.75 | -1.0000 |
| 18 | 24 | 3 | 95.26 | -1.0000 |
| 19 | 24 | 4 | 102.23 | -1.0000 |
| Option | Price | |
|---|---|---|
| 0 | American Put (25-step) | 13.04 |
# (b) One path: e.g. Down for 20 steps then Up for 5 (path ends at node 5 at maturity)
# Path: j=0 at steps 0..19, then j=1,2,3,4,5 at steps 20..25 (so we go 0,0,...,0,1,2,3,4,5)
path_steps_26 = list(range(N25+1))
path_j_26 = [0]*21 + [1,2,3,4,5] # 21 downs then 5 ups -> end at node 5; len = 26
path_S_26 = [S_am[n][path_j_26[n]] for n in range(N25+1)]
path_delta_26 = [Delta_am[n][path_j_26[n]] for n in range(N25)]
cash26 = American_put_price_25 - path_delta_26[0] * path_S_26[0]
rows26 = [{"Step": 0, "S": round(path_S_26[0], 2), "Delta": round(path_delta_26[0], 4), "Cash (after rebal)": round(cash26, 2)}]
for n in range(1, N25):
cash26 = cash26 * np.exp(r_q25 * dt25)
cash26 -= (path_delta_26[n] - path_delta_26[n-1]) * path_S_26[n]
rows26.append({"Step": n, "S": round(path_S_26[n], 2), "Delta": round(path_delta_26[n], 4), "Cash (after rebal)": round(cash26, 2)})
cash26 = cash26 * np.exp(r_q25 * dt25)
payoff26 = max(K_q25 - path_S_26[N25], 0)
cash26 -= payoff26
rows26.append({"Step": N25, "S": round(path_S_26[N25], 2), "Delta": "-", "Cash (after rebal)": round(cash26, 2), "Payoff (seller pays)": payoff26})
print("(b) Cash account along one path (Down x20 then Up x5 → end node 5):")
pd.DataFrame(rows26)
(b) Cash account along one path (Down x20 then Up x5 → end node 5):
| Step | S | Delta | Cash (after rebal) | Payoff (seller pays) | |
|---|---|---|---|---|---|
| 0 | 0 | 180.00 | -0.4756 | 98.64 | NaN |
| 1 | 1 | 173.75 | -0.5608 | 113.49 | NaN |
| 2 | 2 | 167.71 | -0.6479 | 128.14 | NaN |
| 3 | 3 | 161.89 | -0.7331 | 141.98 | NaN |
| 4 | 4 | 156.26 | -0.8124 | 154.43 | NaN |
| 5 | 5 | 150.83 | -0.8822 | 165.03 | NaN |
| 6 | 6 | 145.59 | -0.94 | 173.51 | NaN |
| 7 | 7 | 140.54 | -0.9844 | 179.81 | NaN |
| 8 | 8 | 135.65 | -1.0 | 182.00 | NaN |
| 9 | 9 | 130.94 | -1.0 | 182.08 | NaN |
| 10 | 10 | 126.39 | -1.0 | 182.15 | NaN |
| 11 | 11 | 122.00 | -1.0 | 182.22 | NaN |
| 12 | 12 | 117.77 | -1.0 | 182.30 | NaN |
| 13 | 13 | 113.67 | -1.0 | 182.37 | NaN |
| 14 | 14 | 109.73 | -1.0 | 182.44 | NaN |
| 15 | 15 | 105.91 | -1.0 | 182.51 | NaN |
| 16 | 16 | 102.23 | -1.0 | 182.59 | NaN |
| 17 | 17 | 98.68 | -1.0 | 182.66 | NaN |
| 18 | 18 | 95.26 | -1.0 | 182.73 | NaN |
| 19 | 19 | 91.95 | -1.0 | 182.81 | NaN |
| 20 | 20 | 88.75 | -1.0 | 182.88 | NaN |
| 21 | 21 | 91.95 | -1.0 | 182.95 | NaN |
| 22 | 22 | 95.26 | -1.0 | 183.03 | NaN |
| 23 | 23 | 98.68 | -1.0 | 183.10 | NaN |
| 24 | 24 | 102.23 | -1.0 | 183.17 | NaN |
| 25 | 25 | 105.91 | - | 107.16 | 76.086072 |
Q27 – Asian ATM Put, 25-step binomial
(same data; K = S0 = 180)
# Asian ATM Put: K = S0 = 180. Path-dependent: value at (n,j) depends on running average A.
K_asian = S0_q25 # 180 ATM
M_avg = 35 # number of average buckets
# Average range: from S0*d^N to S0*u^N (wide enough for all paths)
a_min_global = S0_q25 * (d25**N25)
a_max_global = S0_q25 * (u25**N25)
a_grid = np.linspace(a_min_global, a_max_global, M_avg)
def interp_val(a, a_grid, V_vec):
if a <= a_grid[0]: return V_vec[0]
if a >= a_grid[-1]: return V_vec[-1]
return np.interp(a, a_grid, V_vec)
# V_asian[n][j][:] = value at (n,j) for each discretized average
V_asian = [[np.zeros(M_avg) for j in range(n+1)] for n in range(N25+1)]
# At maturity: payoff = max(K - a, 0)
for m in range(M_avg):
payoff_a = max(K_asian - a_grid[m], 0)
for j in range(N25+1):
V_asian[N25][j][m] = payoff_a
# Backward induction: at (n,j) with average a, we go to (n+1,j) with a_d and (n+1,j+1) with a_u
for n in range(N25-1, -1, -1):
for j in range(n+1):
S_here = S_am[n][j]
S_down = S_am[n+1][j]
S_up = S_am[n+1][j+1]
for m in range(M_avg):
a = a_grid[m]
a_d = (a * (n + 1) + S_down) / (n + 2)
a_u = (a * (n + 1) + S_up) / (n + 2)
V_d = interp_val(a_d, a_grid, V_asian[n+1][j])
V_u = interp_val(a_u, a_grid, V_asian[n+1][j+1])
V_asian[n][j][m] = disc25 * (p25 * V_u + (1 - p25) * V_d)
# Initial value: at (0,0) the only average is S0 (so far only one price)
Asian_put_price_25 = round(interp_val(S0_q25, a_grid, V_asian[0][0]), 2)
# Deltas: at (n,j,a) Delta = (V_up - V_down)/(S_up - S_down) for the same a
Delta_asian = [[np.zeros(M_avg) for j in range(n+1)] for n in range(N25)]
for n in range(N25):
for j in range(n+1):
S_up, S_down = S_am[n+1][j+1], S_am[n+1][j]
for m in range(M_avg):
a = a_grid[m]
a_u = (a * (n + 1) + S_up) / (n + 2)
a_d = (a * (n + 1) + S_down) / (n + 2)
V_u = interp_val(a_u, a_grid, V_asian[n+1][j+1])
V_d = interp_val(a_d, a_grid, V_asian[n+1][j])
if S_up != S_down:
Delta_asian[n][j][m] = (V_u - V_d) / (S_up - S_down)
# One path for Asian: same path as Q26. Compute running average A along path.
path_A_27 = [path_S_26[0]] # A[0] = S_0
for n in range(1, N25+1):
path_A_27.append((path_A_27[-1] * n + path_S_26[n]) / (n + 1))
# Delta and value along path (interpolate from grid)
path_delta_asian = []
for n in range(N25):
a_n = path_A_27[n]
j_n = path_j_26[n]
d = interp_val(a_n, a_grid, Delta_asian[n][j_n])
path_delta_asian.append(d)
# Cash account (seller)
cash27 = Asian_put_price_25 - path_delta_asian[0] * path_S_26[0]
rows27 = [{"Step": 0, "S": round(path_S_26[0], 2), "Avg A": round(path_A_27[0], 2), "Delta": round(path_delta_asian[0], 4), "Cash": round(cash27, 2)}]
for n in range(1, N25):
cash27 = cash27 * np.exp(r_q25 * dt25)
cash27 -= (path_delta_asian[n] - path_delta_asian[n-1]) * path_S_26[n]
rows27.append({"Step": n, "S": round(path_S_26[n], 2), "Avg A": round(path_A_27[n], 2), "Delta": round(path_delta_asian[n], 4), "Cash": round(cash27, 2)})
cash27 = cash27 * np.exp(r_q25 * dt25)
payoff_asian = max(K_asian - path_A_27[N25], 0) # Asian payoff uses average, not terminal S
cash27 -= payoff_asian
rows27.append({"Step": N25, "S": round(path_S_26[N25], 2), "Avg A": round(path_A_27[N25], 2), "Delta": "-", "Cash": round(cash27, 2), "Payoff": round(payoff_asian, 4)})
from IPython.display import display # pyright: ignore[reportMissingImports]
display(pd.DataFrame({"Option": ["Asian ATM Put (25-step, K=180)", "American Put (25-step, K=182)"], "Price": [Asian_put_price_25, American_put_price_25]}))
pd.DataFrame(rows27)
| Option | Price | |
|---|---|---|
| 0 | Asian ATM Put (25-step, K=180) | 7.25 |
| 1 | American Put (25-step, K=182) | 13.04 |
| Step | S | Avg A | Delta | Cash | Payoff | |
|---|---|---|---|---|---|---|
| 0 | 0 | 180.00 | 180.00 | -0.4438 | 87.13 | NaN |
| 1 | 1 | 173.75 | 176.87 | -0.5547 | 106.44 | NaN |
| 2 | 2 | 167.71 | 173.82 | -0.6484 | 122.19 | NaN |
| 3 | 3 | 161.89 | 170.84 | -0.7119 | 132.52 | NaN |
| 4 | 4 | 156.26 | 167.92 | -0.7403 | 137.01 | NaN |
| 5 | 5 | 150.83 | 165.07 | -0.7393 | 136.92 | NaN |
| 6 | 6 | 145.59 | 162.29 | -0.7187 | 133.97 | NaN |
| 7 | 7 | 140.54 | 159.57 | -0.6873 | 129.61 | NaN |
| 8 | 8 | 135.65 | 156.91 | -0.6511 | 124.76 | NaN |
| 9 | 9 | 130.94 | 154.32 | -0.6134 | 119.87 | NaN |
| 10 | 10 | 126.39 | 151.78 | -0.5753 | 115.10 | NaN |
| 11 | 11 | 122.00 | 149.30 | -0.5371 | 110.48 | NaN |
| 12 | 12 | 117.77 | 146.87 | -0.4988 | 106.02 | NaN |
| 13 | 13 | 113.67 | 144.50 | -0.4605 | 101.71 | NaN |
| 14 | 14 | 109.73 | 142.18 | -0.4222 | 97.55 | NaN |
| 15 | 15 | 105.91 | 139.92 | -0.3839 | 93.53 | NaN |
| 16 | 16 | 102.23 | 137.70 | -0.3456 | 89.65 | NaN |
| 17 | 17 | 98.68 | 135.53 | -0.3073 | 85.90 | NaN |
| 18 | 18 | 95.26 | 133.41 | -0.2689 | 82.29 | NaN |
| 19 | 19 | 91.95 | 131.34 | -0.2305 | 78.79 | NaN |
| 20 | 20 | 88.75 | 129.31 | -0.1922 | 75.41 | NaN |
| 21 | 21 | 91.95 | 127.61 | -0.1538 | 71.91 | NaN |
| 22 | 22 | 95.26 | 126.21 | -0.1153 | 68.28 | NaN |
| 23 | 23 | 98.68 | 125.06 | -0.0769 | 64.52 | NaN |
| 24 | 24 | 102.23 | 124.15 | -0.0385 | 60.61 | NaN |
| 25 | 25 | 105.91 | 123.44 | - | 4.08 | 56.5557 |
The Asian put price is lower than the vanilla American put because the payoff depends on the average stock price rather than the terminal price. Averaging reduces volatility of the payoff and therefore reduces option value.