---
title: "Chapter 4. Multi-Period Binomial Models"
---
::: {.chapter-objectives}
## Learning Goals
By the end of this chapter, students should be able to:
- build a multi-period binomial stock-price tree from $S_0$, $u$, $d$, and $N$;
- distinguish the physical probability $p$ from the risk-neutral probability $q$;
- state and use the no-arbitrage condition $d<R<u$ in each period;
- prove that the discounted stock price is a martingale under the risk-neutral measure;
- price European claims by conditional expectation and by backward induction;
- derive the binomial distribution formula for a European payoff depending only on $S_N$;
- compute replicating portfolios and dynamic hedge ratios along a tree;
- price American options using the Snell-envelope backward recursion;
- interpret the binomial model as a discrete approximation to geometric Brownian motion;
- implement binomial pricing algorithms in Python;
- use AI tools responsibly to check derivations, assumptions, and code.
:::
::: {.hero-box}
A one-period model explains the idea of no-arbitrage pricing. A multi-period binomial model explains how pricing becomes **dynamic**.
At every node of the tree, the same one-period theorem is applied again. This is the mathematical reason that option pricing is naturally recursive: future values are computed first, and current values are obtained by discounting risk-neutral conditional expectations.
:::
## 4.1 From One Period to Many Periods
In Chapter 3, the stock price moved once. It either went up or down, and a derivative payoff was a vector with two components. In this chapter, the stock moves repeatedly. The model is still simple enough to be computed exactly, but rich enough to introduce dynamic hedging, conditional expectation, American options, and convergence to continuous-time models.
Fix an integer $N\ge 1$. The trading dates are
$$
0,1,2,\ldots,N.
$$
One period may represent one day, one week, one month, or a small time step $\Delta t$ in a continuous-time approximation. At each step, the stock price either moves up by a factor $u$ or down by a factor $d$:
$$
u>d>0.
$$
The bank account grows by the gross risk-free return
$$
R=1+r.
$$
Thus one dollar in the bank account at time $n$ becomes $R$ dollars at time $n+1$.
The central no-arbitrage condition is
$$
d<R<u.
$$
This condition says that the stock can outperform the bank account in an up move and underperform the bank account in a down move. If this fails, then one asset dominates the other and arbitrage is possible.
## 4.2 The Binomial Stock-Price Tree
Let $S_n$ denote the stock price at time $n$. If the stock has moved up $j$ times and down $n-j$ times by time $n$, then
$$
S_n(j)=S_0u^j d^{n-j},
\qquad j=0,1,\ldots,n.
$$
There are $2^n$ possible paths by time $n$, but only $n+1$ possible stock prices because many paths lead to the same number of up moves. This is called a **recombining tree**.
::: {.definition}
### Definition 4.1: Multi-period binomial stock model
A multi-period binomial stock model consists of parameters
$$
S_0>0,\qquad u>d>0,\qquad R>0,\qquad N\in\mathbb N,
$$
and the stock-price process
$$
S_{n+1}=
\begin{cases}
u S_n, & \text{up move},\\
d S_n, & \text{down move},
\end{cases}
\qquad n=0,1,\ldots,N-1.
$$
The bank account is
$$
B_n=R^n.
$$
:::
::: {.example}
### Example 4.1: A three-step tree
Let
$$
S_0=100,\qquad u=1.2,\qquad d=0.8,\qquad N=3.
$$
At time $1$, the possible prices are
$$
80,\quad 120.
$$
At time $2$, the possible prices are
$$
64,\quad 96,\quad 144.
$$
At time $3$, the possible prices are
$$
51.2,\quad 76.8,\quad 115.2,\quad 172.8.
$$
The middle values recombine: an up move followed by a down move gives the same price as a down move followed by an up move:
$$
S_0ud=S_0du.
$$
:::
## 4.3 Paths, Nodes, and Filtrations
A path through the tree is a sequence of up and down moves. Let
$$
\Omega=\{U,D\}^N
$$
be the set of all possible paths. A path is written as
$$
\omega=(\omega_1,\omega_2,\ldots,\omega_N),
\qquad \omega_k\in\{U,D\}.
$$
The information available by time $n$ is the first $n$ moves. The corresponding filtration is
$$
\mathcal F_n=\sigma(\omega_1,\ldots,\omega_n),
\qquad n=0,1,\ldots,N.
$$
A random variable $X$ is $\mathcal F_n$-measurable if its value is known after observing the first $n$ moves. For example, $S_n$ is $\mathcal F_n$-measurable, while $S_N$ is usually not $\mathcal F_n$-measurable for $n<N$.
::: {.remark}
In this chapter, a node can be indexed by $(n,j)$, where $n$ is the time and $j$ is the number of up moves so far. At node $(n,j)$,
$$
S_n(j)=S_0u^jd^{n-j}.
$$
:::
## 4.4 Physical Probability Versus Pricing Probability
A statistical model may assign a physical up probability
$$
\mathbb P(U)=p.
$$
This probability describes beliefs about how the stock may actually move. It affects the real-world distribution of future prices and returns. However, the no-arbitrage price of a derivative is not obtained by simply discounting the physical expectation unless $p$ happens to equal the risk-neutral probability.
The one-period risk-neutral up probability is
$$
q=\frac{R-d}{u-d}.
$$
Under the no-arbitrage condition $d<R<u$, this satisfies
$$
0<q<1.
$$
The risk-neutral probability is not a forecast. It is the probability that makes the discounted stock price a martingale.
::: {.definition}
### Definition 4.2: Risk-neutral measure on the binomial tree
The risk-neutral measure $\mathbb Q$ is the probability measure under which each up move has probability
$$
q=\frac{R-d}{u-d},
$$
and each down move has probability
$$
1-q=\frac{u-R}{u-d}.
$$
In the standard binomial model, the moves are independent under $\mathbb Q$.
:::
## 4.5 Discounted Stock Price as a Martingale
Define the discounted stock price by
$$
\widetilde S_n=\frac{S_n}{B_n}=\frac{S_n}{R^n}.
$$
The risk-neutral probability $q$ is chosen precisely so that
$$
\mathbb E^{\mathbb Q}\left[\widetilde S_{n+1}\mid \mathcal F_n\right]=\widetilde S_n.
$$
::: {.theorem}
### Theorem 4.1: Discounted stock martingale
Assume $d<R<u$ and let
$$
q=\frac{R-d}{u-d}.
$$
Then the discounted stock price process $\widetilde S_n=S_n/R^n$ is a martingale under $\mathbb Q$.
:::
::: {.proof}
At time $n$, the stock price $S_n$ is known. Conditional on $\mathcal F_n$,
$$
S_{n+1}=\begin{cases}
u S_n, & \text{with probability }q,\\
dS_n, & \text{with probability }1-q.
\end{cases}
$$
Therefore
$$
\mathbb E^{\mathbb Q}[S_{n+1}\mid \mathcal F_n]
=quS_n+(1-q)dS_n.
$$
Since $q=(R-d)/(u-d)$, we have
$$
qu+(1-q)d=R.
$$
Hence
$$
\mathbb E^{\mathbb Q}[S_{n+1}\mid \mathcal F_n]=RS_n.
$$
Dividing by $R^{n+1}$ gives
$$
\mathbb E^{\mathbb Q}\left[\frac{S_{n+1}}{R^{n+1}}\mid \mathcal F_n\right]
=
\frac{S_n}{R^n}.
$$
Thus $\widetilde S_n$ is a martingale under $\mathbb Q$.
:::
This theorem is the discrete-time prototype of the continuous-time martingale pricing principle developed later in the book.
## 4.6 European Claims and Terminal Payoffs
A European derivative is a claim whose payoff is determined at the final time $N$. The payoff may depend on the whole path or only on the terminal stock price.
A European call option with strike $K$ has payoff
$$
H=(S_N-K)^+.
$$
A European put option has payoff
$$
H=(K-S_N)^+.
$$
More generally, if the payoff depends only on $S_N$, we write
$$
H=h(S_N)
$$
for some payoff function $h$.
::: {.definition}
### Definition 4.3: European payoff
A European payoff is an $\mathcal F_N$-measurable random variable $H$. If $H=h(S_N)$ for some function $h$, the payoff is called **path-independent**. If $H$ depends on the whole path $(S_0,S_1,\ldots,S_N)$, it is called **path-dependent**.
:::
Examples of path-dependent payoffs include Asian options, barrier options, and lookback options. These appear later in the book. In this chapter we mostly focus on path-independent payoffs.
## 4.7 Risk-Neutral Pricing Formula
The no-arbitrage price at time $n$ of a European payoff $H$ paid at time $N$ is
$$
V_n=R^{-(N-n)}\mathbb E^{\mathbb Q}[H\mid \mathcal F_n].
$$
Equivalently, the discounted value process is a martingale:
$$
\widetilde V_n=\frac{V_n}{R^n},
\qquad
\widetilde V_n=
\mathbb E^{\mathbb Q}\left[\widetilde V_N\mid \mathcal F_n\right].
$$
Since $\widetilde V_N=H/R^N$, this gives
$$
\frac{V_n}{R^n}
=
\mathbb E^{\mathbb Q}\left[\frac{H}{R^N}\mid \mathcal F_n\right].
$$
Multiplying by $R^n$ yields the pricing formula above.
::: {.theorem}
### Theorem 4.2: Multi-period binomial pricing formula
In the $N$-period binomial model with $d<R<u$, the unique no-arbitrage price at time $n$ of a European payoff $H$ paid at time $N$ is
$$
V_n=R^{-(N-n)}\mathbb E^{\mathbb Q}[H\mid \mathcal F_n],
$$
where $\mathbb Q$ is the risk-neutral measure with
$$
q=\frac{R-d}{u-d}.
$$
:::
The theorem says that the price is a discounted conditional expectation under $\mathbb Q$, not under the physical measure $\mathbb P$.
## 4.8 Closed-Form Binomial Formula for Path-Independent Payoffs
Suppose $H=h(S_N)$ depends only on the terminal stock price. If there are $j$ up moves and $N-j$ down moves, then
$$
S_N(j)=S_0u^jd^{N-j}.
$$
Under $\mathbb Q$, the number of up moves has binomial distribution
$$
J\sim \operatorname{Binomial}(N,q).
$$
Therefore
$$
V_0=R^{-N}\sum_{j=0}^N \binom Nj q^j(1-q)^{N-j}h(S_0u^jd^{N-j}).
$$
For a European call,
$$
C_0
=
R^{-N}\sum_{j=0}^N \binom Nj q^j(1-q)^{N-j}
\left(S_0u^jd^{N-j}-K\right)^+.
$$
For a European put,
$$
P_0
=
R^{-N}\sum_{j=0}^N \binom Nj q^j(1-q)^{N-j}
\left(K-S_0u^jd^{N-j}\right)^+.
$$
::: {.example}
### Example 4.2: Two-step European call
Let
$$
S_0=100,\qquad u=1.2,\qquad d=0.8,\qquad R=1.05,
\qquad N=2,
\qquad K=100.
$$
The risk-neutral probability is
$$
q=\frac{1.05-0.8}{1.2-0.8}=0.625.
$$
The terminal stock prices are
$$
S_2(0)=64,\qquad S_2(1)=96,\qquad S_2(2)=144.
$$
The call payoffs are
$$
0,\qquad 0,\qquad 44.
$$
Thus
$$
C_0
=
1.05^{-2}\left[\binom 22(0.625)^2(0.375)^0 44\right]
=15.5896.
$$
:::
## 4.9 Backward Induction
The closed-form formula is convenient for path-independent European claims, but dynamic programming is more general. The tree recursion works for European, American, and many path-dependent claims.
Let $V_n(j)$ be the option value at node $(n,j)$. For a European payoff $H=h(S_N)$, start at maturity:
$$
V_N(j)=h(S_N(j)),
\qquad j=0,1,\ldots,N.
$$
Then move backward through the tree. At node $(n,j)$, the next up node is $(n+1,j+1)$ and the next down node is $(n+1,j)$. The value satisfies
$$
V_n(j)=\frac{1}{R}\left[qV_{n+1}(j+1)+(1-q)V_{n+1}(j)\right].
$$
This is simply the one-period pricing formula applied locally at every node.
::: {.algorithm}
### Algorithm 4.1: Backward induction for a European option
1. Compute terminal payoffs $V_N(j)=h(S_0u^jd^{N-j})$ for $j=0,1,\ldots,N$.
2. For $n=N-1,N-2,\ldots,0$, compute
$$
V_n(j)=\frac{1}{R}\left[qV_{n+1}(j+1)+(1-q)V_{n+1}(j)\right],
\qquad j=0,1,\ldots,n.
$$
3. The option price is $V_0(0)$.
:::
The backward induction formula is often more stable and more flexible than explicitly summing binomial probabilities.
## 4.10 Dynamic Replication and Delta Hedging
Pricing gives the value of a derivative. Hedging gives the portfolio that replicates it.
At node $(n,j)$, suppose the option values one step later are
$$
V_{n+1}(j+1) \quad \text{after an up move},
$$
and
$$
V_{n+1}(j) \quad \text{after a down move}.
$$
The stock prices one step later are
$$
u S_n(j),\qquad dS_n(j).
$$
We seek a portfolio with $\Delta_n(j)$ shares of stock and $B_n(j)$ dollars in the bank account at node $(n,j)$ such that
$$
\Delta_n(j)uS_n(j)+B_n(j)R=V_{n+1}(j+1),
$$
$$
\Delta_n(j)dS_n(j)+B_n(j)R=V_{n+1}(j).
$$
Subtracting the equations gives the hedge ratio
$$
\Delta_n(j)=
\frac{V_{n+1}(j+1)-V_{n+1}(j)}{S_n(j)(u-d)}.
$$
The bank-account position is
$$
B_n(j)=\frac{uV_{n+1}(j)-dV_{n+1}(j+1)}{R(u-d)}.
$$
The value at node $(n,j)$ is then
$$
V_n(j)=\Delta_n(j)S_n(j)+B_n(j).
$$
::: {.remark}
The hedge ratio $\Delta_n(j)$ is not usually constant across the tree. Dynamic hedging means that the portfolio is rebalanced as time passes and information is revealed.
:::
## 4.11 Self-Financing Strategies
A dynamic trading strategy specifies holdings in the stock and bank account at each node. Let
$$
\Delta_n
$$
be the number of shares held over the interval $[n,n+1]$, and let
$$
B_n^{\mathrm{hold}}
$$
be the number of dollars invested in the bank account over the same interval.
The portfolio value at time $n$ is
$$
V_n=\Delta_nS_n+B_n^{\mathrm{hold}}.
$$
A strategy is **self-financing** if changes in the portfolio holdings are paid for only by reallocating wealth within the portfolio, with no external cash added or removed.
In discrete time, this means that after the time-$n$ portfolio value is realized, the trader may rebalance holdings for the next period, but the cost of the new holdings must equal the available wealth:
$$
\Delta_{n+1}S_{n+1}+B_{n+1}^{\mathrm{hold}}
=
\Delta_nS_{n+1}+R B_n^{\mathrm{hold}}.
$$
This equation is the mathematical expression of dynamic replication.
::: {.warning}
### Common mistake
A hedge ratio formula computes the number of shares to hold over the next period. It does not mean the trader holds the same number of shares for the whole life of the option. In a multi-period model, replication is a dynamic strategy, not a static one.
:::
## 4.12 American Options and Early Exercise
An American option may be exercised at any time before or at maturity. This changes the pricing problem from conditional expectation to optimal stopping.
Let $g(S_n)$ be the immediate exercise payoff at time $n$. For an American put,
$$
g(S_n)=(K-S_n)^+.
$$
At maturity, the option value is
$$
V_N(j)=g(S_N(j)).
$$
At an earlier node, the holder chooses between immediate exercise and continuation. Therefore
$$
V_n(j)=\max\left\{g(S_n(j)),\;\frac{1}{R}\left[qV_{n+1}(j+1)+(1-q)V_{n+1}(j)\right]\right\}.
$$
The continuation value is the no-arbitrage value of keeping the option alive for one more period.
::: {.definition}
### Definition 4.4: Snell-envelope recursion
For an American payoff process $g(S_n)$, the binomial American option value is defined backward by
$$
V_N(j)=g(S_N(j)),
$$
and
$$
V_n(j)=\max\left\{g(S_n(j)),\;R^{-1}\left[qV_{n+1}(j+1)+(1-q)V_{n+1}(j)\right]\right\}.
$$
This backward recursion is the discrete-time Snell envelope.
:::
::: {.example}
### Example 4.3: Early exercise of an American put
Let
$$
S_0=100,\quad K=100,\quad u=1.2,\quad d=0.8,\quad R=1.05,\quad N=2.
$$
The terminal put payoffs are
$$
36,\quad 4,\quad 0
$$
at terminal stock prices
$$
64,\quad 96,\quad 144.
$$
At node $(1,0)$, the stock price is $80$. The immediate exercise value is
$$
100-80=20.
$$
The continuation value is
$$
\frac{1}{1.05}\left[0.625(4)+0.375(36)\right]=15.2381.
$$
Therefore early exercise is optimal at this node, and the node value is $20$.
:::
## 4.13 Put-Call Parity in the Binomial Model
For European options on a non-dividend-paying stock, put-call parity holds in the binomial model:
$$
C_0-P_0=S_0-KR^{-N}.
$$
More generally, at time $n$ and node $(n,j)$,
$$
C_n(j)-P_n(j)=S_n(j)-KR^{-(N-n)}.
$$
::: {.proof}
At maturity,
$$
(S_N-K)^+-(K-S_N)^+=S_N-K.
$$
Thus a long call and short put have the same terminal payoff as a forward-style position paying $S_N-K$. The time-$n$ value of $S_N$ is $S_n$ because the stock itself is traded, and the time-$n$ value of paying $K$ at time $N$ is $KR^{-(N-n)}$. By the law of one price,
$$
C_n-P_n=S_n-KR^{-(N-n)}.
$$
:::
American put-call parity is more subtle because early exercise changes the payoff timing.
## 4.14 Interactive: Binomial Tree Pricing Explorer
The next activity builds a binomial tree and prices a European call option by backward induction.
::: {.interactive-card}
### Multi-period binomial call explorer
Use the sliders to change model parameters. The plot shows the terminal payoff and the risk-neutral probability mass over terminal stock prices.
<div class="mf-controls">
<label for="ch04-s0">Current stock price $S_0$</label>
<output id="ch04-s0-out">100</output>
<input id="ch04-s0" type="range" min="50" max="150" step="1" value="100">
<label for="ch04-k">Strike $K$</label>
<output id="ch04-k-out">100</output>
<input id="ch04-k" type="range" min="50" max="150" step="1" value="100">
<label for="ch04-r">Gross riskless return $R$</label>
<output id="ch04-r-out">1.02</output>
<input id="ch04-r" type="range" min="0.95" max="1.10" step="0.005" value="1.02">
<label for="ch04-u">Up factor $u$</label>
<output id="ch04-u-out">1.10</output>
<input id="ch04-u" type="range" min="1.01" max="1.40" step="0.01" value="1.10">
<label for="ch04-d">Down factor $d$</label>
<output id="ch04-d-out">0.92</output>
<input id="ch04-d" type="range" min="0.60" max="0.99" step="0.01" value="0.92">
<label for="ch04-n">Number of periods $N$</label>
<output id="ch04-n-out">5</output>
<input id="ch04-n" type="range" min="1" max="20" step="1" value="5">
</div>
<div id="ch04-binomial-summary" class="mf-output"></div>
<div id="ch04-binomial-plot" class="plotly-demo"></div>
<script>
(function() {
function getNumber(id) { return parseFloat(document.getElementById(id).value); }
function binom(n, k) {
if (k < 0 || k > n) return 0;
k = Math.min(k, n - k);
let c = 1;
for (let i = 1; i <= k; i++) c = c * (n - k + i) / i;
return c;
}
function fmt(x) { return Number.isFinite(x) ? x.toFixed(4) : "not defined"; }
function updateDemo() {
const S0 = getNumber("ch04-s0");
const K = getNumber("ch04-k");
const R = getNumber("ch04-r");
const u = getNumber("ch04-u");
const d = getNumber("ch04-d");
const N = parseInt(document.getElementById("ch04-n").value, 10);
document.getElementById("ch04-s0-out").value = S0.toFixed(0);
document.getElementById("ch04-k-out").value = K.toFixed(0);
document.getElementById("ch04-r-out").value = R.toFixed(3);
document.getElementById("ch04-u-out").value = u.toFixed(2);
document.getElementById("ch04-d-out").value = d.toFixed(2);
document.getElementById("ch04-n-out").value = N.toString();
const noArb = d < R && R < u;
const q = (R - d) / (u - d);
let prices = [];
let payoffs = [];
let probs = [];
let weighted = [];
for (let j = 0; j <= N; j++) {
const s = S0 * Math.pow(u, j) * Math.pow(d, N - j);
const payoff = Math.max(s - K, 0);
const prob = binom(N, j) * Math.pow(q, j) * Math.pow(1 - q, N - j);
prices.push(s);
payoffs.push(payoff);
probs.push(noArb ? prob : 0);
weighted.push(noArb ? prob * payoff : 0);
}
const expectedPayoff = weighted.reduce((a, b) => a + b, 0);
const value = expectedPayoff / Math.pow(R, N);
const summary = noArb
? `<strong>No-arbitrage:</strong> satisfied. q = ${fmt(q)}.<br>
<strong>European call price:</strong> C0 = ${fmt(value)}.<br>
<strong>Discount factor:</strong> R^{-N} = ${fmt(Math.pow(R, -N))}.`
: `<strong>No-arbitrage:</strong> violated. Need d < R < u. Here q = ${fmt(q)}, not a valid risk-neutral probability.`;
document.getElementById("ch04-binomial-summary").innerHTML = summary;
const payoffTrace = {
x: prices,
y: payoffs,
mode: "markers+lines",
name: "terminal call payoff",
marker: {size: 9},
yaxis: "y1"
};
const probTrace = {
x: prices,
y: probs,
type: "bar",
name: "risk-neutral probability",
opacity: 0.45,
yaxis: "y2"
};
const layout = {
title: "Terminal payoffs and risk-neutral probabilities",
xaxis: {title: "terminal stock price S_N"},
yaxis: {title: "payoff"},
yaxis2: {title: "probability", overlaying: "y", side: "right", rangemode: "tozero"},
margin: {l: 60, r: 70, t: 55, b: 55},
barmode: "overlay"
};
Plotly.react("ch04-binomial-plot", [payoffTrace, probTrace], layout, {responsive: true});
}
["ch04-s0", "ch04-k", "ch04-r", "ch04-u", "ch04-d", "ch04-n"].forEach(function(id) {
document.getElementById(id).addEventListener("input", updateDemo);
});
updateDemo();
})();
</script>
:::
## 4.15 Python Example: Building a Binomial Tree
The following code builds the recombining stock-price tree as a list of arrays.
```{python}
#| label: ch04-build-stock-tree
#| echo: true
#| message: false
#| warning: false
import numpy as np
S0 = 100.0
u = 1.20
d = 0.80
N = 4
tree = []
for n in range(N + 1):
level = np.array([S0 * (u ** j) * (d ** (n - j)) for j in range(n + 1)])
tree.append(level)
for n, level in enumerate(tree):
print(f"time {n}:", np.round(level, 4))
```
The indexing convention is that entry $j$ in level $n$ corresponds to $j$ up moves.
## 4.16 Python Example: European Option by Backward Induction
```{python}
#| label: ch04-european-backward-induction
#| echo: true
#| message: false
#| warning: false
import numpy as np
def binomial_european_option(S0, K, R, u, d, N, option_type="call"):
"""Price a European call or put in the N-period binomial model."""
if not (d < R < u):
raise ValueError("No-arbitrage condition d < R < u is required.")
q = (R - d) / (u - d)
terminal_prices = np.array([S0 * (u ** j) * (d ** (N - j)) for j in range(N + 1)])
if option_type == "call":
values = np.maximum(terminal_prices - K, 0.0)
elif option_type == "put":
values = np.maximum(K - terminal_prices, 0.0)
else:
raise ValueError("option_type must be 'call' or 'put'.")
tree_values = [None] * (N + 1)
tree_values[N] = values.copy()
for n in range(N - 1, -1, -1):
values = (q * values[1:] + (1 - q) * values[:-1]) / R
tree_values[n] = values.copy()
return tree_values[0][0], tree_values
price, value_tree = binomial_european_option(
S0=100, K=100, R=1.05, u=1.2, d=0.8, N=2, option_type="call"
)
print("European call price:", round(price, 6))
for n, values in enumerate(value_tree):
print(f"V_{n}:", np.round(values, 6))
```
This example reproduces the two-step call price from Example 4.2.
## 4.17 Python Example: Dynamic Hedge Ratios
The next function computes both the option values and the hedge ratios $\Delta_n(j)$.
```{python}
#| label: ch04-dynamic-deltas
#| echo: true
#| message: false
#| warning: false
import numpy as np
def binomial_values_and_deltas(S0, K, R, u, d, N, option_type="call"):
if not (d < R < u):
raise ValueError("No-arbitrage condition d < R < u is required.")
q = (R - d) / (u - d)
stock_tree = [np.array([S0 * (u ** j) * (d ** (n - j)) for j in range(n + 1)])
for n in range(N + 1)]
terminal = stock_tree[N]
if option_type == "call":
values = np.maximum(terminal - K, 0.0)
else:
values = np.maximum(K - terminal, 0.0)
value_tree = [None] * (N + 1)
delta_tree = [None] * N
value_tree[N] = values.copy()
for n in range(N - 1, -1, -1):
next_values = values
current_prices = stock_tree[n]
deltas = (next_values[1:] - next_values[:-1]) / (current_prices * (u - d))
values = (q * next_values[1:] + (1 - q) * next_values[:-1]) / R
delta_tree[n] = deltas.copy()
value_tree[n] = values.copy()
return stock_tree, value_tree, delta_tree
stock_tree, value_tree, delta_tree = binomial_values_and_deltas(
S0=100, K=100, R=1.05, u=1.2, d=0.8, N=3, option_type="call"
)
print("Option values:")
for n, values in enumerate(value_tree):
print(f"V_{n}:", np.round(values, 4))
print("\nHedge ratios:")
for n, deltas in enumerate(delta_tree):
print(f"Delta_{n}:", np.round(deltas, 4))
```
The hedge ratios are local slopes of the option value with respect to the stock price along the tree.
## 4.18 Python Example: American Put by Backward Induction
```{python}
#| label: ch04-american-put
#| echo: true
#| message: false
#| warning: false
import numpy as np
def binomial_american_put(S0, K, R, u, d, N):
if not (d < R < u):
raise ValueError("No-arbitrage condition d < R < u is required.")
q = (R - d) / (u - d)
stock_tree = [np.array([S0 * (u ** j) * (d ** (n - j)) for j in range(n + 1)])
for n in range(N + 1)]
values = np.maximum(K - stock_tree[N], 0.0)
value_tree = [None] * (N + 1)
exercise_tree = [None] * N
value_tree[N] = values.copy()
for n in range(N - 1, -1, -1):
continuation = (q * values[1:] + (1 - q) * values[:-1]) / R
exercise = np.maximum(K - stock_tree[n], 0.0)
values = np.maximum(exercise, continuation)
exercise_tree[n] = exercise > continuation
value_tree[n] = values.copy()
return value_tree[0][0], value_tree, exercise_tree
american_price, amer_values, exercise = binomial_american_put(
S0=100, K=100, R=1.05, u=1.2, d=0.8, N=2
)
print("American put price:", round(american_price, 6))
for n, values in enumerate(amer_values):
print(f"V_{n}:", np.round(values, 6))
for n, ex in enumerate(exercise):
print(f"exercise at time {n}:", ex)
```
The Boolean arrays mark nodes where immediate exercise is better than continuation.
## 4.19 Calibrating the Binomial Parameters to Volatility
To approximate a continuous-time model over horizon $T$, we divide time into $N$ steps of length
$$
\Delta t=\frac{T}{N}.
$$
A common Cox--Ross--Rubinstein parameterization is
$$
u=e^{\sigma\sqrt{\Delta t}},
\qquad
d=e^{-\sigma\sqrt{\Delta t}},
\qquad
R=e^{r\Delta t}.
$$
Then
$$
ud=1.
$$
The risk-neutral probability is
$$
q=\frac{e^{r\Delta t}-e^{-\sigma\sqrt{\Delta t}}}
{e^{\sigma\sqrt{\Delta t}}-e^{-\sigma\sqrt{\Delta t}}}.
$$
As $N$ increases, the binomial model becomes a better discrete approximation to geometric Brownian motion. Later, the Black--Scholes model will arise as a continuous-time limit.
::: {.remark}
The binomial model is not only a numerical trick. It is a pedagogical bridge from finite-dimensional no-arbitrage linear algebra to stochastic calculus. Many continuous-time results become easier to understand after seeing their exact discrete-time analogues.
:::
## 4.20 Interactive: Convergence Toward Black--Scholes
The following activity compares a Cox--Ross--Rubinstein binomial call price with the Black--Scholes call price as the number of steps increases.
::: {.interactive-card}
### Binomial convergence explorer
<div class="mf-controls">
<label for="ch04conv-s0">Current stock price $S_0$</label>
<output id="ch04conv-s0-out">100</output>
<input id="ch04conv-s0" type="range" min="50" max="150" step="1" value="100">
<label for="ch04conv-k">Strike $K$</label>
<output id="ch04conv-k-out">100</output>
<input id="ch04conv-k" type="range" min="50" max="150" step="1" value="100">
<label for="ch04conv-r">Annual rate $r$</label>
<output id="ch04conv-r-out">0.05</output>
<input id="ch04conv-r" type="range" min="0.00" max="0.12" step="0.005" value="0.05">
<label for="ch04conv-sigma">Volatility $\sigma$</label>
<output id="ch04conv-sigma-out">0.20</output>
<input id="ch04conv-sigma" type="range" min="0.05" max="0.60" step="0.01" value="0.20">
<label for="ch04conv-t">Maturity $T$</label>
<output id="ch04conv-t-out">1.00</output>
<input id="ch04conv-t" type="range" min="0.25" max="3.00" step="0.25" value="1.00">
</div>
<div id="ch04-conv-summary" class="mf-output"></div>
<div id="ch04-conv-plot" class="plotly-demo"></div>
<script>
(function() {
function getNumber(id) { return parseFloat(document.getElementById(id).value); }
function erf(x) {
const sign = x >= 0 ? 1 : -1;
x = Math.abs(x);
const a1 = 0.254829592, a2 = -0.284496736, a3 = 1.421413741;
const a4 = -1.453152027, a5 = 1.061405429, p = 0.3275911;
const t = 1 / (1 + p * x);
const y = 1 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
return sign * y;
}
function normCdf(x) { return 0.5 * (1 + erf(x / Math.sqrt(2))); }
function bsCall(S0, K, r, sigma, T) {
const d1 = (Math.log(S0 / K) + (r + 0.5 * sigma * sigma) * T) / (sigma * Math.sqrt(T));
const d2 = d1 - sigma * Math.sqrt(T);
return S0 * normCdf(d1) - K * Math.exp(-r * T) * normCdf(d2);
}
function binomCall(S0, K, r, sigma, T, N) {
const dt = T / N;
const u = Math.exp(sigma * Math.sqrt(dt));
const d = 1 / u;
const R = Math.exp(r * dt);
const q = (R - d) / (u - d);
let values = [];
for (let j = 0; j <= N; j++) {
const s = S0 * Math.pow(u, j) * Math.pow(d, N - j);
values.push(Math.max(s - K, 0));
}
for (let n = N - 1; n >= 0; n--) {
let next = [];
for (let j = 0; j <= n; j++) {
next.push((q * values[j + 1] + (1 - q) * values[j]) / R);
}
values = next;
}
return values[0];
}
function updateConv() {
const S0 = getNumber("ch04conv-s0");
const K = getNumber("ch04conv-k");
const r = getNumber("ch04conv-r");
const sigma = getNumber("ch04conv-sigma");
const T = getNumber("ch04conv-t");
document.getElementById("ch04conv-s0-out").value = S0.toFixed(0);
document.getElementById("ch04conv-k-out").value = K.toFixed(0);
document.getElementById("ch04conv-r-out").value = r.toFixed(3);
document.getElementById("ch04conv-sigma-out").value = sigma.toFixed(2);
document.getElementById("ch04conv-t-out").value = T.toFixed(2);
const Ns = [1, 2, 3, 5, 10, 20, 40, 80, 120, 160];
const prices = Ns.map(N => binomCall(S0, K, r, sigma, T, N));
const bs = bsCall(S0, K, r, sigma, T);
document.getElementById("ch04-conv-summary").innerHTML =
`<strong>Black--Scholes reference:</strong> ${bs.toFixed(4)}.<br>
<strong>Binomial price at N = ${Ns[Ns.length - 1]}:</strong> ${prices[prices.length - 1].toFixed(4)}.`;
const trace = {x: Ns, y: prices, mode: "markers+lines", name: "binomial call price"};
const ref = {x: Ns, y: Ns.map(() => bs), mode: "lines", name: "Black--Scholes reference", line: {dash: "dash"}};
const layout = {
title: "CRR binomial price as N increases",
xaxis: {title: "number of binomial steps N", type: "log"},
yaxis: {title: "call price"},
margin: {l: 60, r: 25, t: 55, b: 55}
};
Plotly.react("ch04-conv-plot", [trace, ref], layout, {responsive: true});
}
["ch04conv-s0", "ch04conv-k", "ch04conv-r", "ch04conv-sigma", "ch04conv-t"].forEach(function(id) {
document.getElementById(id).addEventListener("input", updateConv);
});
updateConv();
})();
</script>
:::
## 4.21 Python Example: CRR Convergence
```{python}
#| label: ch04-crr-convergence
#| echo: true
#| message: false
#| warning: false
import math
import numpy as np
import pandas as pd
def normal_cdf(x):
return 0.5 * (1.0 + math.erf(x / math.sqrt(2.0)))
def black_scholes_call(S0, K, r, sigma, T):
d1 = (math.log(S0 / K) + (r + 0.5 * sigma**2) * T) / (sigma * math.sqrt(T))
d2 = d1 - sigma * math.sqrt(T)
return S0 * normal_cdf(d1) - K * math.exp(-r * T) * normal_cdf(d2)
def crr_call(S0, K, r, sigma, T, N):
dt = T / N
u = math.exp(sigma * math.sqrt(dt))
d = math.exp(-sigma * math.sqrt(dt))
R = math.exp(r * dt)
q = (R - d) / (u - d)
values = np.array([
max(S0 * (u ** j) * (d ** (N - j)) - K, 0.0)
for j in range(N + 1)
])
for n in range(N - 1, -1, -1):
values = (q * values[1:] + (1 - q) * values[:-1]) / R
return float(values[0])
S0, K, r, sigma, T = 100.0, 100.0, 0.05, 0.20, 1.0
bs = black_scholes_call(S0, K, r, sigma, T)
rows = []
for N in [1, 2, 5, 10, 25, 50, 100, 200]:
price = crr_call(S0, K, r, sigma, T, N)
rows.append({"N": N, "CRR price": price, "BS reference": bs, "error": price - bs})
pd.DataFrame(rows)
```
The error does not necessarily decrease monotonically at every step, but the general convergence pattern becomes visible as $N$ grows.
## 4.22 Mathematical Summary
The chapter can be summarized by four equations.
First, the stock tree is
$$
S_n(j)=S_0u^jd^{n-j}.
$$
Second, the no-arbitrage condition is
$$
d<R<u.
$$
Third, the risk-neutral probability is
$$
q=\frac{R-d}{u-d}.
$$
Fourth, the European pricing recursion is
$$
V_n(j)=\frac{1}{R}\left[qV_{n+1}(j+1)+(1-q)V_{n+1}(j)\right].
$$
The American recursion adds an exercise choice:
$$
V_n(j)=\max\left\{g(S_n(j)),\frac{1}{R}\left[qV_{n+1}(j+1)+(1-q)V_{n+1}(j)\right]\right\}.
$$
## 4.23 AI-Assisted Learning Components
::: {.ai-box}
### AI prompt 1: Explain the backward recursion
Ask an AI system:
> Explain why the binomial backward induction formula
> $V_n(j)=R^{-1}[qV_{n+1}(j+1)+(1-q)V_{n+1}(j)]$
> is a local one-period no-arbitrage pricing formula, not an ordinary physical expectation.
Then check whether the answer clearly distinguishes $p$ and $q$.
:::
::: {.ai-box}
### AI prompt 2: Audit a proof
Ask an AI system to prove that $\widetilde S_n=S_n/R^n$ is a martingale under $\mathbb Q$. Then audit the proof using these questions:
1. Does it condition on $\mathcal F_n$?
2. Does it use $qu+(1-q)d=R$?
3. Does it divide by the correct power of $R$?
4. Does it avoid assuming that $p=q$?
:::
::: {.ai-box}
### AI prompt 3: Code review
Ask an AI system to review your binomial option-pricing function. Require it to check:
- the no-arbitrage condition $d<R<u$;
- whether terminal prices are indexed consistently;
- whether the backward recursion uses `values[1:]` for up nodes and `values[:-1]` for down nodes;
- whether the discount factor is applied once per step;
- whether American exercise is compared with continuation at every node.
:::
::: {.ai-box}
### AI prompt 4: Generate test cases
Ask an AI system to propose unit tests for a binomial pricing implementation. Good tests should include:
- one-period prices that match Chapter 3;
- put-call parity for European options;
- monotonicity in $S_0$ for calls;
- monotonicity in $K$ for puts;
- American put price greater than or equal to European put price;
- error handling when $d<R<u$ fails.
:::
## 4.24 Common Mistakes
::: {.warning}
### Mistake 1: Using the physical probability for pricing
The real-world probability $p$ may be useful for forecasting, risk management, and statistical estimation. The no-arbitrage price uses the risk-neutral probability $q$.
:::
::: {.warning}
### Mistake 2: Forgetting to discount at each backward step
The recursion is
$$
V_n(j)=R^{-1}\left[qV_{n+1}(j+1)+(1-q)V_{n+1}(j)\right],
$$
not merely the weighted average of future values.
:::
::: {.warning}
### Mistake 3: Confusing path count and node count
There are $2^n$ paths by time $n$, but only $n+1$ nodes in a recombining binomial tree.
:::
::: {.warning}
### Mistake 4: Treating delta as constant
A multi-period hedge ratio depends on the node. It is usually written as $\Delta_n(j)$, not just $\Delta$.
:::
## 4.25 Exercises
### Conceptual Exercises
1. Explain why $d<R<u$ is the no-arbitrage condition in a one-step binomial model.
2. Explain why the same condition is sufficient for no-arbitrage in the multi-period binomial model.
3. In your own words, distinguish the physical probability $p$ from the risk-neutral probability $q$.
4. Why is a binomial tree called recombining?
5. Why does dynamic replication require rebalancing?
### Computational Exercises
6. Let $S_0=50$, $u=1.1$, $d=0.9$, $R=1.02$, $N=3$, and $K=50$. Compute the European call price by backward induction.
7. For the same parameters, compute the European put price and verify put-call parity.
8. Compute the hedge ratios $\Delta_n(j)$ for the call in Exercise 6.
9. Compute the American put price for the same parameters and identify the nodes where early exercise is optimal.
10. Compare the European and American put prices. Explain the result.
### Proof Exercises
11. Prove that under $\mathbb Q$, the number of up moves by time $N$ has binomial distribution $\operatorname{Binomial}(N,q)$.
12. Prove the closed-form binomial pricing formula for path-independent European payoffs.
13. Prove put-call parity in the $N$-period binomial model.
14. Prove that an American option value is always at least its immediate exercise value.
15. Prove that the American put price is at least the European put price with the same strike and maturity.
### Python Exercises
16. Write a function that returns the full stock-price tree as a triangular list of NumPy arrays.
17. Write a function that prices a European digital option paying $1$ if $S_N>K$ and $0$ otherwise.
18. Modify the American put function to return a table of exercise decisions.
19. Use the CRR parameterization to plot binomial call prices for $N=1,2,\ldots,200$.
20. Write unit tests for your option-pricing functions.
### AI-Assisted Exercises
21. Ask an AI system to explain the difference between a martingale under $\mathbb P$ and a martingale under $\mathbb Q$. Critique the answer.
22. Ask an AI system to derive the hedge ratio formula. Check whether it correctly subtracts the up and down equations.
23. Ask an AI system to generate a wrong binomial pricing implementation. Identify and correct the errors.
24. Ask an AI system to explain why American calls on non-dividend-paying stocks are usually not exercised early. Compare the answer with put-call parity intuition.
25. Ask an AI system to design a final project based on binomial trees and risk-neutral pricing. Refine the project into a precise mathematical question.
## 4.26 Chapter Summary
The multi-period binomial model extends the one-period model by repeating the same no-arbitrage argument at every node. Its essential ingredients are the stock tree, the bank account, the no-arbitrage inequality $d<R<u$, and the risk-neutral probability
$$
q=\frac{R-d}{u-d}.
$$
Under this probability, discounted traded asset prices are martingales. European derivative prices are discounted conditional expectations under $\mathbb Q$, while American derivative prices are obtained by comparing immediate exercise value with continuation value. The backward induction method is both a pricing algorithm and a mathematical expression of dynamic no-arbitrage.
This chapter prepares the way for the next chapter on discrete-time martingales, where the martingale structure is developed more systematically.