This tutorial demonstrates how to price European and American options using a binomial tree model in Python. The binomial tree model is a popular method for option pricing due to its simplicity and flexibility.
Recap: Binomial Tree Model
The binomial tree model is a discrete-time model for the evolution of an asset’s price. It assumes that, over each small time step, the asset price can move to one of two possible values: an “up” value or a “down” value. The model is built on the following parameters:
Current stock price: \(S_0\)
Strike price of the option: \(K\)
Time to maturity (in years): \(T\)
Risk-free interest rate (annualized): \(r\)
Volatility of the underlying asset (annualized): \(\sigma\)
Number of time steps in the binomial tree: \(N\)
From these parameters, we can calculate the parameters needed for the binomial tree:
Length of each time step: \(\Delta t = \frac{T}{N}\)
Up factor: \(u = e^{\sigma \sqrt{\Delta t}}\)
Down factor: \(d = 1/u\)
Risk-neutral probability of an up move: \(p = \frac{e^{r \Delta t} - d}{u - d}\)
Asset prices at each node of the tree can be calculated as follows:
\[S_{j,i} = S_0 \cdot u^{i-j} \cdot d^{j},\]
where \(i={0,1,2,...,N}\) is the time step and \(j={0,1,2,...,i}\) is the number of down moves.1
1 The notation and formula are chosen such that it matches the code implementation later.
The value of options can be calculated using backward induction, starting from the terminal nodes (maturity) and working backward to the root node (current time).
The value of the option at end of the tree is determined by its payoff at maturity. For a call option, the payoff is given by (denote \(f_{j, i}\) as the option value at node \(j\) at time step \(i\)):
\[f_{j, N} = \max(0, S_{j, N} - K)\]
For a put option, the payoff is given by:
\[f_{j, N} = \max(0, K - S_{j, N})\]
The option value at each preceding node is calculated as the discounted expected value of the option at the next time step:
Implementing the binomial tree model involves the following steps:
Parameters: Define the parameters for the option and the underlying asset. Calculate the binomial tree parameters.
Tree Construction: Construct a binomial tree to store the possible future prices of the option and the underlying asset at each time step. In our implementation, we use 2D arrays to represent the binomial tree. Therefore, effectively, only the upper triangles of the arrays are used.2 We construct three 2D arrays: one for asset prices, one for option values, and one to track whether the option is exercised at each node.3
2 For example, for a tree with \(N=2\), we create a 2D array with 3 rows and 3 columns to store option values. However, we really just need the upper triangle (including the diagonal) of the array to store the option values for all nodes:
3 We use 2D arrays mainly for clarity and visualization purpose. In practice, if intermediate results are not needed, we can optimize the memory usage by using 1D arrays, as shown later.
Asset Price Calculation: Calculate the asset prices at each node of the tree using the up and down factors.
Option Valuation: Calculate the option value at each node of the tree, starting from the terminal nodes (maturity) and working backward to the root node (current time). For European options, the option can only be exercised at maturity, while for American options, it can be exercised at any time before or at maturity.
Pricing a European Call Option
We start by pricing a European call option using a binomial tree with 2 time steps.
import numpy as np# ParametersS0 =100# Initial stock priceK =100# Strike priceT =1# Time to maturity in yearsr =0.05# Risk-free interest ratesigma =0.2# VolatilityN =2# Number of time steps# Calculate parameters for the binomial treedt = T / N # Time step sizeu = np.exp(sigma * np.sqrt(dt)) # Up factord =1/ u # Down factorp = (np.exp(r * dt) - d) / (u - d) # Risk-neutral probability# Initialize three 2D arrays to represent the binomial tree# One for asset prices, one for option values, and one to track exercisesasset_prices = np.zeros((N +1, N +1))option_values = np.zeros((N +1, N +1))exercises = np.zeros((N +1, N +1), dtype=bool)# Set asset prices at each nodefor i inrange(N +1):for j inrange(i +1): asset_prices[j, i] = S0 * (u ** (i - j)) * (d ** j)# Set option values at maturityoption_values[:, N] = np.maximum(0, asset_prices[:, N] - K)# Set exercise values at maturityexercises[:, N] = (asset_prices[:, N] - K) >0# Backward induction to calculate option price at earlier nodesfor i inrange(N -1, -1, -1):# Time step ifor j inrange(i +1):# Update option values option_values[j, i] = np.exp(-r * dt) * (p * option_values[j, i +1] + (1- p) * option_values[j +1, i +1])# Display parameters and option priceprint("Strike Price:", K)print(f"Time to Maturity (T): {T} year(s)")print(f"Time Step (dt): {dt:.4f} year(s)")print(f"Discount Factor per Step: {np.exp(-r * dt):.4f}")print(f"Risk-neutral Probability (p): {p:.4f}")print(f"Up Factor (u): {u:.4f}")print(f"Down Factor (d): {d:.4f}")print()print(f"Option Price: {option_values[0, 0]:.4f}")
Strike Price: 100
Time to Maturity (T): 1 year(s)
Time Step (dt): 0.5000 year(s)
Discount Factor per Step: 0.9753
Risk-neutral Probability (p): 0.5539
Up Factor (u): 1.1519
Down Factor (d): 0.8681
Option Price: 9.5405
We can visualize the binomial tree structure and the option values at each node.
Define a function to plot the binomial tree (click to expand)
# Plotting the Binomial Treeimport matplotlib.pyplot as pltdef plot_binomial_tree(asset_prices, option_values, exercises, N): fig, ax = plt.subplots()for i inrange(N +1):for j inrange(i +1): x = i # Set x-coordinate based on time step y = i -2* j # Adjust y-coordinate for symmetry# Plot the nodeif exercises[j, i]: fcolor ='lemonchiffon'# Color for exercised nodeselse: fcolor ='white'# Color for non-exercised nodes ax.text(x, y, f"S: {asset_prices[j, i]:.2f}\nf: {option_values[j, i]:.2f}", ha='center', va='center', bbox=dict(facecolor=fcolor, edgecolor='black'))# Draw branchesif i < N: ax.plot([x, x +1], [y, y -1], 'k-') # Downward branch ax.plot([x, x +1], [y, y +1], 'k-') # Upward branch# Set limits and labels ax.set_xlim(0, N) ax.set_ylim(-N -1, N +1) ax.set_xlabel('Time Steps')# Set x-ticks (start from 0 and increment by 1) ax.set_xticks(range(N +1))# Remove unnecessary spines and ticks ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.spines['left'].set_visible(False) ax.yaxis.set_visible(False) ax.set_title('Binomial Tree for Option Pricing') plt.show()
# Plot the treeplot_binomial_tree(asset_prices, option_values, exercises, N)
Pricing a American Put Option
The process for pricing an American put option is similar, but we need to account for the possibility of early exercise at each node. (Three code changes are marked in the comments.)
import numpy as np# ParametersS0 =100# Initial stock priceK =100# Strike priceT =1# Time to maturity in yearsr =0.05# Risk-free interest ratesigma =0.2# VolatilityN =4# Number of time steps# Calculate parameters for the binomial treedt = T / N # Time step sizeu = np.exp(sigma * np.sqrt(dt)) # Up factord =1/ u # Down factorp = (np.exp(r * dt) - d) / (u - d) # Risk-neutral probability# Initialize three 2D arrays to represent the binomial tree# One for asset prices, one for option values, and one to track exercisesasset_prices = np.zeros((N +1, N +1))option_values = np.zeros((N +1, N +1))exercises = np.zeros((N +1, N +1), dtype=bool)# Set asset prices at each nodefor i inrange(N +1):for j inrange(i +1): asset_prices[j, i] = S0 * (u ** (i - j)) * (d ** j)# Set option values at maturity (Change 1)option_values[:, N] = np.maximum(0, K - asset_prices[:, N])# Set exercise values at maturity (Change 2)exercises[:, N] = (K - asset_prices[:, N]) >0# Backward induction to calculate option price at earlier nodesfor i inrange(N -1, -1, -1):# Time step ifor j inrange(i +1):# Update option values option_values[j, i] = np.exp(-r * dt) * (p * option_values[j, i +1] + (1- p) * option_values[j +1, i +1])# Check for early exercise and update option values accordingly (Change 3) exercises[j, i] = (K - asset_prices[j, i]) > option_values[j, i] option_values[j, i] = np.maximum(option_values[j, i], K - asset_prices[j, i])# Display parameters and option priceprint("Strike Price:", K)print(f"Time to Maturity (T): {T} year(s)")print(f"Time Step (dt): {dt:.4f} year(s)")print(f"Discount Factor per Step: {np.exp(-r * dt):.4f}")print(f"Risk-neutral Probability (p): {p:.4f}")print(f"Up Factor (u): {u:.4f}")print(f"Down Factor (d): {d:.4f}")print()print(f"Option Price: {option_values[0, 0]:.4f}")
Strike Price: 100
Time to Maturity (T): 1 year(s)
Time Step (dt): 0.2500 year(s)
Discount Factor per Step: 0.9876
Risk-neutral Probability (p): 0.5378
Up Factor (u): 1.1052
Down Factor (d): 0.9048
Option Price: 5.8828
# Plot the treeplot_binomial_tree(asset_prices, option_values, exercises, N)
Putting It All Together with Efficiency Improvements
We can encapsulate the binomial tree option pricing logic into a single function that can handle both European and American options, as well as call and put options.
In this implementation, we are not interested in storing the entire binomial tree structure for visualization purposes. On the other hand, we focus on optimizing the efficiency of the algorithm.
Since during the backward induction process, we only need the current and next time step values, we can reduce the memory usage by using 1D arrays instead of 2D arrays.
At each time step, instead of looping through all nodes, we can use Numpy’s vectorized operations and array slicing to update only the relevant parts of the 1D arrays. NumPy’s internal optimizations make these operations faster than explicit Python loops.
These optimizations will be particularly useful when dealing with a large number of time steps.
import numpy as npdef binomial_tree(S0, K, T, r, sigma, N, call=True, american=False):# Calculate parameters dt = T / N u = np.exp(sigma * np.sqrt(dt)) d =1/ u p = (np.exp(r * dt) - d) / (u - d)# Initialize two 1D arrays to keep track of asset prices and option values# These arrays will be updated in place during the backward induction at each time step# Only the necessary parts of the arrays will be used when updating S = np.zeros(N +1) f = np.zeros(N +1)# Set asset prices at maturity# Note: since d = 1 / u, the calculation can be simplified S = S0 * u ** np.arange(N, -N -2, -2)# Set option values at maturityif call: f = np.maximum(0, S - K)else: f = np.maximum(0, K - S)# Backward induction to calculate option price at earlier nodesfor i inrange(N -1, -1, -1):# Time step i# Only the first i+1 elements need to be updated at this step last_idx = i +1# Update option values f[:last_idx] = np.exp(-r * dt) * (p * f[:last_idx] + (1- p) * f[1:last_idx +1])# For American options, check for early exerciseif american:# Update asset prices S[:last_idx] = S0 * u ** np.arange(i, -i -2, -2)# Check for early exercise and update option values accordinglyif call: f[:last_idx] = np.maximum(f[:last_idx], S[:last_idx] - K)else: f[:last_idx] = np.maximum(f[:last_idx], K - S[:last_idx])return f[0]
Let’s apply this function using the same parameters as before.
# European Call OptionS0 =100# Initial stock priceK =100# Strike priceT =1# Time to maturity in yearsr =0.05# Risk-free interest ratesigma =0.2# VolatilityN =2# Number of time stepscall =True# True for Call option, False for Put optionamerican =False# True for American option, False for European# Calculate option priceoption_price = binomial_tree(S0, K, T, r, sigma, N, call, american)print(f"Option Price: {option_price:.4f}")
Option Price: 9.5405
# American Put OptionS0 =100# Initial stock priceK =100# Strike priceT =1# Time to maturity in yearsr =0.05# Risk-free interest ratesigma =0.2# VolatilityN =4# Number of time stepscall =False# True for Call option, False for Put optionamerican =True# True for American option, False for European# Calculate option priceoption_price = binomial_tree(S0, K, T, r, sigma, N, call, american)print(f"Option Price: {option_price:.4f}")
Option Price: 5.8828
Note that the results match those obtained from the previous implementations.
Finally, let’s use a larger number of time steps to get more accurate results.
# European Call OptionS0 =100# Initial stock priceK =100# Strike priceT =1# Time to maturity in yearsr =0.05# Risk-free interest ratesigma =0.2# VolatilityN =15000# Number of time stepscall =True# True for Call option, False for Put optionamerican =False# True for American option, False for European# Calculate option priceec_price = binomial_tree(S0, K, T, r, sigma, N, call, american)print(f"European Call Option Price: {ec_price:.4f}")# American Put Optioncall =False# True for Call option, False for Put optionamerican =True# True for American option, False for European# Calculate option priceap_price = binomial_tree(S0, K, T, r, sigma, N, call, american)print(f"American Put Option Price: {ap_price:.4f}")
European Call Option Price: 10.4505
American Put Option Price: 6.0903
Since Black-Scholes formula can be used for pricing European options, we can compare the European call option price with the Black-Scholes price.