Workshop 3, Hedging Vehicles - Module 2

Author

Alberto Dorantes

Published

November 20, 2025

Abstract
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:

  1. Revisit the one-step (2 time points) binomial valuation of a European call (no dividends).
  2. Extend the idea to a three-step binomial lattice within one year.
  3. Explain where the up (u) and down (d) factors come from, in terms of volatility.
  4. Show how to adapt the method for:
    • European put options
    • American call and put options
    • Options on dividend-paying stocks
  5. 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
  • StrikeK = 110

Then:

S_u = 1.2 \times 100 = 120, \quad S_d = 0.9 \times 100 = 90.

Call payoffs at maturity:

C_u = \max(120 - 110, 0) = 10, \quad C_d = \max(90 - 110, 0) = 0.

Risk-neutral probability:

q = \dfrac{R - d}{u - d} = \dfrac{1.05127 - 0.9}{1.2 - 0.9} \approx 0.504.

Call price today:

C_0 = \dfrac{1}{R} \big[ q \cdot 10 + (1 - q)\cdot 0 \big] \approx \dfrac{10 \times 0.504}{1.05127} \approx 4.8.

So the fair price is about $4.8.

Bellow is the Python code to do this excercise:

Code
import numpy as np
import matplotlib.pyplot as plt

# Parameters from Lecture Note Page 8
S0 = 100
K = 110
u = 1.2
d = 0.9
rf = 0.05
R = 1 + rf

# Calculate Payoffs at t=1
Su = S0 * u
Sd = S0 * d

Cu = max(Su - K, 0) # Should be 10 
Cd = max(Sd - K, 0) # Should be 0 

# Risk Neutral Probability q 
q = (R - d) / (u - d)

# Calculate Price today 
C0 = (1/R) * (q * Cu + (1-q) * Cd)

print(f"Risk-Neutral Probability (q): {q:.4f}")
print(f"Stock Up: {Su}, Option Up: {Cu}")
print(f"Stock Down: {Sd}, Option Down: {Cd}")
print(f"Call Option Price (C0): ${C0:.2f}") 
# Expected result matches PDF page 9: $4.76
Risk-Neutral Probability (q): 0.5000
Stock Up: 120.0, Option Up: 10.0
Stock Down: 90.0, Option Down: 0
Call Option Price (C0): $4.76

We can also visualize the payoff diagram of the call at maturity:

Code
import numpy as np
import matplotlib.pyplot as plt

K = 110
ST_grid = np.linspace(50, 160, 200)   # possible stock prices at maturity
call_payoff = np.maximum(ST_grid - K, 0)

plt.figure(figsize=(6, 4))
plt.plot(ST_grid, call_payoff)
plt.axvline(K, linestyle="--")
plt.xlabel("Stock price at maturity$S_T$")
plt.ylabel("Call payoff$\max(S_T - K, 0)$")
plt.title("European Call Payoff at Maturity")
plt.grid(True)
plt.show()

3 Extending to Multi-Period (3 Periods in 1 Year)

Realistically, stocks move more frequently than once a year. We can divide the 1-year horizon into N steps of length \Delta t.

3.1 Parameterizing u and d using Volatility

How do we Choose u and d from volatility?

In continuous time, the standard model for a stock price is:

dS_t = \mu S_t dt + \sigma S_t dW_t

This implies that the log-return over a short period \Delta t is approximately:

\ln\left(\frac{S_{t+\Delta t}}{S_t}\right) \sim \mathcal{N}\big((\mu - \tfrac{1}{2}\sigma^2)\Delta t,\ \sigma^2 \Delta t\big)

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:

  1. Forward Pass: Build the tree of Stock Prices from t=0 to t=3.

  2. Terminal Value: Calculate option payoff at the final nodes (t=3) using \max(S_T - K, 0).

  3. 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.

C_{t} = e^{-r \Delta t} [q C_{t+1}^{up} + (1-q) C_{t+1}^{down}]

3.3 Python Implementation: 3-Step Lattice

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 in range(N + 1):
        for j in range(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 in range(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 Induction
    for i in range(N - 1, -1, -1):
        for j in range(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 = 0
                if 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] = continuation
    if return_tree:
        return option_tree[0, 0], stock_tree, option_tree, (u, d, R, q, dt)
    else:
        return option_tree[0, 0]
         

# Define Parameters
sigma = 0.30  # 30% volatility
N = 3         # 3 periods
T = 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 = params

print(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 in range(N):          # time steps: 0,1,2
    for j in range(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 nodes
for i in range(N + 1):     # time: 0..3
    for j in range(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)

# Formatting
plt.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 values
for i in range(N):          # time steps: 0,1,2
    for j in range(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 them
for i in range(N + 1):      # time: 0..3
    for j in range(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:

  1. Hold the option (Continuation Value: discounted expected future value).

  2. Exercise immediately (Intrinsic Value: S-K for Call, K-S for Put).

Value_{node} = \max(\text{Intrinsic}, \text{Continuation})

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 steps N \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:

d_1 = \frac{\ln(S/K) + (r + \sigma^2/2)T}{\sigma\sqrt{T}}

d_2 = d_1 - \sigma\sqrt{T}

C = S N(d_1) - K e^{-rT} N(d_2)

P = K e\^{-rT} N(-d_2) - S N(-d_1)

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 norm

def 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 steps

print(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

  1. Binomial Model: Great for intuition and complex features (like American exercise).

  2. Black-Scholes: The standard for European options, acting as the limit case of the Binomial model.

  3. Arbitrage Free: Both rely on the principle that we can replicate the option with a portfolio of Stock and Bonds.

7 EXERCISES

  1. 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

  1. 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

  1. 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.