In the binomial options pricing model, the underlying security at one time period, represented as a node with a given price, is assumed to traverse to two other nodes in the next time step, representing an up state and a down state. Since options are derivatives of the underlying asset, the binomial pricing model tracks the underlying conditions on a discrete-time basis. Binomial option pricing can be used to value European options, American options, as well as Bermudan options.
The initial value of the root node is the spot price of the underlying security with a given probability of returns should its value increase, and a probability of loss should its value decrease. Based on these probabilities, the expected values of the security are calculated for each state of price increase or decrease for every time step. The terminal nodes represent every value of the expected security prices for every combination of up states and down states. We can then calculate the value of the option at every node, traverse the tree by risk-neutral expectations, and after discounting from the forward interest rates, we can derive the value of the call or put option.
Consider a two-step binomial tree. A non-dividend paying stock price starts at $50, and in each of the two time steps, the stock may go up by 20 percent or go down by 20 percent. We suppose that the risk-free rate is 5 percent per annum and the time to maturity is 0.5 years. We would like to find the value of an European put option with a strike price of $52. The following figure shows the pricing of the stock using a binomial tree:
Here, the nodes are calculated as follows:
At the ultimate nodes, which hold the values of the underlying stock at maturity, the payoff from exercising an European call option is given as follows:
In the case of an European put option, the payoff is as follows:
From the option payoff values, we can then traverse the binomial tree backward to the current time and, after discounting from the risk-free rate, we will obtain our present value of the option. Traversing the tree backward takes into account the risk-neutral probabilities of the option's up states and down states.
We may assume that investors are indifferent to risk and that expected returns on all assets are equal. In the case of investing in stocks, by risk-neutral probability, the payoff from holding the stock, taking into account the up and down state possibilities, would be equal to the continuously compounded risk-free rate expected in the next time step, as follows:
The risk-neutral probability of investing in the stock can be rewritten as follows:
Unlike investing in stocks, investors do not have to make an upfront payment to take a position in a futures contract. In a risk-neutral sense, the expected growth rate from holding a futures contract is zero and the payoff can be written as follows:
The risk-neutral probability of investing in futures can be rewritten as follows:
The risk-neutral probability of the stock given in the preceding example is calculated as 0.62817, and the payoff of the put option is given as follows:
Before going further in implementing the various pricing models that we are about to discuss, let's create a StockOption
class to store and calculate the common attributes of the stock option that will be reused throughout this chapter. You can save the following code to a file named StockOption.py
:
""" Store common attributes of a stock option """ import math class StockOption(object): def __init__(self, S0, K, r, T, N, params): self.S0 = S0 self.K = K self.r = r self.T = T self.N = max(1, N) # Ensure N have at least 1 time step self.STs = None # Declare the stock prices tree """ Optional parameters used by derived classes """ self.pu = params.get("pu", 0) # Probability of up state self.pd = params.get("pd", 0) # Probability of down state self.div = params.get("div", 0) # Dividend yield self.sigma = params.get("sigma", 0) # Volatility self.is_call = params.get("is_call", True) # Call or put self.is_european = params.get("is_eu", True) # Eu or Am """ Computed values """ self.dt = T/float(N) # Single time step, in years self.df = math.exp( -(r-self.div) * self.dt) # Discount factor
The current underlying price, strike price, risk-free rate, time to maturity, and number of time steps are compulsory common attributes for pricing options. The params
variable is a dictionary object that accepts the required additional information pertaining to the model being used. From all of this information, the delta of the time step dt
and the discount factor df
are computed and can be reused throughout the pricing implementation.
The Python implementation of the binomial option pricing model of an European option is given as the BinomialEuropeanOption
class, which inherits the common attributes of the option from the StockOption
class.
The price
method of the BinomialEuropeanOption
class is a public method that is the entry point for all the instances of this class. It calls the _setup_parameters_
method to set up the required model parameters, and then calls the _initialize_stock_price_tree_
method to simulate the expected values of the stock prices for the period up till T.
Finally, the __begin_tree_traversal__
private method is called to initialize the payoff array and store the discounted payoff values, as it traverses the binomial tree back to the present time. The payoff tree nodes are returned as a NumPy array object, where the present value of the European option is found at the initial node.
Method names starting with double underlines "__" are private methods and can only be accessed within the same class. Method names starting with a single underline "_" are a protected method and may be overwritten by child classes. Method names not starting with an underline are public functions and may be accessed from any object.
Save this code to a file named BinomialEuropeanOption.py
:
""" Price a European option by the binomial tree model """ from StockOption import StockOption import math import numpy as np class BinomialEuropeanOption(StockOption): def __setup_parameters__(self): """ Required calculations for the model """ self.M = self.N + 1 # Number of terminal nodes of tree self.u = 1 + self.pu # Expected value in the up state self.d = 1 - self.pd # Expected value in the down state self.qu = (math.exp((self.r-self.div)*self.dt) - self.d) / (self.u-self.d) self.qd = 1-self.qu def _initialize_stock_price_tree_(self): # Initialize terminal price nodes to zeros self.STs = np.zeros(self.M) # Calculate expected stock prices for each node for i in range(self.M): self.STs[i] = self.S0*(self.u**(self.N-i))*(self.d**i) def _initialize_payoffs_tree_(self): # Get payoffs when the option expires at terminal nodes payoffs = np.maximum( 0, (self.STs-self.K) if self.is_call else(self.K-self.STs)) return payoffs def _traverse_tree_(self, payoffs): # Starting from the time the option expires, traverse # backwards and calculate discounted payoffs at each node for i in range(self.N): payoffs = (payoffs[:-1] * self.qu + payoffs[1:] * self.qd) * self.df return payoffs def __begin_tree_traversal__(self): payoffs = self._initialize_payoffs_tree_() return self._traverse_tree_(payoffs) def price(self): """ The pricing implementation """ self.__setup_parameters__() self._initialize_stock_price_tree_() payoffs = self.__begin_tree_traversal__() return payoffs[0] # Option value converges to first node
Let's take the values from the two-step binomial tree example discussed earlier to price the European put option:
>>> from StockOption import StockOption >>> from BinomialEuropeanOption import BinomialEuropeanOption >>> eu_option = BinomialEuropeanOption ... 50, 50, 0.05, 0.5, 2, ... {"pu": 0.2, "pd": 0.2, "is_call": False}) >>> print option.price() 4.82565175126
Using the binomial option pricing model gives us a present value of $4.826 for the European put option.
Unlike European options that can only be exercised at maturity, American options can be exercised at any time during their lifetime.
To implement the pricing of American options in Python, we do the same with the BinomialEuropeanOption
class and create a class named BinomialTreeOption
. The parameters used in the _setup_parameters_
method remain the same with the removal of an unused M
parameter. The various methods used in American options are as follows:
_initialize_stock_price_tree_
: This method uses a two-dimensional NumPy array to store the expected returns of the stock prices for all time steps. This information is used to calculate the payoff values from exercising the option at each period._initialize_payoffs_tree_
: This method creates the payoff tree as a two-dimensional NumPy array, starting with the intrinsic values of the option at maturity.__check_early_exercise__
: This method is a private method that returns the maximum payoff values between exercising the American option early and not exercising the option at all._traverse_tree_
: This method now includes the invocation of the __check_early_exercise__
method to check whether it is optimal to exercise an American option early at every time step.Implementation of the __begin_tree_traversal__
and the price
methods remains the same.
The BinomialTreeOption
class can now price both European and American options when the is_eu
key of the params
dictionary object is set to true
or false
respectively, when creating an instance of the class. Save the file as BinomialAmericanOption.py
with the following code:
""" Price a European or American option by the binomial tree """ from StockOption import StockOption import math import numpy as np class BinomialTreeOption(StockOption): def _setup_parameters_(self): self.u = 1 + self.pu # Expected value in the up state self.d = 1 - self.pd # Expected value in the down state self.qu = (math.exp((self.r-self.div)*self.dt) - self.d)/(self.u-self.d) self.qd = 1-self.qu def _initialize_stock_price_tree_(self): # Initialize a 2D tree at T=0 self.STs = [np.array([self.S0])] # Simulate the possible stock prices path for i in range(self.N): prev_branches = self.STs[-1] st = np.concatenate((prev_branches*self.u, [prev_branches[-1]*self.d])) self.STs.append(st) # Add nodes at each time step def _initialize_payoffs_tree_(self): # The payoffs when option expires return np.maximum( 0, (self.STs[self.N]-self.K) if self.is_call else (self.K-self.STs[self.N])) def __check_early_exercise__(self, payoffs, node): early_ex_payoff = (self.STs[node] - self.K) if self.is_call else (self.K - self.STs[node]) return np.maximum(payoffs, early_ex_payoff) def _traverse_tree_(self, payoffs): for i in reversed(range(self.N)): # The payoffs from NOT exercising the option payoffs = (payoffs[:-1] * self.qu + payoffs[1:] * self.qd) * self.df # Payoffs from exercising, for American options if not self.is_european: payoffs = self.__check_early_exercise__(payoffs, i) return payoffs def __begin_tree_traversal__(self): payoffs = self._initialize_payoffs_tree_() return self._traverse_tree_(payoffs) def price(self): self._setup_parameters_() self._initialize_stock_price_tree_() payoffs = self.__begin_tree_traversal__() return payoffs[0]
Taking the same variables in the European put option pricing example, we can create an instance of the BinomialTreeOption
class and price this American option:
>>> from BinomialAmericanOption import BinomialTreeOption >>> am_option = BinomialTreeOption( ... 50, 50, 0.05, 0.5, 2, ... {"pu": 0.2, "pd": 0.2, "is_call": False, "is_eu": False}) >>> print am_option.price() 5.11306008282
The American put option is priced at $5.113. Since American options can be exercised at any time and European options can only be exercised at maturity, this added flexibility of American options increases their value over European options in certain circumstances.
For American call options on an underlying asset that does not pay dividends, there might not be an extra value over its European call option counterpart. Because of the time value of money, it costs more to exercise the American call option today before the expiration at the strike price than at a future time with the same strike price. For an in-the-money American call option, exercising the option early loses the benefit of protection against adverse price movement below the strike price as well as its intrinsic time value. With no entitlement of dividend payments, there are no incentives to exercise American call options early.
In the preceding examples, we assumed that the underlying stock price would increase by 20 percent and decrease by 20 percent in its respective up state u
and down state d
. The Cox-Ross-Rubinstein (CRR) model proposes that, over a short period of time in the risk-neutral world, the binomial model matches the mean and variance of the underlying stock. The volatility of the underlying stock, or the standard deviation of returns of the stock, is taken into account as follows:
The implementation of the binomial CRR model remains the same as the binomial tree discussed earlier with the exception of the model parameters u
and d
.
In Python, we will create a class named BinomialCRROption
and simply inherit the BinomialTreeOption
class. Then, all that we need to do is to override the _setup_parameters_
method with values from the CRR model.
Instances of the BinomialCRROption
object will invoke the price method, which will call all other methods, except the overwritten _setup_parameters_
method, of the parent BinomialTreeOption
class.
Save the following code to a file named BinomialCRROption.py
:
""" Price an option by the binomial CRR model """ from BinomialTreeOption import BinomialTreeOption import math class BinomialCRROption(BinomialTreeOption): def _setup_parameters_(self): self.u = math.exp(self.sigma * math.sqrt(self.dt)) self.d = 1./self.u self.qu = (math.exp((self.r-self.div)*self.dt) - self.d)/(self.u-self.d) self.qd = 1-self.qu
Consider again the two-step binomial tree. The non-dividend paying stock has a current price of $50 and a volatility of 30 percent. Suppose that the risk-free rate is 5 percent per annum and the time to maturity is 0.5 years. We would like to find the value of an European put option with a strike price of $50 by the CRR model:
>>> from BinomialCRROption import BinomialTreeOption >>> eu_option = BinomialCRROption( ... 50, 50, 0.05, 0.5, 2, ... {"sigma": 0.3, "is_call": False}) >>> print "European put: %s" % eu_option.price() European put: 3.1051473413 >>> am_option = BinomialCRROption( ... 50, 50, 0.05, 0.5, 2, ... {"sigma": 0.3, "is_call": False, "is_eu": False}) >>> print "American put: %s" % am_option.price() American put: 3.4091814964
In the binomial models discussed earlier, we made several assumptions on the probability of up and down states as well as the resulting risk-neutral probabilities. Besides the binomial model with CRR parameters discussed, other forms of parameterization discussed widely in mathematical finance include the Jarrow-Rudd parameterization, Tian parameterization, and Leisen-Reimer parameterization. Let's take a look at the Leisen-Reimer model in detail.
Dr. Dietmar Leisen and Matthias Reimer proposed a binomial tree model with the purpose of approximating to the Black-Scholes solution as the number of steps increases. It is known as the Leisen-Reimer (LR) tree, and the nodes do not recombine at every alternate step. It uses an inversion formula to achieve better accuracy during tree transversal.
A detailed explanation of the formulas is given in the paper Binomial Models For Option Valuation - Examining And Improving Convergence, March 1995, which is available at http://papers.ssrn.com/sol3/papers.cfm?abstract_id=5976.
We will be using method two of the Peizer and Pratt Inversion function with the following characteristic parameters:
The parameter is the current stock price, is the strike price of the option, is the annualized volatility of the underlying stock, is the time to maturity of the option, is the annualized risk-free rate, is the dividend yield, and is the time interval between each tree step.
The Python implementation of the Leisen-Reimer tree is given in the following BinomialLROption
class. Similar to the BinomialCRROption
class, we can inherit the BinomialTreeOption
class and override the variables in the _setup_parameters_
method with those of the LR tree model:
""" Price an option by the Leisen-Reimer tree """ from BinomialTreeOption import BinomialTreeOption import math class BinomialLROption(BinomialTreeOption): def _setup_parameters_(self): odd_N = self.N if (self.N%2 == 1) else (self.N+1) d1 = (math.log(self.S0/self.K) + ((self.r-self.div) + (self.sigma**2)/2.) * self.T) / (self.sigma * math.sqrt(self.T)) d2 = (math.log(self.S0/self.K) + ((self.r-self.div) - (self.sigma**2)/2.) * self.T) / (self.sigma * math.sqrt(self.T)) pp_2_inversion = lambda z, n: .5 + math.copysign(1, z) * math.sqrt(.25 - .25 * math.exp( -((z/(n+1./3.+.1/(n+1)))**2.)*(n+1./6.))) pbar = pp_2_inversion(d1, odd_N) self.p = pp_2_inversion(d2, odd_N) self.u = 1/self.df * pbar/self.p self.d = (1/self.df - self.p*self.u)/(1-self.p) self.qu = self.p self.qd = 1-self.p
Using the same examples as before, we can price the options using an LR tree:
>>> from BinomialLROption import BinomialLROption >>> eu_option = BinomialLROption( ... 50, 50, 0.05, 0.5, 3, ... {"sigma": 0.3, "is_call": False}) >>> print "European put: %s" % eu_option.price() European put: 3.56742999918 >>> am_option = BinomialLROption( ... 50, 50, 0.05, 0.5, 3, ... {"sigma": 0.3, "is_call": False, "is_eu": False}) >>> print "American put: %s" % am_option.price() American put: 3.66817910413