In this workshop we learn about the Valuation of Stock Options: From Binomial Lattices to Black-Scholes
1 Introduction to Option Valuation
In my Lecture note Basics of Options I review the following:
What is a call and put options and their payoffs
Explain how to build a one-step binomial model where the stock can only go up or down in one period
Construct a replicating portfolio with stock + risk-free bond that mimics the option payoff in order to come up with a fair value of a call option
Here we will:
Revisit the one-step (2 time points) binomial valuation of a European call (no dividends).
Extend the idea to a three-step binomial lattice within one year.
Explain where the up (u) and down (d) factors come from, in terms of volatility.
Show how to adapt the method for:
European put options
American call and put options
Options on dividend-paying stocks
Show how the Black–Scholes model is related to the binomial model and compare prices on the same examples.
The main objective of the binomial and Black-Scholes model is to determine the “fair price” of an option.
As noted in my lecture note, there is “no free lunch” in financial markets; options must have a specific cost to prevent arbitrage.
We define the basic instruments as follows:
Call Option: The right (but not obligation) to buy a security at strike price K.
Put Option: The right (but not obligation) to sell a security at strike price K.
European Option: Can only be executed at the expiration date.
American Option: Can be executed at any time up to expiration.
2 The One-Period Binomial Model (2 Time Periods)
We begin with the simplest world described in the lecture notes: a single time step (from t=0 to t=1).
2.1 The Setup with Two Possible Future Prices
Consider a very simple world with only two time points:
Today: t = 0
One year from now: t = T = 1
Assume that during this year the stock can only:
Go up by a factor u > 1:
S_u = u S_0
Go down by a factord < 1:
S_d = d S_0
We have a European call with strike K. Its payoff at maturity T is:
If stock goes up: C_u = \max(S_u - K, 0)
If stock goes down: C_d = \max(S_d - K, 0)
Our goal: find the fair price C_0 of the call today.
2.2 The Replicating Portfolio with no-Arbitrage
We build a portfolio that mimics the call’s payoff exactly, no matter if the stock goes up or down:
Buy \Delta shares of the stock
Borrow (or lend) an amount B in the risk-free asset (if B<0 we borrow and then invest; if B>0 we invest by lending money)
The value of this mimic portfolio is:
P_0 = B_0 + N \cdot S
To prevent arbitrage, the value of this portfolio must equal the value of the option in both the “up” and “down” states:
If stock goes up: rB_0 + N(uS) = C_u
If stock goes down: rB_0 + N(dS) = C_d
Where r is the gross risk-free return (1 + R_f).
Solving this system yields the N (# of shares to be bought), which is called \Delta, the Hedge Ratio (Delta):
N = \Delta= \frac{C_u - C_d}{S(u-d)}
And the Bond position:
B_0 = \frac{uC_d - dC_u}{r(u-d)}
Because the portfolio’s future payoffs are identical to the call’s payoffs in both states, no-arbitrage implies that the Call value must be equal to this portfolio value:
C = P_0 = \Delta S_0 + B
2.3 Risk-Neutral Valuation Formula
Substituting N and B_0 back into the portfolio equation yields the pricing formula:
C = \dfrac{1}{R} \big[ q C_u + (1 - q) C_d \big],
Where q is the risk-neutral probability:
q = \dfrac{R - d}{u - d}.
Note:q is not a real probability, but a synthetic weight that allows us to value the option as if investors were risk-neutral.
2.4 Python Example: 1-Step Model
Let’s replicate the example from the lecture note: * S_0 = 100 * K = 110 * u = 1.2 (20% increase) * d = 0.9 (10% decrease) * R_f = 0.05 (so r = 1.05)
S_0 = 100
Up factoru = 1.2(20% up)
Down factord = 0.9(10% down)
Risk-free rater = 5\%per year ⇒R = e^{0.05} \approx 1.05127
The annual standard deviation of log returns is \sigma. Then, the standard deviation of the log-return over \Delta t is \sigma \sqrt{\Delta t}.
In the Cox–Ross–Rubinstein (CRR) binomial model, we choose up and down moves so that:
An up move corresponds to + one standard deviation of log-return
A down move corresponds to – one standard deviation
So we set:
u = e^{\sigma \sqrt{\Delta t}}, \qquad d = e^{-\sigma \sqrt{\Delta t}} = \frac{1}{u}.
Then:
A single up move gives log-return \ln u = \sigma \sqrt{\Delta t}.
A single down move gives log-return \ln d = -\sigma \sqrt{\Delta t}.
After many steps, the sum of these +\sigma \sqrt{\Delta t} and -\sigma \sqrt{\Delta t} increments behaves like a normal distribution (by the Central Limit Theorem), matching the behavior assumed in the continuous-time model.
This is why the CRR binomial lattice is a discrete-time approximation to the continuous Black–Scholes model.
This is called the Cox-Ross-Rubinstein (CRR) parameterization.
Then, to ensure the binomial tree’s variance matches the continuous stock variance \sigma^2:
u = e^{\sigma \sqrt{\Delta t}}
d = e^{-\sigma \sqrt{\Delta t}} = \frac{1}{u}
In this case, if we have 3 periods or steps within 1 year, then
\Delta t=\frac{1}{3}
Consequently, we adjust the discount factor to continuous compounding: R=e^{r \Delta t}.
3.2 Backward Induction
To value an option with 3 steps:
Forward Pass: Build the tree of Stock Prices from t=0 to t=3.
Terminal Value: Calculate option payoff at the final nodes (t=3) using \max(S_T - K, 0).
Backward Pass: Step back to t=2, t=1, t=0.
At each node, calculate the value as the discounted expected value of the next two nodes.
Let’s assume the same stock (S=100, K=110, r=5\%) but now we incorporate volatility \sigma = 30\% and split the year into 3 periods (T=1, N=3).
Code
def binomial_option_pricing(S, K, T, r, sigma, N, option_type='call', style='european', return_tree=False): dt = T / N u = np.exp(sigma * np.sqrt(dt)) d =1/ u discount = np.exp(-r * dt) q = (np.exp(r * dt) - d) / (u - d)# 1. Initialize Stock Price Tree stock_tree = np.zeros((N +1, N +1))for i inrange(N +1):for j inrange(i +1): stock_tree[j, i] = S * (u ** (i - j)) * (d ** j)# 2. Initialize Option Value Tree at Maturity option_tree = np.zeros((N +1, N +1))for j inrange(N +1):if option_type =='call': option_tree[j, N] =max(0, stock_tree[j, N] - K)elif option_type =='put': option_tree[j, N] =max(0, K - stock_tree[j, N])# 3. Backward Inductionfor i inrange(N -1, -1, -1):for j inrange(i +1):# Continuation value (European logic) continuation = discount * (q * option_tree[j, i +1] + (1- q) * option_tree[j +1, i +1])if style =='american':# Check for early exercise intrinsic =0if option_type =='call': intrinsic =max(0, stock_tree[j, i] - K)else: intrinsic =max(0, K - stock_tree[j, i]) option_tree[j, i] =max(intrinsic, continuation)else: option_tree[j, i] = continuationif return_tree:return option_tree[0, 0], stock_tree, option_tree, (u, d, R, q, dt)else:return option_tree[0, 0]# Define Parameterssigma =0.30# 30% volatilityN =3# 3 periodsT =1# 1 year#price_3step = binomial_option_pricing(S0, K, T, rf, sigma, N, 'call', 'european')price_3step, stock_tree, option_tree, params = binomial_option_pricing( S0, K, T, rf, sigma, N, 'call', 'european', return_tree=True)u, d, R, q, dt = paramsprint(f"3-Step Binomial Call Price: ${price_3step:.2f}")print(f"u = {u:.4f}, d = {d:.4f}, R = {R:.4f}, risk-neutral prob = {q:.4f}")
3-Step Binomial Call Price: $10.34
u = 1.1891, d = 0.8410, R = 1.0500, risk-neutral prob = 0.5051
We can visualize the 3-step binomial Stock Price Tree for this example:
Code
# --- Plot the 3-step binomial stock price tree based on stock_tree ---plt.figure(figsize=(8, 6))# Draw the branches (lines between nodes)for i inrange(N): # time steps: 0,1,2for j inrange(i +1): # nodes at each step x0, y0 = i, stock_tree[j, i]# Up move: (i, j) -> (i+1, j) x1_up, y1_up = i +1, stock_tree[j, i +1] plt.plot([x0, x1_up], [y0, y1_up], 'k-', linewidth=1)# Down move: (i, j) -> (i+1, j+1) x1_dn, y1_dn = i +1, stock_tree[j +1, i +1] plt.plot([x0, x1_dn], [y0, y1_dn], 'k-', linewidth=1)# Plot the nodesfor i inrange(N +1): # time: 0..3for j inrange(i +1): # nodes: 0..i x, y = i, stock_tree[j, i] plt.scatter(x, y, color='blue') plt.text(x +0.03, y, f"{y:.2f}", fontsize=9)# Formattingplt.title("3-Step Binomial Stock Price Tree")plt.xlabel("Time Step")plt.ylabel("Stock Price")plt.grid(True, linestyle='--', alpha=0.5)plt.tight_layout()plt.show()
We can visualize the 3-step binomial Stock Call Value for this example:
Code
# --- Plot the 3-step binomial Call VALUE tree based on option_tree ---plt.figure(figsize=(8, 6))# Draw the branches (lines between nodes) for option valuesfor i inrange(N): # time steps: 0,1,2for j inrange(i +1): # nodes at each step x0, V0 = i, option_tree[j, i]# Up move in option tree: (i, j) -> (i+1, j) x1_up, V_up = i +1, option_tree[j, i +1] plt.plot([x0, x1_up], [V0, V_up], 'k-', linewidth=1)# Down move in option tree: (i, j) -> (i+1, j+1) x1_dn, V_dn = i +1, option_tree[j +1, i +1] plt.plot([x0, x1_dn], [V0, V_dn], 'k-', linewidth=1)# Plot the nodes (call values at each node) and label themfor i inrange(N +1): # time: 0..3for j inrange(i +1): # nodes: 0..i x, V = i, option_tree[j, i] plt.scatter(x, V, color='darkgreen') plt.text(x +0.03, V, f"{V:.2f}", fontsize=9)plt.title("3-Step Binomial Call Value Tree")plt.xlabel("Time Step")plt.ylabel("Call Option Value")plt.grid(True, linestyle='--', alpha=0.5)plt.tight_layout()plt.show()
4 Variations on the Model
The power of the binomial lattice is its flexibility.
We can easily adapt the logic for different instrument types.
4.1 Put European Option
For a Put option, the only change occurs at the expiration nodes (and intrinsic value checks). The payoff becomes:
P = \max(0, K - S)
The recursive formula remains the same, using risk-neutral probability q.
4.2 American Options (Call and Put)
An American option allows early exercise.
In the binomial tree, at every node, we must check if it is better to:
Hold the option (Continuation Value: discounted expected future value).
Exercise immediately (Intrinsic Value: S-K for Call, K-S for Put).
Note: For a non-dividend paying stock, it is never optimal to exercise an American Call early. However, early exercise is very relevant for American Puts.
4.3 Dividend Payments
If the stock pays a continuous dividend yield \delta, the stock price growth is dampened. We adjust the risk-neutral probability formula:
q = \frac{e^{(r - \delta)\Delta t} - d}{u - d}
This lowers the probability of “up” moves, decreasing Call values and increasing Put values.
5 The Black-Scholes-Merton (BSM) Model
5.1 Relation to Binomial Lattice
The Binomial Model was developed after Black-Scholes (by Cox, Ross, Rubinstein in 1979 4) as a simplified approach.
Mathematically, as the number of stepsN \to \infty (and \Delta t \to 0), the Binomial Model price converges to the Black-Scholes price.
The “jagged” binomial tree becomes a smooth log-normal distribution.
5.2 The Formulas
For a European Call (C) and Put (P) on a non-dividend stock:
Where N(x) is the cumulative standard normal distribution.
5.3 Comparison Examples
Let’s compare the pricing of the 12-Step Binomial model against the exact Black-Scholes analytical solution using our parameters (S=100, K=110, r=5\%, \sigma=30\%, T=1).
Code
from scipy.stats import normdef black_scholes(S, K, T, r, sigma, option_type='call'): d1 = (np.log(S / K) + (r +0.5* sigma **2) * T) / (sigma * np.sqrt(T)) d2 = d1 - sigma * np.sqrt(T)if option_type =='call': price = S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)else: price = K * np.exp(-r * T) * norm.cdf(-d2) - S * norm.cdf(-d1)return price# --- Comparison 1: Call Option ---bs_call = black_scholes(S0, K, T, rf, sigma, 'call')bin_call_50 = binomial_option_pricing(S0, K, T, rf, sigma, 50, 'call', 'european') # 50 stepsprint(f"--- Example 1: European Call (K=110) ---")print(f"BSM Price: ${bs_call:.4f}")print(f"Binomial (N=3): ${price_3step:.4f}")print(f"Binomial (N=50): ${bin_call_50:.4f}")print(f"Note that when N=50 vs N=3, the Binomial value is much closer to the BSM value.\n")# --- Comparison 2: Put Option ---bs_put = black_scholes(S0, K, T, rf, sigma, 'put')bin_put_3 = binomial_option_pricing(S0, K, T, rf, sigma, 3, 'put', 'european')bin_put_50 = binomial_option_pricing(S0, K, T, rf, sigma, 50, 'put', 'european')print(f"--- Example 2: European Put (K=110) ---")print(f"BSM Price: ${bs_put:.4f}")print(f"Binomial (N=3): ${bin_put_3:.4f}")print(f"Binomial (N=50): ${bin_put_50:.4f}")print(f"Note that when N=50 vs N=3, the Binomial value is much closer to the BSM value.\n")
--- Example 1: European Call (K=110) ---
BSM Price: $10.0201
Binomial (N=3): $10.3363
Binomial (N=50): $10.0052
Note that when N=50 vs N=3, the Binomial value is much closer to the BSM value.
--- Example 2: European Put (K=110) ---
BSM Price: $14.6553
Binomial (N=3): $14.9716
Binomial (N=50): $14.6404
Note that when N=50 vs N=3, the Binomial value is much closer to the BSM value.
6 Summary
Binomial Model: Great for intuition and complex features (like American exercise).
Black-Scholes: The standard for European options, acting as the limit case of the Binomial model.
Arbitrage Free: Both rely on the principle that we can replicate the option with a portfolio of Stock and Bonds.
7 EXERCISES
Create an Excel Sheet as a template to valuate a European Call Option with no dividend payments using the Binomial Lattice method with 3 Steps/Periods within a year. Start with the following initial parameters/values:
Spot Price today S0 = $100
Strike price K = 105
Annual Risk-free rate (in continuously compounded)= 5%
Annual volatility = 20%
Time-to-Maturity = 1 year
Indicate which is the final value of the Option
Based on your previous Template, do another Sheet with the following extensions:
Extend the Binomial Tree to 12 steps (12 months)
Allow for Dividend Yield
(Optional, extra points) Include the option to value a Call or a Put
(Optional, extra points) Include the option to value an American or an European option
In your previous Sheet, add the formulas to calculate the corresponding Option using the Black & Sholes formula. Report how close was the BSC valuation vs your Binomial valuation.