FCI-2 Binomial Tree for Option Pricing

FCI-2 Binomial Tree for Option Pricing

Vanilla European Option

Assumptions:
  • underlying asset pays no dividend
  • volatility of the underlying asset’s price movements is constant all the time
  • payoff of the call/put is made at expiration time and cannot be collected at any prior time
  • risk-free interest rate is constant
  • asset price evolution follows a geometric Brownian motion
for a European Call, the payoff on is
for a European Put, the payoff on is

Option Pricing Approaches

Based on above assumptions about the price movement of the underlying asset, there are different ways to compute the price (or ),
  • Closed-form formulas: Black-Scholes-Merton
  • Numerical integration of a differential equation
  • Statistical simulation
  • A Binomial (or Trinomial, or other) Tree

BSM Formula

BSM model assumptions:
  • No dividends are paid out during the life of the option.
  • Markets are random because market movements can't be predicted.
  • There are no transaction costs in buying the option.
  • The risk-free rate and volatility of the underlying asset are known and constant.
  • The returns of the underlying asset are normally distributed.
  • The option is European and can only be exercised at expiration.
Notations:
  • : call / put price of Stock at time
  • : Strike price of the call
  • : Continuously compounded risk-free annual rate
  • : Standard deviation of stock’s returns
  • : Time in years (0 is now, is expiration)
  • : Standard normal CDF
where

Binomial Tree

Basic Idea: Capture the assumptions about stock price volatility and risk neutral probabilities in tree form. Option pricing is done by backward induction on the tree nodes, from time back to time 0.
Algorithm Inputs:
  • : initial price (e.g. 50)
  • : strike price (e.g. 50)
  • : risk-free rate (e.g. 0.10)
  • : volatility (e.g. 0.40)
  • : term (e.g. 0.4167 i.e. 5 months)
  • : Number of intervals (simulation steps, e.g. 5)
Derived Values:
  • : Length in years of a time interval
  • : Up factor by which stock rises
  • : Down factor by which stock falls
  • : Risk free rate factor
  • : Risk neutral probability of up move
  • : Risk neutral probability of dowm move
  • : Stock price at node
Procedure
(1) Initialization
notion image
Actual Data Structure
Actual Data Structure
(2) Fill in the Stock Prices
notion image
(3) Fill in Terminal Option Values
notion image
(4) Backwards to Fill in Internal Option Values
notion image

Demo Codes

Tree Class
from numpy import log, exp, sqrt from scipy.stats import norm # for Normal CDF class EuropeanCallOption: def __init__(self, S0, K, r, sigma, T): self._S0 = S0 # initial stock price self._K = K # strike price self._r = r # risk-free rate self._sigma = sigma # volatility self._T = T # expiration time # Inner class used by the binomial tree model class _PriceNode: def __init__(self): self._stock_price = 0 self._option_price = 0 # EuropeanCallOption methods: def __str__(self): return ('EuropeanCallOption:\n' + ' S0: ' + str(self._S0) + ' K: ' + str(self._K) + ' r: ' + str(self._r) + ' sigma: ' + str(self._sigma) + ' T: ' + str(self._T)) def binomialPrice(self, num_intervals): # local variables used by this function deltaT = self._T / num_intervals u = exp(self._sigma * deltaT ** 0.5) d = 1 / u a = exp(self._r * deltaT) p = (a - d) / (u - d) q = 1 - p # abbreviated name ni = num_intervals # fill tree with all 0.0s binom_tree = [] for i in range(ni + 1): ith_row = [self._PriceNode() for j in range(i+1)] binom_tree.append(ith_row) if ni < 10: print('\nAfter filled in with all 0.0:') self.binomialTreePretty(binom_tree) # fill tree with stock prices for i in range(ni + 1): for j in range(i + 1): binom_tree[i][j]._stock_price = ( self._S0 * u ** j * d ** (i-j)) if ni < 10: print('\nAfter filled in with stock prices:') self.binomialTreePretty(binom_tree) # fill in terminal node option prices for j in range(ni + 1): binom_tree[ni][j]._option_price = ( max(binom_tree[ni][j]._stock_price - self._K, 0.0)) if ni < 10: print('\nAfter filled in with terminal option values:') self.binomialTreePretty(binom_tree) # Now work backwards, filling in the # option prices in the rest of the tree for i in range(ni-1, -1, -1): for j in range(i+1): binom_tree[i][j]._option_price = exp(-self._r*deltaT) * \ (p * binom_tree[i+1][j+1]._option_price + q * binom_tree[i+1][j]._option_price) if ni < 10: print('\nAfter filled in with all option values:') self.binomialTreePretty(binom_tree) return binom_tree[0][0]._option_price def binomialTreePretty(self, binom_tree): nlevels = len(binom_tree) if nlevels < 10: print('\nBinomialTree with', nlevels - 1, 'time steps:') for row in range(nlevels): print('\nStock: ', end='') ncols = len(binom_tree[row]) for col in range(ncols): print('{:8.4f}'.format(binom_tree[row][col]._stock_price), end='') print('') print('Option: ', end='') for col in range(ncols): print('{:8.4f}'.format(binom_tree[row][col]._option_price), end='') print('') print('')
Main Script
if __name__ == '__main__': ec = EuropeanCallOption(50.0, 50.0, 0.1, 0.4, 0.4167) print(ec) print('Binomial Tree Euro call price, 5 time intervals: ' + '${:.4f}'.format(ec.binomialPrice(5))) for steps in [10, 20, 50, 100, 200, 500, 1000]: print(('Binomial Tree Euro call price, {:d} time intervals: ' + '${:.4f}').format(steps, ec.binomialPrice(steps))) """ Comment: As step increases, corresponding option values are $5.9912, $6.0535, $6.0914, $6.1041, $6.1104, $6.1142, $6.1155, which gradually converge to BSM's value """ for S0 in [5, 50, 500]: # strike price equals stock price ecS0 = EuropeanCallOption(S0, S0, 0.1, 0.4, 0.4167) print('With S0, K ==', S0, ecS0) print(('Binomial Tree Euro call price, 1000 time intervals: ' + '${:.4f}').format(ecS0.binomialPrice(1000))) """ Comment: The call option with K=$50 is about 10 times of the call option K=$5, and about 1/10 of the price of call option with K=500 """

Loading Comments...