Binomial trees in options pricing

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 Binomial trees in options pricing of the underlying security with a given probability of returns Binomial trees in options pricing should its value increase, and a probability of loss Binomial trees in options pricing 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.

Pricing European options

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 Pricing European options is 0.5 years. We would like to find the value of an European put option with a strike price Pricing European options of $52. The following figure shows the pricing of the stock using a binomial tree:

Pricing European options

Here, the nodes are calculated as follows:

Pricing European options
Pricing European options
Pricing European options
Pricing European options
Pricing European options
Pricing European options
Pricing European options
Pricing European options

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:

Pricing European options

In the case of an European put option, the payoff is as follows:

Pricing European options

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:

Pricing European options

The risk-neutral probability Pricing European options of investing in the stock can be rewritten as follows:

Pricing European options

Are these formulas relevant to stocks? What about futures?

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:

Are these formulas relevant to stocks? What about futures?

The risk-neutral probability Are these formulas relevant to stocks? What about futures? of investing in futures can be rewritten as follows:

Are these formulas relevant to stocks? What about futures?

The risk-neutral probability Are these formulas relevant to stocks? What about futures? of the stock given in the preceding example is calculated as 0.62817, and the payoff of the put option is given as follows:

Are these formulas relevant to stocks? What about futures?

Writing the StockOption class

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.

Writing the BinomialEuropeanOption class

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.

Tip

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.

Pricing American options with the BinomialTreeOption class

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.

The Cox-Ross-Rubinstein model

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 Cox-Ross-Rubinstein model
The Cox-Ross-Rubinstein model

Writing the BinomialCRROption class

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 Writing the BinomialCRROption class is 0.5 years. We would like to find the value of an European put option with a strike price Writing the BinomialCRROption class 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

Using a Leisen-Reimer tree

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 Using a Leisen-Reimer treewith the following characteristic parameters:

Using a Leisen-Reimer tree
Using a Leisen-Reimer tree
Using a Leisen-Reimer tree
Using a Leisen-Reimer tree
Using a Leisen-Reimer tree
Using a Leisen-Reimer tree
Using a Leisen-Reimer tree
Using a Leisen-Reimer tree

The parameter Using a Leisen-Reimer tree is the current stock price, Using a Leisen-Reimer treeis the strike price of the option, Using a Leisen-Reimer treeis the annualized volatility of the underlying stock, Using a Leisen-Reimer tree is the time to maturity of the option, Using a Leisen-Reimer tree is the annualized risk-free rate, Using a Leisen-Reimer tree is the dividend yield, and Using a Leisen-Reimer tree is the time interval between each tree step.

Writing the BinomialLROption class

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
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset