Binomial Pricing Model

(Go to Quant Lab)

Source: Numerical Methods in Finance with C++

© Maciej J. Capinski, Tomasz Zastawniak

Initially import all the modules we will be using for our notebook

In [2]:
import math
import numpy as np
import numpy.random as npr  
# from pylab import plt, mpl
import sys
import os

1.1 Creating Main Program

In [2]:
if __name__== "__main__":
    print("Hi there")
    input("Provide a character: ")
Hi there
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
~/anaconda3/lib/python3.6/site-packages/ipykernel/kernelbase.py in _input_request(self, prompt, ident, parent, password)
    884             try:
--> 885                 ident, reply = self.session.recv(self.stdin_socket, 0)
    886             except Exception:

~/anaconda3/lib/python3.6/site-packages/jupyter_client/session.py in recv(self, socket, mode, content, copy)
    802         try:
--> 803             msg_list = socket.recv_multipart(mode, copy=copy)
    804         except zmq.ZMQError as e:

~/anaconda3/lib/python3.6/site-packages/zmq/sugar/socket.py in recv_multipart(self, flags, copy, track)
    469         """
--> 470         parts = [self.recv(flags, copy=copy, track=track)]
    471         # have first part already, only loop while more to receive

zmq/backend/cython/socket.pyx in zmq.backend.cython.socket.Socket.recv()

zmq/backend/cython/socket.pyx in zmq.backend.cython.socket.Socket.recv()

zmq/backend/cython/socket.pyx in zmq.backend.cython.socket._recv_copy()

~/anaconda3/lib/python3.6/site-packages/zmq/backend/cython/checkrc.pxd in zmq.backend.cython.checkrc._check_rc()

KeyboardInterrupt: 

During handling of the above exception, another exception occurred:

KeyboardInterrupt                         Traceback (most recent call last)
<ipython-input-2-7d983df3fa32> in <module>
      1 if __name__== "__main__":
      2     print("Hi there")
----> 3     input("Provide a character: ")

~/anaconda3/lib/python3.6/site-packages/ipykernel/kernelbase.py in raw_input(self, prompt)
    858             self._parent_ident,
    859             self._parent_header,
--> 860             password=False,
    861         )
    862 

~/anaconda3/lib/python3.6/site-packages/ipykernel/kernelbase.py in _input_request(self, prompt, ident, parent, password)
    888             except KeyboardInterrupt:
    889                 # re-raise KeyboardInterrupt, to truncate traceback
--> 890                 raise KeyboardInterrupt
    891             else:
    892                 break

KeyboardInterrupt: 
In [3]:
# Directory where we will save our plots
directory = "./images"
if not os.path.exists(directory):
    os.makedirs(directory)

1.2 Entering Data

(Back to Top)

In [4]:
if __name__== "__main__":
    S0 = float(input("Enter S_0: "))
    U = float(input("Enter U: "))
    D = float(input("Enter D: "))
    R = float(input("Enter R: "))

    # making sure that 0<S0, -1<D<U, -1<R
    if (S0 <= 0.0 or U <= -1.0 or D <= -1.0 or U <= D or R <= -1.0):
        print("Illegal data ranges")
        print("Terminating program")
        sys.exit()
    
    # checking for arbitrage
    if (R >= U or R <= D):
        print("Arbitrage exists")
        print("Terminating program")
        sys.exit()

    print("Input data checked")
    print("There is no arbitrage\n")
    
    # compute risk-neutral probability
    print("q = ", (R - D) / (U - D))
    
    # compute stock price at node n=3,i=2
    n = 3; i = 2
    print("n = ", n)
    print("i = ", i)
    print("S(n,i) = ", S0* math.pow(1 + U,i) * math.pow(1 + D, n - i))
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
~/anaconda3/lib/python3.6/site-packages/ipykernel/kernelbase.py in _input_request(self, prompt, ident, parent, password)
    884             try:
--> 885                 ident, reply = self.session.recv(self.stdin_socket, 0)
    886             except Exception:

~/anaconda3/lib/python3.6/site-packages/jupyter_client/session.py in recv(self, socket, mode, content, copy)
    802         try:
--> 803             msg_list = socket.recv_multipart(mode, copy=copy)
    804         except zmq.ZMQError as e:

~/anaconda3/lib/python3.6/site-packages/zmq/sugar/socket.py in recv_multipart(self, flags, copy, track)
    469         """
--> 470         parts = [self.recv(flags, copy=copy, track=track)]
    471         # have first part already, only loop while more to receive

zmq/backend/cython/socket.pyx in zmq.backend.cython.socket.Socket.recv()

zmq/backend/cython/socket.pyx in zmq.backend.cython.socket.Socket.recv()

zmq/backend/cython/socket.pyx in zmq.backend.cython.socket._recv_copy()

~/anaconda3/lib/python3.6/site-packages/zmq/backend/cython/checkrc.pxd in zmq.backend.cython.checkrc._check_rc()

KeyboardInterrupt: 

During handling of the above exception, another exception occurred:

KeyboardInterrupt                         Traceback (most recent call last)
<ipython-input-4-a1a06ba97b6b> in <module>
      1 if __name__== "__main__":
----> 2     S0 = float(input("Enter S_0: "))
      3     U = float(input("Enter U: "))
      4     D = float(input("Enter D: "))
      5     R = float(input("Enter R: "))

~/anaconda3/lib/python3.6/site-packages/ipykernel/kernelbase.py in raw_input(self, prompt)
    858             self._parent_ident,
    859             self._parent_header,
--> 860             password=False,
    861         )
    862 

~/anaconda3/lib/python3.6/site-packages/ipykernel/kernelbase.py in _input_request(self, prompt, ident, parent, password)
    888             except KeyboardInterrupt:
    889                 # re-raise KeyboardInterrupt, to truncate traceback
--> 890                 raise KeyboardInterrupt
    891             else:
    892                 break

KeyboardInterrupt: 

1.3 Functions

(Back to Top)

In [5]:
# computing risk-neutral probability
def RiskNeutProb(U, D, R):
    return (R - D) / (U - D)

# computing the stock price at node n,i
def S(S0, U, D, n, i):
    return S0 * math.pow(1 + U,i) * math.pow(1 + D, n - i)

def isValidInput(S0, U, D, R):
    # making sure that 0<S0, -1<D<U, -1<R
    if (S0 <= 0.0 or U <= -1.0 or D <= -1.0 or U <= D or R <= -1.0):
        print("Illegal data ranges")
        print("Terminating program")
        return 0
    
    # checking for arbitrage
    if (R >= U or R <= D):
        print("Arbitrage exists")
        print("Terminating program")
        return 0
    
    return 1

# inputting, displaying and checking model data
def GetInputData():
    #entering data
    params = ("S0", "U", "D", "R")
    S0, U, D, R = [float(input("Enter %s: " % (var))) for var in params]
    
    if not isValidInput(S0, U, D, R):
        return 0
    
    print("Input data checked")
    print("There is no arbitrage\n")
    d = locals().copy()
    return d


if __name__== "__main__":
    # compute risk-neutral probability
    print("q = ", RiskNeutProb(U, D, R))
    
    output = GetInputData()
    if output == 0:
        sys.exit()
    
    # Update our parameters
    locals().update(output)
    
    # compute stock price at node n=3,i=2
    n = 3; i = 2
    print("n = ", n)
    print("i = ", i)
    print("S(n,i) = ", S(S0,U,D,n,i))
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-5-74dfc56e9700> in <module>
     39 if __name__== "__main__":
     40     # compute risk-neutral probability
---> 41     print("q = ", RiskNeutProb(U, D, R))
     42 
     43     output = GetInputData()

NameError: name 'U' is not defined

2.0 Object Oriented European and American

(Back to Top)

In [4]:
# Binomial Model (Parent class) ---------------------------------------------------
class BinModel:
    def __init__(self, S0=100, U=0.1, D=-0.1, R=0):
        self.S0 = S0
        self.U = U
        self.D = D
        self.R = R
    
    def RiskNeutProb(self):
        return (self.R - self.D) / (self.U - self.D)
    
    def S(self, n, i):
        return self.S0 * math.pow(1 + self.U, i) * math.pow(1 + self.D, n - i)
        
    
# European Option class ------------------------------------------------------------------
class EurOption():
    
    def PriceByCRR(self, Model, N=3):
        q = Model.RiskNeutProb();
        Price = np.zeros(N + 1)
        
        for i in range(0, N + 1):
            Price[i] = self.Payoff(Model.S(N, i))
        
        for n in range(N - 1, -1, -1):
            for i in range(0, n + 1):
                Price[i] = (q * Price[i+1] + (1 - q) * Price[i]) / (1 + Model.R);

        return Price[0]
    
# American Option class
class AmOption():
    def PriceBySnell(self, Model, N=3):
        q=Model.RiskNeutProb()
        Price = np.zeros(N + 1)
        
        for i in range(0, N + 1):
            Price[i] = self.Payoff(Model.S(N, i))

        for n in range(N - 1, -1, -1):
            for i in range(0, n + 1):
                ContVal = (q * Price[i+1] + (1 - q) * Price[i]) / (1 + Model.R)
                Price[i] = self.Payoff(Model.S(n,i))
                if (ContVal > Price[i]):
                    Price[i] = ContVal
        
        return Price[0]
    
class AsianOption()
    def PriceAsian(self, Model, N=3):
        q = Model.RiskNeutProb();
        Price = np.zeros(N + 1)
        
        for i in range(0, 2 * N + 1):
            
            Price[i] = self.Payoff(Model.S(N, i))
        
        for n in range(N - 1, -1, -1):
            for i in range(0, n + 1):
                Price[i] = (q * Price[i+1] + (1 - q) * Price[i]) / (1 + Model.R);

        return Price[0]

# Payoff Classes ---------------------------------------------------------------------------- 
class Call(EurOption, AmOption):
    
    def __init__(self, K=1):
        self.K = K
    
    def Payoff(self, z):
        if (z > self.K): 
            return z - self.K
        
        return 0

class Put(EurOption, AmOption):
    
    def __init__(self, K=1):
        self.K = K
        
    def Payoff(self, z):
        if (z < self.K): 
            return self.K - z
        
        return 0

class Digital_Call(EurOption, AmOption):
    def __init__(self, K=1):
        self.K = K
        
    def Payoff(self, z):
        if self.K < z:
            return 1
        return 0
    
class BullSpread(EurOption, AmOption):
    def __init__(self, K1=1, K2 = 2):
        self.K1 = K1
        self.K2 = K2
    
    def Payoff
    

model = BinModel(S0=10, U=0.2, D=-0.1, R=0.1)
Option1 = Digital_Call(K=22)
Option1.PriceByCRR(model, N=6)
Out[4]:
0.19822404128088741

Exercises:

  1. Modify the PriceByCRR() function in EurOption Class to compute the time $0$ price of a European option using the Cox–Ross–Rubinstein (CRR) formula: $$H(0)=\frac{1}{(1+R)^{N}}\sum_{i=0}^{N}\frac{N!}{i!(N-i)!}q^{i}(1-q)^{N-i}h(S(N,I))$$
  1. The payoff of a digital call with strike price $K$ is: $$h^{digit\thinspace call}(z)=\begin{cases} 1 & \textrm{{if} }K<z,\\ 0 & \textrm{otherwise.} \end{cases}$$ Include the ability to price digital calls in the program developed in the present section by adding the new payoff Class DigitCall just as was done for calls and puts.
  1. Add the ability to price bull spreads and bear spreads by introducing new subclasses BullSpread and BearSpread of the EurOption and AmOption classes defined in our current program. The payoffs of a bull spread and a bear spread, which depend on two parameters $K1 < K2$, are given by: $$h^{bull}(z)=\begin{cases} 0 & \textrm{if }z\leq K_{1},\\ z-K_{1} & \textrm{if }K_{1}<z<K_{2}\\ K_{2}-K_{1} & \textrm{if }K_{2}\leq z, \end{cases}$$ and $$h^{bear}(z)=\begin{cases} K_{2}-K_{1} & \textrm{if }z\leq K_{1},\\ K_{2}-z & \textrm{if }K_{1}<z<K_{2}\\ 0 & \textrm{if }K_{2}<z, \end{cases}$$
  1. Consider the fixed-strike Asian call option with process, $$A_{N}=\frac{1}{N}\sum_{k=0}^{N-1}S(k)$$ and payoff, $$C(N)=max\{A_{N},0\}$$ Add a new Class AsianOption to price an Asian style Call Option.
In [ ]: