赞
踩
A derivative[dɪˈrɪvətɪv](金融)衍生工具(产品) is a contract whose payoff depends on the value of some underlying asset. In cases where closed-form derivative pricing may be complex or even impossible, numerical procedures excel. A numerical procedure is the use of iterative computational methods in attempting to converge to a solution. One such basic implementation is a binomial tree. In a binomial tree, a node represents the state of an asset at a certain point of time associated with a price. Each node leads to two other nodes in the next time step. Similarly, in a trinomial tree, each node leads to three other nodes in the next time step. However, as the number of nodes or the time steps of trees increase, so do the computational resources that are consumed. Lattice pricing attempts to solve this problem by storing only the new information at each time step, while reusing values where possible.
In finite difference pricing限差分定价, the nodes of the tree can also be represented as a grid. The terminal values on the grid consist of terminal conditions, while the edges of the grid represent boundary conditions in asset pricing. We will discuss the explicit method, implicit method, and the Crank-Nicolson method of the finite difference schemes to determine the price of an asset.
Although vanilla[vəˈnɪlə] options普通期权 and certain exotics某些特殊产品 such as European barrier options欧式障碍期权 and lookback options回溯期权 can be found to have a closed-form solution, other exotic products such as Asian options do not contain a closed-form solution. In these cases, the pricing of options can be used with numerical procedures.
In this chapter, we will cover the following topics:
An option is a derivative of an asset that gives an owner the right but not the obligation to transact the underlying asset at a certain date for a certain price, known as the maturity date and strike price, respectively.
A call option看涨期权 gives the buyer the right to buy an asset by a certain date for a certain price. A seller or writer of a call option is obligated to sell the underlying security to the buyer at the agreed price, should the buyer exercise his/her rights on the agreed date.
A put option看跌期权 gives the buyer the right to sell the underlying asset by a certain date for a certain price. A seller or writer of a put option is obligated to buy the underlying security from the buyer at the agreed price, should the buyer exercise his/her rights on the agreed date.
The most common options that are available are the European options and American options. Other exotic options include Bermudan options百慕大期权 and Asian options. This chapter will deal mainly with European and American options. A European option can only be exercised on the maturity date. An American option, on the other hand, may be exercised at any time throughout the lifetime of the option.
In the binomial option 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 risk-neutral probability of increase q风险中性增加概率为 q, and a risk-neutral probability of loss 1-q风险中性损失概率为 1-q, at the next time step. 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. Suppose that the risk-free rate is five percent per annum[ˈænəm]年 and that the time to maturity, T, is two years. We would like to find the value of a European put option with a strike price K of $52. The following diagram shows the pricing of the stock and the payoffs收益 at the terminal nodes using a binomial tree:
Here, the nodes are calculated as follows:
At the terminal nodes, the payoff from exercising a European call option is given as follows:
In the case of a European put option, the payoff is as follows:
European call and put options are usually denoted by lowercase letters, c and p, while American call and put options are usually denoted by uppercase letters, C and P.
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[ɪnˈdɪfrənt]不关心的 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 and 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:OR
which ensures that over the small period of time Δt the expected return of the binomial model matches the expected return in a risk-neutral world, and the equation,
which ensures that the variance matches.
The risk-neutral probability q of investing in the stock can be rewritten as follows:
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 risk-neutral probability q of investing in futures can be rewritten as follows:
Let's calculate the risk-neutral probability q of the stock given in the preceding example:
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. Suppose that the risk-free rate is 5% per annum[ˈænəm]年 and that the time to maturity, T, is two years. We would like to find the value of a European put option with a strike price K of $52.
The risk-neutral probability q of investing in the stock:OR
- import math
-
- r=0.05 # risk-free rate
- T = 2 # the time to maturity, T, is two years
- t = T/2 # the next time step
- u = 1.2 # go up by 20 percent
- d = 0.8 # go down by 20 percent
-
- q = ( math.exp(r*t) - d )/(u-d)
- print('the risk-neutral probability q:',q)
if S (such as ) is the current price, then in the next period the price will either be
or
.
- Su = S0 * u
- Sd = S0 * d
- Suu = Su * u
- Sud = Su * d # or Sd*u
- Sdd = Sd * d
-
- print( "Su:", Su )
- print( "Sd:", Sd )
- print( "Suu:", Suu )
- print( "Sud:", Sud )
- print( "Sdd:", Sdd )
- puu = max(0, K-Suu)
- pud = max(0, K-Sud)
- pdd = max(0, K-Sdd)
-
- pu = math.exp(-r*t)*( puu*q + pud*(1-q) )
- pd = math.exp(-r*t)*( pud*q + pdd*(1-q) )
-
- p0=math.exp(-r*t)*( pu*q + pd*(1-q) )
-
- print( "puu:", puu)
- print( "pud:", pud)
- print( "pdd:", pdd)
- print( "pu: ", pu )
- print( "pd: ", pd )
- print( "p0: ", p0 )
The payoffs of exercising the European put option at the terminal nodes are $0, $4, and $20 at the respective states. The present value of the put option can be priced as follows:<==strike price K : $52
- pu=math.exp(-r*t)*( 0*q + 4*(1-q) )
- pu
vs
- pd=math.exp(-r*t)*( 4*q + 20*(1-q) )
- pd
- p0=math.exp(-r*t)*( pu*q + pd*(1-q) )
- p0
- pt=math.exp(-r*T)*( 0*q**2 + 4*q*(1-q)*2 + 20*(1-q)**2 )
- pt
<==
######################## Call option
A stock price is currently $50. Over each of the next two three-month periods it is expected to go up by 6% or down by 5%. The risk-free interest rate is 5% per annum with continuous compounding. What is the value of a six-month European call option with a strike price of $51?Figure S12.1
A tree describing the behavior of the stock price is shown in Figure S12.1. The risk-neutral probability of an up move, p, is given by
- S0 = 50 # A stock price is currently $50
- K = 51 # a strike price of $51
- r=0.05 # risk-free rate
- T = 6/12 # the time to maturity, T, in years
- n_t = 2 # the number of time steps
- t = T/n_t
- u = 1.06 # it is expected to go up by 6%
- d = 0.95 # it is expected to go down by 5%
-
- q = ( math.exp(r*t) - d )/(u-d)
- print('the risk-neutral probability q:',q)
if S (such as ) is the current price, then in the next period the price will either be
or
.
- Su = S0 * u
- Sd = S0 * d
- Suu = Su * u
- Sud = Su * d # or Sd*u
- Sdd = Sd * d
-
- print( "Su:", Su )
- print( "Sd:", Sd )
- print( "Suu:", Suu )
- print( "Sud:", Sud )
- print( "Sdd:", Sdd )
- puu = max(0, Suu-K)
- pud = max(0, Sud-K)
- pdd = max(0, Sdd-K)
-
- pu = math.exp(-r*t)*( puu*q + pud*(1-q) )
- pd = math.exp(-r*t)*( pud*q + pdd*(1-q) )
-
- p0=math.exp(-r*t)*( pu*q + pd*(1-q) )
-
- print( "puu:", puu)
- print( "pud:", pud)
- print( "pdd:", pdd)
- print( "pu: ", pu )
- print( "pd: ", pd )
- print( "p0: ", p0 )
There is a payoff from the call option of 56.18-51=5.18 for the highest final node (which corresponds to two up moves) zero in all other cases. The value of the option is therefore
- pt=math.exp(-r*T)*( puu*q**2 + pud*q*(1-q)*2 + pdd*(1-q)**2 )
- pt
########################
Before going any further and 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:
- import math
- # Stores common attributes of stock option
- class StockOption(object):
- def __init__(
- self, S0, K, r =0.05, T=1, N=2, pu=0, pd=0,
- div=0, sigma=0, is_put = False, is_am=False
- ):
- """
- Initialize the stock option base class.
- Defaults to European call unless specified.
- : param S0: initial stock price
- : param K: strike price
- : param r: risk-free interest rate
- : param T: time to maturity
- : param N: number of time steps
- : param pu: probability at up state
- : param pd: probability at down state
- : param div: Dividend yield
- : param is_put: True for a put option,
- False for a call option
- : param is_am: True for an American option,
- False for a European option
- """
- self.S0=S0
- self.K=K
- self.r=r
- self.T=T
- self.N = max(1,N)
- self.STs = [] # Declare the stock prices tree
-
- # Optional parameters used by derived classes
- self.pu, self.pd = pu, pd
- self.div = div
- self.sigma = sigma
- self.is_call = not is_put
- self.is_european = not is_am
- # @property:https://www.programiz.com/python-programming/property
- @property
- def dt(self):
- # Single time step, in years
- return self.T/float(self.N)
-
- @property
- def df(self):
- # The discount factor
- return math.exp( -(self.r - self.div)*self.dt )

- s = StockOption(50, 52,T=2)
- s.df #e^(-rt)
== code: math.exp(-r*1) and r=0.05
The
are compulsory common attributes for pricing options. The
are computed as properties of the class and may be overwritten by implementing classes if needed.
The Python implementation of the binomial option pricing model of a European option is given as the BinomialEuropeanOption class, inheriting the common attributes of the option from the StockOption class. The implementations of the methods in this class are as follows:
The class implementation of BinomialEuropeanOption is given in the following Python code:
- import math
- import numpy as np
- from decimal import Decimal
-
- # Price a European option by the binomial tree model
-
- 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, e.g 1+0.2=1.2
- self.d = 1 - self.pd # Expected value in the down state e.g 1-0.2=0.8
-
- # qu : The risk-neutral (up state) probability
- self.qu = ( math.exp( (self.r-self.div)*self.dt )-self.d
- )/(self.u-self.d)
- # qd : The risk-neutral (down state) probability
- self.qd = 1-self.qu
-
- def init_stock_price_tree( self ):
- # Initialize terminal price nodes to zeros
- self.STs = np.zeros( self.M )
-
- # Calculate expected stock prices for each terminal node
- for i in range( self.M ):
- self.STs[i] = self.S0 * ( self.u**(self.N-i) )*( self.d**i )
- # Suu = $72, Sud=Sdu=$48, Sdd=$32
-
- def init_payoffs_tree( self ):
- # Returns the payoffs when the option expires at terminal nodes
- if self.is_call: # call option
- return np.maximum( 0, self.STs - self.K )
- else: # put option
- return np.maximum( 0, self.K - self.STs )
- # Puu=$0, Pud=Pdu=$4, Pdd=$20
-
- def traverse_tree( self, payoffs ):
- """
- Starting from the time the option expires, traverse
- backwards and calculate discounted payoffs at each node
- """
- # N: number of time steps
- # payoffs : [Puu=$0, Pud=Pdu=$4, Pdd=$20]
- for i in range( self.N ):
- payoffs = ( payoffs[:-1] * self.qu +
- payoffs[1:] * self.qd
- ) * self.df # df: The discount factor
- # * math.exp( -(self.r - self.div)*self.dt )
- # array([1.41475309, 9.46393007])
- # array([4.19265428])
- return payoffs
-
- def begin_tree_traversal( self ):
- # Initialize the payoffs at the terminal node
- payoffs = self.init_payoffs_tree() # [Puu=$0, Pud=Pdu=$4, Pdd=$20]
- return self.traverse_tree( payoffs )
-
- ###### Entry point of the pricing implementation ######
- def price( self ):
- self.setup_parameters()
- self.init_stock_price_tree() # STs : Suu = $72, Sud=Sdu=$48, Sdd=$32
- payoffs = self.begin_tree_traversal()
- # Option value converges to first node
- return payoffs[0]

Let's take the values from the two-step binomial tree example we discussed earlier to price the European put option:
- eu_option = BinomialEuropeanOption(
- 50, 52, r=0.05, T=2, N=2, pu=0.2, pd=0.2, is_put=True)
- print( 'European put option price is:', eu_option.price() )
Using the binomial option pricing model gives us a present value of $4.19 for the European put option.
- eu_option = BinomialEuropeanOption(
- 50, 51, r=0.05, T=6/12, N=2, pu=0.06, pd=0.05, is_put=True)
- print( 'European put option price is:', eu_option.price() )
###############
Call option
- eu_option = BinomialEuropeanOption(
- 50, 51, r=0.05, T=6/12, N=2, pu=0.06, pd=0.05, is_put=False)
-
- print( 'European Call option price is:', eu_option.price() )
- print( 'Expected stock prices for each terminal node:', eu_option.STs )
- print( "Expected payoff for each terminal node:",
- eu_option.init_payoffs_tree()
- )
put option
- eu_option = BinomialEuropeanOption(
- 50, 51, r=0.05, T=6/12, N=2, pu=0.06, pd=0.05, is_put=True)
-
- print( 'European put option price is:', eu_option.price() )
- print( 'Expected stock prices for each terminal node:', eu_option.STs )
- print( "Expected payoff for each terminal node:",
- eu_option.init_payoffs_tree()
- )
Verify that the European call and European put prices satisfy put–call parity看跌期权平价?
The value of the put plus the stock price is
The value of the call plus the present value of the strike price is
eu_option.price() + eu_option.K * math.exp(-eu_option.r * eu_option.T )
=
This verifies that put–call parity holds
To test whether it worth exercising the option early we compare the value calculated for the option at each node with the payoff from immediate exercise. At node C the payoff from immediate exercise is . Because this is greater than 2.8664, the option should be exercised at this node. The option should not be exercised at either node A or node B.
###############
Unlike European options, which 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, in the same way we did with the BinomialEuropeanOption class, create a class named BinomialTreeOption that inherits the Stockoption class. The parameters that are used in the setup_parameters() method remain the same, except for the removal of an unused M parameter(Number of terminal nodes of tree).
The methods that are used in American options are as follows: K=52
The following code is for pricing the American option:
- import math
- import numpy as np
-
- # Price a European or American option by the binomial tree
- class BinomialTreeOption(StockOption):
- def setup_parameters( self ):
- self.u = 1 + self.pu # Expected value % in the up state, e.g 1+0.2=1.2
- self.d = 1 - self.pd # Expected value % in the down state e.g 1-0.2=0.8
- # qu : The risk-neutral (up state) probability
- self.qu = ( math.exp( (self.r-self.div)*self.dt )-self.d
- )/(self.u-self.d)
- # qd : The risk-neutral (down state) probability
- self.qd = 1-self.qu
-
- def init_stock_price_tree(self):
- # Initialize a 2D tree at T=0
- # to store the expected returns of the stock prices for all time steps
- self.STs = [ np.array([self.S0]) ] # array([50]) # root
-
- # Simulate the possible stock prices path
- # calculate the payoff values
- # from exercising the option at each period
- for i in range( self.N ): # N: number of time steps(branch_nodes)
- prev_branches = self.STs[-1] # at last time step
- st = np.concatenate( # note, cross node: Sud = Sdu
- ( prev_branches*self.u, # go up
- [prev_branches[-1]*self.d] # the last one goes down
- )# array([60., 40.])
- ) # array([72., 48., 32.])
-
- self.STs.append(st) # Add nodes at each time step
-
- # the intrinsic values of the option at maturity
- def init_payoffs_tree(self):
- if self.is_call:# call option
- return np.maximum( 0, self.STs[self.N]-self.K )
- else:# put option
- return np.maximum( 0, self.K-self.STs[self.N] )
- # e.g. payoffs: [ 0. 4. 20.]
-
- # Returns the maximum payoff values between
- # exercising the American option early
- # and not exercising the option at all
- def check_early_exercise( self, payoffs, node ):
- if self.is_call:
- return np.maximum( payoffs, self.STs[node]-self.K )
- else:
- # payoffs: [1.41475309 9.46393007]
- # payoffs at maturity: [-8. 12.]
- # np.maximum ==>[1.41475309 12.]
- return np.maximum( payoffs, self.K-self.STs[node] )
-
- def traverse_tree( self, payoffs ):
- """
- Starting from the time the option expires, traverse
- backwards and calculate discounted payoffs at each node
- """
- 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 # df: The discount factor
- # * math.exp( -(self.r - self.div)*self.dt )
- print("At the time step",i,", payoffs:",payoffs)
- # [1.41475309 9.46393007]
- # [4.19265428]
- # Payoffs from exercising, for American options
- if not self.is_european:
- payoffs = self.check_early_exercise( payoffs, i )
- print("At the time step",i,", max payoffs:",payoffs)
-
- return payoffs
-
- def begin_tree_traversal( self ):
- # the intrinsic values of the option at maturity
- payoffs = self.init_payoffs_tree() # e.g. payoffs: [ 0. 4. 20.]
- print("the payoffs at maturity", payoffs)
- return self.traverse_tree( payoffs ) # calculate option values
-
- ###### Entry point of the pricing implementation ######
- def price(self):
- self.setup_parameters()
- self.init_stock_price_tree()
- # self.STs : [array([50]), array([60., 40.]), array([72., 48., 32.])]
- print("Nodes:",self.STs)
- payoffs = self.begin_tree_traversal()
- #print(payoffs)
- return payoffs[0]

- am_option = BinomialTreeOption(50, 52,
- r=0.05, T=2, N=2, pu=0.2, pd=0.2, is_put=True, is_am=True)
-
- print( "American put option price is:", am_option.price() )
The American put option is priced at $5.0896. 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.
################
European put option
- eu_option = BinomialTreeOption(
- 50, 51, r=0.05, T=6/12, N=2, pu=0.06, pd=0.05, is_put=True, is_am=False)
-
- print( "European put option price is:", eu_option.price() )
American put option
- am_option = BinomialTreeOption(
- 50, 51, r=0.05, T=6/12, N=2, pu=0.06, pd=0.05, is_put=True, is_am=True)
-
- print( "American put option price is:", am_option.price() )
The American put option is priced at $1.6456. 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.
To test whether it worth exercising the option early we compare the value calculated for the option at each node with the payoff from immediate exercise. At node C the payoff from immediate exercise is . Because this is greater than 2.8664, the option should be exercised at this node. The option should not be exercised at either node A or node B.
################
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 losses
- am_option2 = BinomialTreeOption(10, 12,
- r=0.006, T=9, N=3, pu=0.2, pd=0.2, is_put=True, is_am=False)
-
- print("European put option price is:", am_option2.price())
- am_option2 = BinomialTreeOption(10, 12,
- r=0.006, T=9, N=3, pu=0.2, pd=0.2, is_put=True, is_am=True)
-
- print("American put option price is:", am_option2.price())
In the preceding examples, we assumed that the underlying stock price would increase by 20 percent and decrease by 20 percent in the respective u>=1 up state and 0<d<=1 down state. 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 volatilityof the underlying stock, or the standard deviation of returns of the stock, is taken into account as follows:http://home.cerge-ei.cz/petrz/fm/f400n10.pdf
From the condition that the variance of the log of the price is
,
######################
Calculate u, d, and p when a binomial tree is constructed to value an option on a foreign currency. The tree step size is one month(delta t =1/12), the domestic interest rate is 5% per annum, the foreign interest rate is 8% per annum, and the volatility is 12% per annum.==>
and q=0
==>
The volatility of a non-dividend-paying stock whose price is $78, is 30%. The risk-free
rate is 3% per annum (continuously compounded) for all maturities. Calculate values for
u, d, and p when a two-month time step is used(2/12=).
==>sigma=0.03
q=0 ==>==>
######################==>
The implementation of the binomial CRR model remains the same as the binomial tree we discussed earlier, with the exception of the u and d model parameters.
In Python, let's create a class named BinomialCRROption and simply inherit the BinomialTreeOption class. Then, all that we need to do is override the setup_parameters() method with values from the CRR model.
Instances of the BinomialCRROption object will invoke the price() method, which invokes all other methods of the parent BinomialTreeOption class, except the overwritten setup_parameters() method:and
- import math
-
- # Price an option by the binomial CRR model
-
- class BinomialCRROption( BinomialTreeOption ):
- def setup_parameters(self): # self.dt = self.T/float(self.N)
- # Expected stock price % in the up state
- self.u = math.exp( self.sigma * math.sqrt(self.dt) )
- # Expected stock price % in the down state
- self.d = 1./self.u
-
- # Calculate the transition probabilities
- # qu : The risk-neutral (up state) probability
- self.qu = ( math.exp( (self.r-self.div)*self.dt )-self.d
- )/(self.u-self.d)
- # qd : The risk-neutral (down state) probability
- self.qd = 1-self.qu

Again, consider 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 r is five percent per annum and the time to maturity T is two years. We would like to find the value of a European put option with a strike price K of $52 by the CRR model:
- eu_option = BinomialCRROption( 50, 52,
- r=0.05, T=2, N=2, sigma=0.3, is_put=True )
- print('European put option price is:', eu_option.price() )
- am_option = BinomialCRROption( 50, 52,
- r=0.05, T=2, N=2, sigma=0.3, is_put=True, is_am=True)
- print( "American put option price is:", am_option.price() )
By using the CRR two-step binomial tree model, the price of the European put option and the American put option are $6.2457 and $7.4284, respectively.
######################
The volatility of a non-dividend-paying stock whose price is $78, is 30%. The risk-free
rate is 3% per annum (continuously compounded) for all maturities. Calculate values for
u, d, and p when a two-month time step is used(2/12=).
What is the value of a four-month European call option with a strike price of $80 given by a two-step binomial tree
==>
q=0 ==>==>
T=2/12*2
- eu_option = BinomialCRROption( 78, 80,
- r=0.03, T=2/12*2, N=2, sigma=0.3, is_put=False )
- print('European put option price is:', eu_option.price() )
Suppose a trader sells 1,000 options (10 contracts). What position in the stock is necessary to hedge the trader’s position at the time of the trade?
The value of the option is $4.67. The initial delta is 9.58/(88.16 – 69.01) which is almost exactly 0.5 so that 500 shares should be purchased.
Problem 12.19
The current price of a non-dividend-paying biotech stock is $140 with a volatility of 25%. The risk-free rate is 4%. For a three-month time step:
Use a two-step tree to value a six-month European call option and a six-month European put option. In both cases the strike price is $150.
The tree for valuing the call is in Figure S12.6a and that for valuing the put is in Figure S12.6b. The values are 7.56 and 14.58, respectively.
- eu_call_option = BinomialCRRLattice(140, 150, r=0.04, T=6/12,
- sigma=0.25, N=2, is_put=False)
- print("call option price:",eu_call_option.price())
- eu_put_option = BinomialCRRLattice(140, 150, r=0.04, T=6/12,
- sigma=0.25, N=2, is_put=True)
- print("put option price:",eu_put_option.price())
######################
In the binomial models we discussed earlier, we made several assumptions about the probability of up and down states, as well as the resulting risk-neutral probabilities. Besides the binomial model with CRR(Cox-Ross-Rubinstein) parameters that we discussed, other forms of parameterization that are 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(https://blog.csdn.net/Linli522362242/article/details/96593981) as the number of step 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 traversal.节点不会在每个交替步骤中重新组合。 它使用反演公式在树遍历期间实现更好的准确性。
A detailed explanation of the formula is given in the paper Binomial Models For Option Valuation - Examining And Improving Convergence, March 1995, which is available at https://papers.ssrn.com/sol3/papers.cfm?abstract_id=5976. We will be using method two of the Peizer and Pratt inversion function f with the following characteristic parameters:
The
The Python implementation of the LR tree is given in the following BinomialLROption class. Similar to the BinomialCRROption class, we simply inherit the BinomialTreeOption class and override the variables in the setup_parameters method with those of the LR tree model:
- import math
-
- # Price an option by the Leisen-Reimer tree
-
- class BinomialLROption( BinomialTreeOption ):
- def pp_2_inversion( self, z, n ):
- # copysign : return the value of the first parameter and the sign of the second parameter
- # math.copysign(4, -1) ==> -4.0
- # math.copysign(-8, 97.21) ==> 8.0
- # math.copysign(-43, -76) ==> -43.0
- return 0.5 + math.copysign(1,z) *\
- math.sqrt( 0.25-0.25*math.exp(
- -( ( z/( n+1./3.+0.1/(n+1) ) )**2.
- ) * ( n+1./6. ) )
- )
-
- def setup_parameters( self ):
- odd_N = self.N if (self.N%2==0) 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) )
- d2 = d1-self.sigma*math.sqrt(self.T)
-
- pbar = self.pp_2_inversion( d1, odd_N )
- self.p = self.pp_2_inversion( d2, odd_N )
-
- # self.dt : self.T/float(self.N)
- # self.df : math.exp( -(self.r - self.div)*self.dt )
- # self.u : Expected value in the up state, e.g 1+0.2=1.2
- self.u = 1/self.df * pbar/self.p
- # self.d : Expected value in the down state e.g 1-0.2=0.8
- self.d = (1/self.df - self.p*self.u)/(1-self.p)
-
- # qu : The risk-neutral (up state) probability
- self.qu = self.p
- # qd : The risk-neutral (down state) probability
- self.qd = 1-self.p

Using the same examples that we used previously, we can price the options using an LR tree:
- eu_option = BinomialLROption(
- 50, 52, r=0.05, T=2, N=2, sigma=0.3, is_put=True)
-
- print('European put option price is:', eu_option.price() )
- eu_option = BinomialLROption(
- 50, 52, r=0.05, T=2, N=4, sigma=0.3, is_put=True)
-
- print('European put option price is:', eu_option.price() )
- am_option = BinomialLROption(
- 50, 52, r=0.05, T=2, N=4, sigma=0.3, is_put=True, is_am=True )
-
- print('American put option price is:', am_option.price() )
By using the LR binomial tree model with four time steps, the price of the European put option and the American put option are $5.87865 and $6.7636, respectively.
In the binomial tree pricing models that we have covered so far, we traversed up and down the tree at each point in time to determine the node values. From the information at each node, we can reuse these computed values easily. One such use is the computation of Greeks.
The Greeks measure the sensitivities of the price of derivatives, such as options with respect to changes in the parameters of the underlying asset, often represented by Greek letters. In mathematical finance, the common names associated with Greeks include alpha, beta, delta, gamma, vega, theta, and rho.
Two particularly useful Greeks for options are delta and gamma.
As shown in the following diagram, an additional layer of nodes is added around our original 2-step tree to make it a 4-step tree, which extends 2 steps backward in time. Even with additional terminal payoff nodes, all nodes will contain the same information as our original 2-step tree. Our option value of interest is now located in the middle of the tree at t=0:==>
Notice that at t=0 there exists two additional nodes' worth of information that we can use to compute the delta formula, as follows:
The delta formula states that the difference in the option prices in the up and down state is represented as a unit of the difference between the respective stock prices at time t=0.
Conversely, the gamma formula can be computed as follows:
The gamma formula states that the difference of deltas between the option prices in the up node and the down node against the initial node value are computed as a unit of the differences in price of the stock at the respective states.
To illustrate the computation of Greeks with the LR tree(Leisen-Reimer tree), let's create a new class named BinomialLRWithGreeks that inherits the BinomialLROption class with our own implementation of the price method.
In the price method, we will start by calling the setup_parameters() method of the parent class to initialize all variables required by the LR tree. However, this time, we will also call the new_stock_price_tree() method, which is a new method that's used to create an extra layer of nodes around the original tree.
The begin_tree_traversal() method is called to perform the usual LR tree implementation in the parent class. The returned NumPy array object now contains information on the three nodes at t=0, where the middle node is the option price. The payoffs in the up and down states at t=0 are in the first and last index of the array, respectively.
With this information, the price() method computes and returns the option price, the delta, and the gamma values together:
- import numpy as np
-
- # Compute option price, delta and gamma by the LR tree
-
- class BinomialLRWithGreeks( BinomialLROption ):
- def new_stock_price_tree( self ):
- """
- Creates an additional layer of nodes to our
- original stock price tree
- """
- self.STs = [np.array([self.S0 * self.u/self.d,
- self.S0,
- self.S0 * self.d/self.u
- ]),# at t=0
- ]
-
- # Simulate the possible stock prices path
- # calculate the payoff values
- # from exercising the option at each period
- for i in range( self.N ): # N : number of time steps
- prev_branches = self.STs[-1]
- # note, cross node: Sud = Sdu
- st = np.concatenate( (prev_branches*self.u, # branch_nodes : go up
- [prev_branches[-1]*self.d]# the last one goes down
- ) )
- self.STs.append(st) # Add nodes at each time step
-
- def price(self):
- # def setup_parameters( self ):
- # odd_N = self.N if (self.N%2==0) 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 = d1-self.sigma*math.sqrt(self.T)
-
- # pbar = self.pp_2_inversion( d1, odd_N )
- # self.p = self.pp_2_inversion( d2, odd_N )
-
- # # self.dt : self.T/float(self.N)
- # # self.df : math.exp( -(self.r - self.div)*self.dt )
- # # self.u : Expected value in the up state, e.g 1+0.2=1.2
- # self.u = 1/self.df * pbar/self.p
- # # self.d : Expected value in the down state e.g 1-0.2=0.8
- # self.d = (1/self.df - self.p*self.u)/(1-self.p)
-
- # # qu : The risk-neutral (up state) probability
- # self.qu = self.p
- # # qd : The risk-neutral (down state) probability
- # self.qd = 1-self.p
- self.setup_parameters()
- self.new_stock_price_tree()
- print("Nodes:",self.STs)
-
- # # the intrinsic values of the option at maturity
- # def init_payoffs_tree(self):
- # if self.is_call:# call option
- # return np.maximum( 0, self.STs[self.N]-self.K )
- # else:# put option
- # return np.maximum( 0, self.K-self.STs[self.N] )
- # # e.g. payoffs: [ 0. 4. 20.]
-
- # def traverse_tree( self, payoffs ):
- # """
- # Starting from the time the option expires, traverse
- # backwards and calculate discounted payoffs at each node
- # """
- # 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 # df: The discount factor
- # # * math.exp( -(self.r - self.div)*self.dt )
- # print("At the time step",i,", payoffs:",payoffs)
- # # [1.41475309 9.46393007]
- # # [4.19265428]
- # # Payoffs from exercising, for American options
- # if not self.is_european:
- # payoffs = self.check_early_exercise( payoffs, i )
- # print("At the time step",i,", max payoffs:",payoffs)
- # return payoffs
-
- # def begin_tree_traversal( self ):
- # # the intrinsic values of the option at maturity
- # payoffs = self.init_payoffs_tree() # e.g. payoffs: [ 0. 4. 20.]
- # print("the payoffs at maturity", payoffs)
- # return self.traverse_tree( payoffs )
-
- payoffs = self.begin_tree_traversal()
-
- #option value is now in the middel node at t=0
- # self.STs[0] : [ S0*u/d , S0, S0*d/u ]
- option_value = payoffs[ len(payoffs)//2 ] #option_value at S0(stock initial price)
-
- payoff_up = payoffs[0] # at S0*u/d : option value in the up state
- payoff_down = payoffs[-1] # at S0*d/u : option value in the down state
-
- S_up = self.STs[0][0] # S0*u/d : stock price in the up state
- S_down = self.STs[0][-1] # S0*d/u : stock price in the down state
-
- dS_up = S_up - self.S0 # stock price in the up state - S0
- dS_down = self.S0 - S_down # S0 - stock price in the down state
-
- # Calculate the delta value
- # the difference in the 'option prices' in the up and down state
- # is represented as a unit of
- # the difference between the respective 'stock prices' at time t=0.
- dV = payoff_up - payoff_down
- dS = S_up - S_down
- delta = dV/dS
-
- # Calculate the gamma value
- gamma = ( (payoff_up - option_value)/dS_up
- - ( option_value - payoff_down) /dS_down
- )/( (self.S0 + S_up)/2 - (self.S0 + S_down)/2 )
-
- return option_value, delta, gamma

- # σ or sigma the annualized volatility of the underlying stock
- eu_put = BinomialLRWithGreeks( 50, 52, r=0.05, T=2, N=2, sigma=0.3, is_put=True )
- results = eu_put.price()
-
- print('European put values')
- print('Price: %s\nDelta: %s\nGamma: %s' % results)
Nodes(stock prices): [array([84.90937139, 50. , 29.44315756]),
array([113.61937767, 66.90626477, 39.3986339 , 23.20040371]),
array([152.03696329, 89.5289653 , 52.72030863, 31.04504707, 18.28128424])]
the payoffs at maturity [ 0. 0. 0. 20.95495293 33.71871576]
At the time step 1 , payoffs: [ 0. 0. 10.39321697 26.26352636]
At the time step 0 , payoffs: [ 0. 5.15481754 17.75767426]
European put values
Price: 5.154817539524094
Delta: -0.3201529909525428
Gamma: 0.01678177314959919
VS
As we can see from the price() method and results, we managed to obtain additional information on Greeks from the modified binomial tree without any extra overhead in computational complexity.
Using the same example from the LR tree, we can compute the option values and Greeks for a European call and put option with 300 time steps:
- # σ or sigma the annualized volatility of the underlying stock
- eu_put = BinomialLRWithGreeks( 50, 52, r=0.05, T=2, N=300, sigma=0.3,
- is_put=True )
- results = eu_put.price()
-
- print('European put values')
- print('Price: %s\nDelta: %s\nGamma: %s' % results)
- # σ or sigma the annualized volatility of the underlying stock
- eu_call = BinomialLRWithGreeks( 50, 52, r=0.05, T=2, N=300, sigma=0.3,
- is_put=False )
- results = eu_call.price()
-
- print('European call values')
- print('Price: %s\nDelta: %s\nGamma: %s' % results)
As we can see from the price() method and results, we managed to obtain additional information on Greeks from the modified binomial tree without any extra overhead in computational complexity.
In the binomial tree, each node leads to two other nodes in the next time step. Similarly, in a trinomial tree, each node leads to three other nodes in the next time step. Besides having up and down states, the middle node of the trinomial tree indicates no change in state. When extended over more than 2 time steps, the trinomial tree can be thought of as a recombining tree, where the middle nodes always retain the same values as the previous time step.==>
Let's consider the Boyle trinomial tree, where the tree is calibrated[ˈkælɪbreɪtɪd]校准使...标准化 so that the probability of up, down, and flat movements, u, d, and m with risk-neutral probabilities ,
, and
are as follows:
We can see that
In general, with an increased number of nodes to process, a trinomial tree gives better accuracy than the binomial tree when fewer time steps are modeled, saving on computation speed and resources. The following diagram illustrates the stock price movements of a trinomial tree with 2 time steps:
Let's create a TrinomialTreeOption class, inheriting from the BinomialTreeOption class.
The methods for the TrinomialTreeOption are provided as follows:
- import math
- import numpy as np
-
- class TrinomialTreeOption( BinomialTreeOption ):
- def setup_parameters( self ):
- # Calculate the jump sizes u, d and m
- self.u = math.exp( self.sigma*math.sqrt( 2.*self.dt) )
- self.m = 1
- self.d = 1/self.u
-
- # Calculate the transition probabilities
- # self.dt : self.T/float(self.N)
- exponential_part = math.exp( self.sigma * math.sqrt(self.dt/2.) )
- self.qu = ( ( math.exp( (self.r-self.div)*self.dt/2)
- - 1/exponential_part
- )/( exponential_part - 1/exponential_part )
- )**2
- self.qd = ( ( exponential_part
- - math.exp( (self.r-self.div)*self.dt/2)
- )/( exponential_part - 1/exponential_part )
- )**2
-
- self.qm = 1 - self.qu - self.qd
-
- def init_stock_price_tree(self):
- # Initialize a 2D tree at t=0
- self.STs = [ np.array([self.S0]) ]
-
- for i in range( self.N ):
- prev_nodes = self.STs[-1]
- st = np.concatenate( ( prev_nodes*self.u, # up state nodes
- [prev_nodes[-1]*self.m,# middle nodes
- prev_nodes[-1]*self.d # down state nodes
- ]
- ) )
- self.STs.append( st )
-
- def traverse_tree( self, payoffs ):
- """
- Starting from the time the option expires, traverse
- backwards and calculate discounted payoffs at each node
- """
- # # Returns the maximum payoff values between
- # # exercising the American option early
- # # and not exercising the option at all
- # def check_early_exercise( self, payoffs, node ):
- # if self.is_call:
- # return np.maximum( payoffs, self.STs[node]-self.K )
- # else:
- # # payoffs: [1.41475309 9.46393007]
- # # payoffs at maturity: [-8. 12.]
- # # np.maximum ==>[1.41475309 12.]
- # return np.maximum( payoffs, self.K-self.STs[node] )
-
- for i in reversed( range(self.N) ):
- payoffs = ( payoffs[:-2] * self.qu +
- payoffs[1:-1] * self.qm +
- payoffs[2:] * self.qd
- ) * self.df # df: The discount factor
- # * math.exp( -(self.r - self.div)*self.dt )
- if not self.is_european:
- payoffs = self.check_early_exercise( payoffs, i )
-
- print("At the time step",i,", payoffs:",payoffs)
-
- return payoffs

Using the same example of the binomial tree, we get the following result:
- eu_put = TrinomialTreeOption( 50, 52, r=0.05, T=2, N=2, sigma=0.3, is_put=True )
-
- print( "European put option:", eu_put.price() )
Nodes: [array([50]), array([76.42325802, 50. , 32.71255459]), array([116.81028732, 76.42325802, 50. , 32.71255459, 21.40222456])] the payoffs at maturity [ 0. 0. 2. 19.28744541 30.59777544] At the time step 1 , payoffs: [ 0.46353923 5.42131793 16.75137548] At the time step 0 , payoffs: [6.57356527] European put option: 6.573565269142496
- am_put = TrinomialTreeOption(50, 52,
- r=0.05, T=2, N=2, sigma=0.3, is_put=True, is_am=True)
- print( "American put option:", am_put.price() )
Nodes: [array([50]), array([76.42325802, 50. , 32.71255459]), array([116.81028732,76.42325802, 50. , 32.71255459, 21.40222456])] the payoffs at maturity [ 0. 0. 2. 19.28744541 30.59777544] At the time step 1 , payoffs: [ 0.46353923 5.42131793 19.28744541] At the time step 0 , payoffs: [7.16134922] American put option: 7.161349217272585
By the trinomial tree model, we obtain prices of $6.57 and $7.16 for the European and American put options, respectively.
This property of recombining trees can also be represented as lattices[ˈlætɪs]格子,[计]点阵 to save memory without recomputing and storing recombined nodes.
We will create a binomial lattice from the binomial CRR tree(Cox-Ross-Rubinstein) since at every alternate up and down nodes, the prices recombine to the same probability of ud=1. (self.u : Expected value in the up state AND self.d : Expected value in the down state) In the following diagram,
and
recombine with
. The tree can now be represented as a single list:(
S*u*u = S_uu,
S_uu*d = S*u*u*d = S*u= S_u,
S_u*d = S*u*d = S = S_0
S*d = S_d
S_d * d = S_dd
)
For a N-step binomial tree, a list of size 2N +1 is required to contain the information on the underlying stock prices.
Let's convert the binomial tree pricing into a lattice by CRR. We can inherit from the BinomialCRROption class (which in turn inherits the BinomialTreeOption class) and create a new class named BinomialCRRLattice, as follows:
- import numpy as np
-
- # import math
-
- # # Price an option by the binomial CRR model
-
- # class BinomialCRROption( BinomialTreeOption ):
- # def setup_parameters(self): # self.dt = self.T/float(self.N)
- # # Expected stock price % in the up state
- # self.u = math.exp( self.sigma * math.sqrt(self.dt) )
- # # Expected stock price % in the down state
- # self.d = 1./self.u
- #
- # # Calculate the transition probabilities
- # # qu : The risk-neutral (up state) probability
- # self.qu = ( math.exp( (self.r-self.div)*self.dt )-self.d
- # )/(self.u-self.d)
- # # qd : The risk-neutral (down state) probability
- # self.qd = 1-self.qu
-
- class BinomialCRRLattice( BinomialCRROption ):
- def setup_parameters( self ):
- # inherit from the 'BinomialCRROption' class
- # (which in turn inherits the 'BinomialTreeOption' class)
- # python 2.x
- super(BinomialCRRLattice, self).setup_parameters()
- # python 3.x
- # super().setup_parameters()
- # N: number of time steps
- self.M = 2*self.N + 1 # Number of nodes of Lattice
-
- def init_stock_price_tree( self ):
- self.STs = np.zeros( self.M ) # initialize a lattice
- # lattice[0]
- self.STs[0] = self.S0 * self.u**self.N # e.g. if N=2, then S_uu=S*u*u
-
- for i in range(self.M)[1:]: # u*d=1
- self.STs[i] = self.STs[i-1]*self.d # if i=1, then S_uu*d=S*u*u*d=S*u
- # Current stock price <== S # if i=2, then S_u*d = S*u*d = S
- # ...
- def init_payoffs_tree( self ):
- odd_nodes = self.STs[::2] # Take odd nodes only
- print('the stock price at maturity',odd_nodes)
- # calculate all payoffs, e.g. payoffs at S_uu, S_0, S_dd
- if self.is_call: # call option
- return np.maximum(0, odd_nodes-self.K)
- else: # put option
- return np.maximum(0, self.K-odd_nodes)
-
- def check_early_exercise( self, payoffs, node ):
- # S_u, S_0, S_d
- self.STs = self.STs[1:-1] # Shorten ends of the list
- # S_uu, S_0, S_dd
- odd_STs = self.STs[::2] # Take odd nodes only
- if self.is_call:
- return np.maximum( payoffs, odd_STs - self.K )
- else:
- return np.maximum( payoffs, self.K - odd_STs )

Using the same stock information from our binomial CRR model example, we can price a European and American put option using the binomial lattice pricing:
- eu_option = BinomialCRRLattice(
- 50, 52, r=0.05, T=2, N=2, sigma=0.3, is_put=True)
-
- print( 'European put option price:', eu_option.price() )
Nodes: [91.10594002 67.49294038 50. 37.04091103 27.4405818 ]
the stock price at maturity [91.10594002 50. 27.4405818 ] VS 52
the payoffs at maturity [ 0. 2. 24.5594182]
At the time step 1 , payoffs: [ 0.93269783 12.42301904]
At the time step 0 , payoffs: [6.24570845]
European put option price: 6.245708445206432
(save memory without recomputing and storing recombined nodes, e.g. Sud=Sdu=S0=50) VS The Cox–Ross–Rubinstein model for the code def init_stock_price_tree(self):
- am_option = BinomialCRRLattice(50, 52,
- r=0.05, T=2, N=2, sigma=0.3, is_put=True, is_am=True)
-
- print("American put option price:", am_option. price() )
Nodes: [91.10594002 67.49294038 50. 37.04091103 27.4405818 ]
the stock price at maturity [91.10594002 50. 27.4405818 ]
the payoffs at maturity [ 0. 2. 24.5594182]
At the time step 1 , payoffs: [ 0.93269783 12.42301904]
At the time step 1 , max payoffs: [ 0.93269783 14.95908897]
At the time step 0 , payoffs: [7.4284019]
At the time step 0 , max payoffs: [7.4284019]
American put option price: 7.428401902704828
VS The Cox–Ross–Rubinstein model for the code def init_stock_price_tree(self):
By using the CRR binomial tree lattice pricing model, we obtain prices of $6.2457 and $7.428 for the European and American put options, respectively.
https://www.maths.usyd.edu.au/u/UG/SM/MATH3075/r/Slides_7_Binomial_Market_Model.pdf
- # # delta_t=1/12
- print( "1+r*delta_t", 1+0.1*(1/12),
- "is close to", math.exp( 0.1*(1/12) )
- )
- # r=0.1 variance=0.1 2*sigma
- print( "p~:", 0.5 + ( (0.1 - 0.1/2)*math.sqrt(1/12)/(2*math.sqrt(0.1) ) ) )
-
- # sigma T=4/12=1/3
- print( "u:",
- math.exp( math.sqrt(0.1) * math.sqrt( (1/3)/float(4) ) )
- )
-
- print( "d:",
- 1/math.exp( math.sqrt(0.1) * math.sqrt( (1/3)/float(4) ) )
- )
For European option pricing, the odd nodes of payoffs from the list represent the option value upon maturity. The tree traverses backward to obtain the option value.
- eu_option2 = BinomialCRRLattice(50, 53,
- r=0.1, T=4/12, N=4, sigma=math.sqrt(0.1), is_put=True, is_am=False)
-
- print("European put option price is:", eu_option2.price())
(save memory without recomputing and storing recombined nodes)
Nodes: [72.03638777 65.75161833 60.01515966 54.77917472 50. 45.63778138
41.65614179 38.02187784 34.70468297]
the stock price at maturity [72.03638777 60.01515966 50. 41.65614179 34.70468297] vs 53
the payoffs at maturity [ 0. 0. 3. 11.34385821 18.29531703]
At the time step 3 , payoffs: [ 0. 1.4192295 6.92238713 14.53829067]
At the time step 2 , payoffs: [ 0.67140413 4.01086629 10.46784527]
At the time step 1 , payoffs: [2.24565342 7.03222631]
At the time step 0 , payoffs: [4.49143255]
European put option price is: 4.491432553637773
- am_option2 = BinomialCRRLattice(50, 53,
- r=0.1, T=4/12, N=4, sigma=math.sqrt(0.1), is_put=True, is_am=True)
-
- print("American put option price is:", am_option2.price())
Nodes: [72.03638777 65.75161833 60.01515966 54.77917472 50. 45.63778138
41.65614179 38.02187784 34.70468297]
the stock price at maturity [72.03638777 60.01515966 50. 41.65614179 34.70468297]
the payoffs at maturity [ 0. 0. 3. 11.34385821 18.29531703]
At the time step 3 , payoffs: [ 0. 1.4192295 6.92238713 14.53829067] #European
At the time step 3 , max payoffs: [ 0. 1.4192295 7.36221862 14.97812216] #American
##############def check_early_exercise( self, payoffs, node ):
self.STs = self.STs[1:-1] # Shorten ends of the list
Nodes: [72.03638777 65.75161833 60.01515966 54.77917472 50. 45.63778138
41.65614179 38.02187784 34.70468297]
odd_STs = self.STs[::2] # Take odd nodes only
Nodes: [ 65.75161833>53 60.01515966 54.77917472>53 50. 53-45.63778138=7.36221862
41.65614179 53-38.02187784=14.97812216 ]
For American option pricing, as the tree traverses backward, both ends of the list shrink列表的两端缩小, and the odd nodes represent the associated stock prices for any time step. Payoffs from the earlier exercise can then be taken into account.
##############
At the time step 2 , payoffs: [ 0.67140413 4.21894023 10.90402672]
At the time step 2 , max payoffs: [ 0.67140413 4.21894023 11.34385821]
At the time step 1 , payoffs: [2.34408831 7.55455975]
At the time step 1 , max payoffs: [2.34408831 7.55455975]
At the time step 0 , payoffs: [4.78958701]
At the time step 0 , max payoffs: [4.78958701]
American put option price is: 4.789587008231834https://www.maths.usyd.edu.au/u/UG/SM/MATH3075/r/Slides_7_Binomial_Market_Model.pdf
The trinomial lattice works in very much the same way as the binomial lattice. Since each node recombines at every other node instead of alternate nodes, extracting odd nodes from the list is not necessary. Since the size of the list is the same as the one in the binomial lattice, there are no extra storage requirements in trinomial lattice pricing, as explained in the following diagram:
A class for the trinomial lattice option pricing model
In Python, let's create a class named TrinomialLattice for the trinomial lattice implementation that inherits from the TrinomialTreeOption class(which in turn inherits the BinomialTreeOption class).
Just as we did for the BinomialCRRLattice class, the setup_parameters, init_stock_price_tree , init_payoffs_tree , and check_early_exercise methods are overwritten, without having to take into account the payoffs at odd nodes:
- import numpy as np
-
- # Price an option by the trinomial lattice
- class TrinomialLattice( TrinomialTreeOption ):
- def setup_parameters( self ):
- # inherit from the 'TrinomialTreeOption' class
- # (which in turn inherits the 'BinomialTreeOption' class)
- # python 2.x
- # super(TrinomialLattice, self).setup_parameters()
- # python 3.x
- super().setup_parameters()
- self.M = 2*self.N + 1
-
- def init_stock_price_tree( self ):
- self.STs = np.zeros( self.M ) # initialize a lattice
- # lattice[0]
- self.STs[0] = self.S0 * self.u**self.N # e.g. if N=2, then S_uu=S*u*u
-
- for i in range(self.M)[1:]: # u*d=1
- self.STs[i] = self.STs[i-1]*self.d # if i=1, then S_uu*d=S*u*u*d=S*u
- # Current stock price <== S # if i=2, then S_u*d = S*u*d = S
- # ...
- def init_payoffs_tree( self ):
- # without having to take into account the payoffs at odd nodes ==>self.STs
-
- # calculate all payoffs, e.g. payoffs at S_uu, S_0, S_dd
- if self.is_call: # call option
- return np.maximum(0, self.STs-self.K)
- else: # put option
- return np.maximum(0, self.K-self.STs)
-
- def check_early_exercise( self, payoffs, node ):
- # S_u, S_0, S_d
- self.STs = self.STs[1:-1] # Shorten ends of the list
- # without having to take into account the payoffs at odd nodes ==>self.STs
- if self.is_call:
- return np.maximum( payoffs, self.STs - self.K )
- else:
- return np.maximum( payoffs, self.K - self.STs )

Using the same examples as before, we can price the European and American options using the trinomial lattice model:
- eu_option = TrinomialLattice(
- 50, 52, r=0.05, T=2, N=2, sigma=0.3, is_put=True)
- print('European put option price:', eu_option.price() )
Nodes: [116.81028732 76.42325802 50. 32.71255459 21.40222456]
the payoffs at maturity [ 0. 0. 2. 19.28744541 30.59777544]
At the time step 1 , payoffs: [ 0.46353923 5.42131793 16.75137548]
At the time step 0 , payoffs: [6.57356527]
European put option price: 6.573565269142496
(save memory without recomputing and storing recombined nodes) VS
- am_option = TrinomialLattice(50, 52,
- r=0.05, T=2, N=2, sigma=0.3, is_put=True, is_am=True)
- print('American put option price:', am_option.price() )
Nodes: [116.81028732 76.42325802 50. 32.71255459 21.40222456]
the payoffs at maturity [ 0. 0. 2. 19.28744541 30.59777544]
At the time step 1 , payoffs: [ 0.46353923 5.42131793 19.28744541]
At the time step 0 , payoffs: [7.16134922]
American put option price: 7.161349217272585
(save memory without recomputing and storing recombined nodes) VS
The output agrees with the results that were obtained from the trinomial tree option pricing model.
################################################################
Stochastics : https://blog.csdn.net/Linli522362242/article/details/96593981
It all starts with pseudo-random numbers, which build the basis for all simulation efforts; although quasi-random numbers准随机数 (e.g., based on Sobol sequences) have gained some popularity in finance, pseudo-random numbers伪随机数 still seem to be the benchmark.
Throughout this chapter, to generate random numbers(For simplicity, we will speak of random numbers knowing that all numbers used will be pseudo-random), the functions provided by the numpy.random subpackage are used:
- import numpy as np
- # Imports the random number generation subpackage from NumPy.
- import numpy.random as npr
- import matplotlib.pyplot as plt
- %matplotlib inline
For example, the rand() function returns random numbers from the open interval [0,1) in the shape provided as a parameter to the function. The return object is an ndarray object. Such numbers can be easily transformed to cover other intervals of the real line. For instance, if one wants to generate random numbers from the interval, one can transform the returned numbers from npr.rand() as in the next example—this also works in multiple dimensions due to NumPy broadcasting:
- npr.seed(100) # Fixes the seed value for reproducibility
- np.set_printoptions(precision=4) # fixes the number of digits for printouts
- # Uniformly distributed random numbers as one-dimensional ndarray object
- npr.rand(10)
npr.rand(5,5) # Uniformly distributed random numbers as 2-dimensional ndarray object.
shape:(5,5)
- a = 5. # Lower limit …
- b = 10. # … and upper limit …
- npr.rand(10) * (b - a) + a # for the transformation to another interval [5,10)
npr.rand(5, 5) * (b - a) + a # The same transformation [5,10) for 2 dimensions
- sample_size = 500
- rn1 = npr.rand(sample_size, 3)# Uniformly distributed random numbers in the given shape
- rn2 = npr.randint(0,10,sample_size) # Random integers for a given interval [0,10)
- rn3 = npr.sample(size=sample_size) # the “continuous uniform” distribution over the stated interval
- a = [0,25,50,75,100]
- rn4 = npr.choice(a, size=sample_size) # Randomly sampled values from a finite list object.
- rn4
- fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(nrows = 2, ncols=2, figsize=(7,7))
-
- ax1.hist(rn1, bins=25, stacked = True) #stacked by columns
- ax1.set_title('rand')
- ax1.set_ylabel('frequency')
- ax1.grid(True)
-
- ax2.hist(rn2, bins=25)
- ax2.set_title('randint')
- ax2.grid(True)
-
- ax3.hist(rn3, bins=25)
- ax3.set_title('sample')
- ax3.set_ylabel('frequency')
- ax3.grid(True)
-
- ax4.hist(rn4, bins=25)
- ax4.set_title('choice')
- ax4.grid(True)

Figure 12-1. Histograms of simple random numbers shows the results graphically for two continuous distributions(Uniformly distributed random numbers : rand, sample) and two discrete ones(randint(given interval [0,10)), choice(Randomly sampled values from a finite list object))
Although there is much criticism[ˈkrɪtɪsɪzəm]批评 around the use of (standard) normal distributions in finance, they are an indispensable[ˌɪndɪˈspensəbl]不可或缺的 tool and still the most widely used type of distribution, in analytical as well as numerical applications. One reason is that many financial models directly rest in依赖于 one way or another on a normal distribution or a log-normal distribution. Another reason is that many financial models that do not rest directly on a (log-)normal assumption can be discretized离散化, and therewith对此 approximated for simulation purposes, by the use of the normal distribution.
As an illustration, Figure 12-2 visualizes random draws from the following distributions:
Figure 12-2 shows the results for the 3 continuous distributions and the discrete one (Poisson). The Poisson distribution is used, for example, to simulate the arrival of (rare) external events, like a jump in the price of an instrument or an exogenic shock外来冲击. Here is the code that generates it:
- sample_size = 500
- rn1 = npr.standard_normal(sample_size) # with mu=0, gamma=1
- rn2 = npr.normal(100,20, sample_size) # with mu=100 gamma=20 (standard deviation)
- rn3 = npr.chisquare(df=0.5, size=sample_size) # 0.5 degrees of freedom
- rn4 = npr.poisson(lam=1.0, size=sample_size) #lambda =1
-
- fig, ( (ax1, ax2), (ax3,ax4) ) = plt.subplots(nrows=2, ncols=2, figsize=(7,7) )
-
- ax1.hist(rn1, bins=25)
- ax1.set_title('standard normal')
- ax1.set_ylabel('frequency')
- ax1.grid(True)
-
- ax2.hist(rn2, bins=25)
- ax2.set_title('normal(100, 20)')
- ax2.grid(True)
-
- ax3.hist(rn3, bins=25)
- ax3.set_title('chi square')
- ax3.set_ylabel('frequency')
- ax3.grid(True)
-
- ax4.hist(rn4, bins=25)
- ax4.set_title('Poisson')
- ax4.grid(True)

Figure 12-2. Histograms of random samples for different distributions
In finance, 2 simulation tasks are of particular importance: simulation
Monte Carlo simulation (MCS) is among the most important numerical techniques in finance, if not the most important and widely used. This mainly stems from the fact that it is the most flexible numerical method when it comes to the evaluation of mathematical expressions (e.g., integrals), and specifically the valuation of financial derivatives. The flexibility comes at the cost of a relatively high computational burden, though, since often hundreds of thousands or even millions of complex computations have to be carried out to come up with a single value estimate.
Consider, for example, the Black-Scholes-Merton setup for option pricing. In their setup, the level of a stock index at a future date
given a level
as of today stock index is given according to Equation 12-1.
Equation 12-1. Simulating future index level in Black-Scholes-Merton setup
The variables and parameters have the following meaning:
This financial model is parameterized and simulated as follows. The output of this simulation code is shown in Figure 12-3:
- s0 = 100 # initial value
- r = 0.05 # constant short rate
- sigma=0.25 # constant volatility
- T=2.0 # in years # The horizon in year fractions
- Z=10000 # number of random draws
-
- #the level of a stock index ST1 at a future date T given
- # a level s0 as of today is given according to Equation
- # standard_normal(size) Samples from a standard normal distribution (mean=0, stdev=1)
-
- ST1 = s0 * np.exp( (r-0.5 * sigma **2 )* T + sigma*np.sqrt(T) * npr.standard_normal(Z) )
-
- plt.figure(figsize=(10,6))
- plt.hist(ST1, bins=50)
- plt.xlabel('index level')
- plt.ylabel('frequency')

Figure 12-3. Statically simulated geometric Brownian motion (via npr.standard_normal())
Figure 12-3 suggests that the distribution of the random variable as defined in Equation 12-1
is log-normal. One could therefore also try to use the numpy.random.lognormal() function to directly derive the values for the random variable. In that case, one has to provide the mean and the standard deviation to the function:
- s0 = 100 # initial value
- r = 0.05 # constant short rate
- sigma=0.25 # constant volatility
- T=2.0 # in years
- Z=10000 # number of random draws
-
- #the level of a stock index ST1 at a future date T given a level s0 as of today is given according to Equation
-
- #ST1 = s0 * np.exp( (r-0.5 * sigma **2 )* T + sigma*np.sqrt(T) * npr.standard_normal(Z) )
-
- ST2 = s0 * npr.lognormal( (r-0.5* sigma**2)*T, #mu==mean of lognormal Distribution
- sigma*np.sqrt(T), #standard deviation of lognormal Distribution
- size=Z
- )
- plt.hist(ST2, bins=50)
- plt.xlabel('index level')
- plt.ylabel('frequency')
- plt.grid(True)

Figure 12-4. Statically simulated geometric Brownian motion (via npr.lognormal())
By visual inspection, Figures 12-3 and 12-4 indeed look pretty similar. But let us verify this more rigorously by comparing statistical moments统计矩 of the resulting distributions.
To compare the distributional characteristics of simulation results, the scipy.stats subpackage and the helper function print_statistics(), as defined here, prove useful
- import scipy.stats as scs
-
- def print_statistics(a1, a2):
- sta1 = scs.describe(a1)
- sta2 = scs.describe(a2)
- print('%14s %14s %14s' % ('statistic', 'data set 1', 'data set 2') )
- print(45 * "-")
- print('%14s %14.3f %14.3f' % ('size', sta1[0], sta2[0]))
- print('%14s %14.3f %14.3f' % ('min', sta1[1][0], sta2[1][0]))
- print('%14s %14.3f %14.3f' % ('max', sta1[1][1], sta2[1][1]))
- print('%14s %14.3f %14.3f' % ('mean', sta1[2], sta2[2]))
- print('%14s %14.3f %14.3f' % ('std', np.sqrt(sta1[3]), np.sqrt(sta2[3])))
- print('%14s %14.3f %14.3f' % ('skew', sta1[4], sta2[4]))
- print('%14s %14.3f %14.3f' % ('kurtosis', sta1[5], sta2[5]))
-
- print_statistics(ST1, ST2)

Obviously, the statistics of both simulation results are quite similar. The differences are mainly due to what is called the sampling error in simulation. Another error can also be introduced when discretely simulating continuous stochastic processes—namely the discretization error, which plays no role here due to the static nature of the simulation approach.
Roughly speaking, a stochastic process is a sequence of random variables. In that sense, we should expect something similar to a sequence of repeated simulations of a random variable when simulating a process随机变量的一系列 重复模拟. This is mainly true, apart from the fact that the draws are in general not independent but rather depend on the result(s) of the previous draw(s). In general, however, stochastic processes used in finance exhibit the Markov property, which mainly says that tomorrow’s value of the process only depends on today’s state of the process, and not any other more “historic” state or even the whole path history. The process then is also called memoryless.
###########################
以W(t)表示运动中一微粒从时刻t=0到时刻t>0的位移的横坐标(同样也可以讨论纵坐标),且设W(0)=0,根据爱因斯坦1905年提出的理论,微粒的这种运动是由于受到大量随机的相互独立的分子的碰撞的结果. 于是,粒子在时段(s,t]上的位移可以看作是许多微小位移的代数和. 则W(t)-W(s)服从正态分布。
The Wiener process is characterised by the following properties:
That the process has independent increments means that if then
and
are independent random variables, and the similar condition holds for n increments.
The unconditional probability density function follows a normal distribution with mean = 0 and variance = t, at a fixed time t:
normal distribution:
The expectation is zero:
The variance, using the computational formula, is t:
These results follow immediately from the definition that increments have a normal distribution, centered at zero. Thus
We assume that the stock price is driven by the stochastic differential equation (SDE)
(1)
where is Brownian motion. We simulate
over the time interval [0, T], which we assume to be is discretized as
, where the time increments are equally spaced with width dt: Equally-spaced time increments is primarily used for notational convenience, because it allows us to write
as simply dt: All the results derived with equally-spaced increments are easily generalized to unequal spacing
Integrating from t to t + dt produces
(2)
Equation (2) is the starting point for any discretization scheme. At time t, the value of is known, and we wish to obtain the next value
The simplest way to discretize the process in Equation (2) is to use Euler discretization. This is equivalent to approximating the integrals using the leftpoint rule. Hence the first integral is approximated as the product of the integrand at time t, and the integration range dt
We use the left-point rule since at time t the value is known. The right-hand rule would require that
be known at time t. In an identical fashion, the second integral is approximated as
since and
are identical in distribution, where Z is a standard normal variable.(
=
=
). Hence, Euler discretization of (2) is
(3)
The Black-Scholes stock price dynamics under the risk neutral measure are
(4) OR
An application of Equation (3) produces Euler discretization for the BlackScholes model
(5)
: Constant riskless short rate
==>
######
=
A process S is said to follow a geometric Brownian motion with constant volatility σ and constant drift μ if it satisfies the stochastic differential equation OR
, for a Brownian motion B. Applying Itô's lemma伊藤引理 with
gives正常的全微分
, d_Y = d_St
==
(6)
######
Alternatively, we can generate log-stock prices, and exponentiate the result. By lemma ln
follows the process
(6)
Euler discretization via Equation (3) produces
so that (7) OR
where
###########################
Consider now the Black-Scholes-Merton model in its dynamic form, as described by the stochastic differential equation (SDE, 随机微分方程) in Equation 12-2. Here, is a standard Brownian motion. The SDE is called a geometric Brownian motion. The values of
are log-normally distributed and the (marginal) returns
normally.
Equation 12-2. Stochastic differential equation in Black-Scholes-Merton setup
The SDE in Equation 12-2 can be discretized exactly by an Euler scheme. Such a scheme is presented in Equation 12-3, with being the fixed discretization interval and
being a standard normally distributed random variable.
Equation 12-3. Simulating index levels dynamically in Black-Scholes-Merton setup
- s0 = 100 # initial value
- T=2.0 # in years
- r = 0.05 # constant short rate
- sigma=0.25 # constant volatility
- Z=10000 # number of random draws
-
- I =10000 #The number of paths to be simulated.
- M = 50 #The number of time intervals for the discretization.
- dt = T/M #The length of the time interval in year fractions.
- S = np.zeros((M+1, I) )
- S[0] = s0 #first row #The initial values for the initial point in time t=0.
-
-
- #ST1 = s0 * np.exp( (r-0.5 * sigma **2 )* T + sigma*np.sqrt(T) * npr.standard_normal(Z) )
-
- #ST2 = s0 * npr.lognormal( (r-0.5* sigma**2)*T, #mu==mean of lognormal Distribution
- # sigma*np.sqrt(T), #standard deviation of lognormal Distribution
- # size=Z
- # )
-
- for t in range(1, M+1): #a stochastic process is a sequence of random variables
- S[t] = S[t-1] * np.exp((r-0.5*sigma**2)*dt + sigma*np.sqrt(dt)*npr.standard_normal(Z) )
- # The simulation via semivectorized expression;
- # the loop is over the points in time starting at t=1 and ending at t=T.
-
- plt.figure( figsize=(10,6) )
- plt.hist(S[-1], bins=50) # the option price at maturity
- plt.xlabel('index level')
- plt.ylabel('frequency')

Figure 12-5. Dynamically simulated geometric Brownian motion at maturity(S[-1])
Following is a comparison of the statistics resulting from the dynamic simulation as well as from the static simulation(ST2).
print_statistics(S[-1], ST2)
Figure 12-6 shows the first 10 simulated paths:
- #Using the dynamic simulation approach allows us to visualize paths as displayed
- plt.figure(figsize=(10,6))
- plt.plot(S[:, :10], lw=1.5)
- plt.xlabel('time')
- plt.ylabel('index level')
Figure 12-6. Dynamically simulated geometric Brownian motion paths
Using the dynamic simulation approach not only allows us to visualize paths as displayed in Figure 12-6, but also to value估值
Itô's lemma can be used to derive the Black–Scholes equation for an option. Suppose a stock price follows a geometric Brownian motion given by the stochastic differential equation dS = S(σdB + μ dt) <==. Then, if the value of an option at time t is f(t, St), Itô's lemma gives
...
(26)==>https://www.zhihu.com/question/36828182?sort=created
The term represents the change in (Option) value in time dt of the trading strategy consisting of holding an amount
(or
)of the stock
################################################################
Finite difference schemes有限差分方案 are very much similar to trinomial tree option pricing, where each node is dependent on three other nodes with an up movement, a down movement, and a flat movement. The motivation behind the finite differencing is the application of the Black-Scholes Partial Differential Equation (PDE 偏微分方程) framework (involving functions and their partial derivatives偏导数), where
The finite difference technique tends to converge faster than lattices and approximates complex exotic options very well.
To solve a PDE by finite differences working backward in time, a discrete-time grid of size M by N is set up to reflect asset prices over a course of time建立一个大小为 M×N 的离散时间网格以反映一段时间内的资产价格, so that S and t take on the following values at each point on the grid:
The lattice is generated by dividing the time between today and expiry into M equal periods, and the underlying price into N equal levels. This leads to a grid with M+1 time points and N+1 price levels. The grid is typically chosen so that the current price of the underlying asset lies close to the middle of the N equal price levels of the grid.
It follows that by grid notation,.
is a suitably large asset price that cannot be reached by the maturity time, T. Thus dS and dt are intervals between each node in the grid, incremented by price and time, respectively.(Note that time is considered to run from 0 (today) to M*Δt=M*dt=T (at expiry). Alternative derivations may reverse this order and consider time counting down towards expiry.)
The terminal condition at expiration time T for every value of S is
The grid traverses backward from the terminal conditions, complying with the PDE while adhering to the boundary conditions of the grid(遵守 PDE,同时遵守网格的边界条件), such as the payoff from an earlier exercise.
The boundary conditions are defined values at the extreme ends of the nodes, where i=0 and i=N for every time at t. Values at the boundaries are used to calculate the values of all other lattice nodes iteratively using the PDE.
A visual representation of the grid is given in the following diagram. As i and j increase from the top-left corner of the grid, the price S tends toward (the maximum price possible) at the bottom-right corner of the grid:
Consider the function of one variable ƒ(x) shown in Figure 1.
Then ƒ′(x), the derivative of the function at x, can be approximated in many ways. The most common are called the forward, backward and central approximation, all of which are drawn and indicated on Figure 1.
A number of ways to approximate the PDE are as follows:
Once we have the boundary conditions set up, we can now apply an iterative approach using the explicit, implicit, or Crank-Nicolson method.
The explicit method for approximatingƒ(S,t)==>is given by the following equation:
Discretizing the Black-Scholes-Merton PDE :
For the explicit method the Black-Scholes-Merton partial differential equation,
is discretized using the following formulae:
where the indices i and j represent nodes on the pricing grid.
Substituting these approximations into the PDE gives,
Here, we can see that the 1st difference is the backward difference with respect to t, the 2nd difference is the central difference with respect to S, and the 3rd difference is the second-order difference with respect to S. When we rearrange the terms, we get the following equation:
By algebraic rearranging, we end up with the formula we need to implement to price the options,
Where:
Then:
The iterative approach of the explicit method can be visually represented by the following diagram:ƒ(S,t)
can be stated in a number of equivalent ways, most tersely as:
where
Using spot price S instead of forward price F yields: <==
<==
Rearranging the terms yields a different interpretation:
In this case the left-hand side is a fiduciary call, which is long a call and enough cash (or bonds) to pay the strike price if the call is exercised,
while the right-hand side is a protective put, which is long a put and the asset, so the asset can be sold for the strike price if the spot is below strike at expiry.
Both sides have payoff max(S(T), K) at expiry (i.e., at least the strike price, or the value of the asset if more), which gives another way of proving or interpreting put–call parity.
In more detail, this original equation can be stated as:
Note that the right-hand side of the equation is also the price of buying a forward contract on the stock with delivery price K. Thus one way to read the equation is that a portfolio that is long a call and short a put is the same as being long a forward. In particular, if the underlying is not tradeable but there exists forwards on it, we can replace the right-hand-side expression by the price of a forward.
If the bond interest rate, r, is assumed to be constant then
Note: r refers to the force of interest, which is approximately equal to the effective annual rate for small interest rates. However, one should take care with the approximation, especially with larger rates and larger time periods. To find r exactly, use r=ln(1+i), where i is the effective annual interest rate.
When valuing European options written on stocks with known dividends that will be paid out during the life of the option, the formula becomes:
where D(t) represents the total value of the dividends from one stock share to be paid out over the remaining life of the options, discounted to present value. We can rewrite the equation as:
and note that the right-hand side is the price of a forward contract on the stock with delivery price K, as before.
####################################
A stock price is currently $50. Over each of the next two(n_t=2) three-month( t=3/12, T=6/12) years) periods it is expected to go up by 6%(u=1+0.06=1.06) or down by 5%(d=1-0.05=0.95). The risk-free interest rate is 5% per annum with continuous compounding. What is the value of a six-month European call option with a strike price of $51?
Figure S12.1
A tree describing the behavior of the stock price is shown in Figure S12.1. The risk-neutral probability of an up move, p(), is given by
There is a payoff from the option of 56.18-51=5.18 ( <== max(S − K, 0) ) for the highest final node (which corresponds to two up moves) zero in all other cases. The value of the option is therefore
For the situation considered in Problem 12.12, what is the value of a six-month European put option with a strike price of $51? Verify that the European call and European put prices satisfy put–call parity. If the put option were American, would it ever be optimal to exercise it early at any of the nodes on the tree?Figure S12.2
The tree for valuing the put option is shown in Figure S12.2. We get
a payoff of 51-50.35=0.65 (<==max(K − S, 0) ) if the middle final node is reached
and a payoff of 51-45.125=5.875 (<==max(K − S, 0) ) if the lowest final node is reached. The value of the option is therefore
The value of the put plus the stock price is from Problem 12.12
=
The value of the call plus the present value of the strike price is
==> the discount factor D=
==>
This verifies that put–call parity holds
####################################
Since we will be writing the explicit, implicit, and Crank-Nicolson methods of finite differences in Python, let's write a base class that inherits the common properties and functions of all three methods.
We will create a class called FiniteDifferences that accepts and assigns all the required parameters in the __init__ constructor method. The price() method is the entry point for invoking the specific finite difference scheme implementation, and will invoke these methods in the following order: setup_boundary_conditions(), setup_coefficients(), traverse_grid(), and interpolate(). These methods are explained as follows:
All of these methods are abstract methods that can be implemented by the derived classes. An exception type of NotImplementedError will be thrown should we forget to implement these methods.
The base class with the mandatory methods should look like this:https://www.semanticscholar.org/paper/Excel-implementation-of-finite-difference-methods-Kyng-Purcal/cd18bc626774407cc692e562f1f4cbe7143942ca/figure/5
Abstract base classes (ABCs) provide a way to define interfaces for a class. The @abstractmethod() decorator declares abstract methods that child classes should implement. Unlike Java's abstract methods, these methods may have an implementation and may be called via the super() mechanism from the class that overrides it.
In addition to these methods, we would need to define dS and dt , the change in S per unit time, and the change in T per iteration, respectively.
Finally, add the price() method as the entry point that shows the steps in calling our discussed abstract methods:
- from abc import ABCMeta, abstractmethod
- import numpy as np
-
- """
- Base class for sharing attributes and functions of FD
- """
-
- class FiniteDifferences( object ):
- def __init__( self, S0, K, r=0.05, T=1,
- sigma=0, Smax=1, M=1, N=1, is_put=False
- ):
- self.S0 = S0
- self.K = K
- self.r = r
- self.T = T
- self.sigma = sigma
- self.Smax = Smax
- # N: number of time steps
- self.M, self.N = M, N # stock price levels X time points
- self.is_call = not is_put
-
- self.i_values = np.arange( self.M ) # Smin~Smax: price level indices
- self.j_values = np.arange( self.N ) # 0~T: time point indices
-
- self.boundary_conds = np.linspace( 0, Smax, self.M+1 )
- self.grid = np.zeros( shape=(self.M+1, self.N+1) )
- """
- https://numpy.org/doc/stable/reference/generated/numpy.linspace.html
- numpy.linspace( start, stop, num=50, endpoint=True,
- retstep=False, dtype=None, axis=0 )
- np.linspace(2.0, 3.0, num=5)
- array([2. , 2.25, 2.5 , 2.75, 3. ])
- """
- @property
- def dS( self ):
- return self.Smax/float(self.M)
-
- @property
- def dt( self ):
- return self.T/float(self.N)
-
- @abstractmethod
- def setup_boundary_conditions( self ):
- raise NotImplementedError('Implementation required: boundary_conditions!')
-
- @abstractmethod
- def setup_coefficients( self ):
- raise NotImplementedError('Implementation required: coefficients!')
-
- @abstractmethod
- def traverse_grid( self ):
- # Iterate the grid backwards in time
- raise NotImplementedError('Implementation required: traverse_grid!')
-
- @abstractmethod
- def interpolate( self ):
- """
- Use piecewise linear interpolation on the initial
- grid column to get the closest price at S0.
-
- numpy.interp(x, xp, fp, left=None, right=None, period=None)
- left: Value to return for x < xp[0], default is fp[0]
- right: Value to return for x > xp[-1], default is fp[-1]
- One-dimensional linear interpolation for monotonically
- increasing sample points.
- Returns the one-dimensional piecewise linear interpolant to
- a function with given discrete data points (xp, fp),
- evaluated at x.
- xp = [1, 2, 3]
- fp = [3, 2, 0]
- np.interp(2.5, xp, fp) ==> 1.0
-
- self.boundary_conds = np.linspace( 0, Smax, self.M+1 )
- # grid : M+1 stock price levels X N+1 time points
- """
- # self.boundary_conds = np.linspace( 0, Smax, self.M+1 )
- # self.grid = np.zeros( shape=(self.M+1, self.N+1) )
- return np.interp( self.S0,
- self.boundary_conds, self.grid[:,0]
- )# lookup grid to return option present value
- def price( self ):
- self.setup_boundary_conditions()
- self.setup_coefficients()
- self.traverse_grid()
- return self.interpolate()

The Python implementation of finite differences by using the explicit method is given in the following FDExplicitEu class, which inherits from the FiniteDifferences class and overrides the required implementation methods (with @abstractmethod() decorator):
- import numpy as np
-
- # Explicit method of Finite Differences
-
- class FDExplicitEu( FiniteDifferences ):
- def setup_boundary_conditions(self):
- print('The boundary(Stock price) at expiry:', self.boundary_conds)
- print('strike price K:',self.K)
-
- if self.is_call:
- # The terminal condition at expiration time T for every value of S :
- # max(S − K, 0) for a call option
- # max(K − S, 0) for a put option
- # self.boundary_conds = np.linspace( 0, Smax, self.M+1 )
- self.grid[:,-1] = np.maximum( 0, self.boundary_conds-self.K )
- print("Payoff=max(boundary_conds-K,0):\n", self.grid )
-
- # N: number of time steps
- # self.j_values = np.arange( self.N ) # 0~T: time point indices
- # self.dt = self.T/float(self.N)
- # grid: (M+1) stock price levels X (N+1) time points
- # 1st time step: payoff *
- self.grid[-1, :-1] = self.Smax-self.K*\
- np.exp( -self.r*self.dt*(self.N-self.j_values) )
- print('Dirichlet boundary condition for call option',
- self.grid
- )
-
- else:
- self.grid[:,-1] = np.maximum( 0, self.K-self.boundary_conds )
- print("Payoff=max(K-boundary_conds,0):\n", self.grid )
-
- self.grid[0, :-1] = self.K*\
- np.exp( -self.r*self.dt*(self.N-self.j_values) )
- print('Dirichlet boundary condition for put option',
- self.grid
- )
-
- def setup_coefficients(self):
- # self.i_values = np.arange( self.M ) # Smin~Smax: price level indices
- self.a = 0.5*self.dt*( (self.sigma**2)*(self.i_values**2)-\
- self.r*self.i_values
- )
-
- self.b = 1 - self.dt*( (self.sigma**2)*(self.i_values**2)+\
- self.r
- )
- self.c = 0.5*self.dt*( (self.sigma**2)*(self.i_values**2)+\
- self.r*self.i_values
- )
- def traverse_grid(self):
- for j in reversed(self.j_values):
- for i in range(self.M)[2:]:# i>=2 since S_min=S_0 = 0
- self.grid[i,j] = \
- self.a[i]*self.grid[i-1,j+1] +\
- self.b[i]*self.grid[i,j+1] + \
- self.c[i]*self.grid[i+1,j+1]
- print( 'Option price grid', self.grid)

On completion of traversing the grid structure, the 1st column contains the present value of the initial asset prices at t=0. The interp function of NumPy is used to perform a linear interpolation to approximate the option value.
Besides using linear interpolation as the most common choice for the interpolation method, the other methods such as the spline or cubic may be used to approximate the option value.
- option = FDExplicitEu(30, 30, r=0.1, T=0.75,
- sigma=0.4, Smax=60, M=6, N=3, is_put=True)
- print(option.price())
The boundary(Stock price) at expiry: [ 0. 10. 20. 30. 40. 50. 60.]
self.boundary_conds = np.linspace( 0, Smax, self.M+1 ), t=0
strike price K: 30
self.grid[:,-1] = np.maximum( 0, self.K-self.boundary_conds )
Payoff=max(K-boundary_conds,0):
[[ 0. 0. 0. 30.]
[ 0. 0. 0. 20.]
[ 0. 0. 0. 10.]
[ 0. 0. 0. 0.]
[ 0. 0. 0. 0.]
[ 0. 0. 0. 0.]
[ 0. 0. 0. 0.]
]
Dirichlet boundary condition for put option
self.grid[0, :-1] = self.K*np.exp( -self.r*self.dt*(self.N-self.j_values) )
[[27.8323 28.5369 29.2593 30. ]
[ 0. 0. 0. 20. ]
[ 0. 0. 0. 10. ]
[ 0. 0. 0. 0. ]
[ 0. 0. 0. 0. ]
[ 0. 0. 0. 0. ]
[ 0. 0. 0. 0. ]
]
Option price grid
[[27.8323 28.5369 29.2593 30. ]
[ 0. 0. 0. 20. ]
[ 6.4964 7.6884 9.25 10. ]
[ 2.5289 2.1945 1.425 0. ]
[ 0.7214 0.3848 0. 0. ]
[ 0.1683 0. 0. 0. ]
[ 0. 0. 0. 0. ]
]
option. price() ==>put option price: 2.5288940625
- option = FDExplicitEu(30, 30, r=0.1, T=0.75,
- sigma=0.4, Smax=60, M=6, N=3, is_put=False)
- print("call option price:",option.price())
The boundary(Stock price) at expiry: [ 0. 10. 20. 30. 40. 50. 60.]
self.boundary_conds = np.linspace( 0, Smax, self.M+1 ), t=0
strike price K: 30
self.grid[:,-1] = np.maximum( 0, self.boundary_conds-self.K )
Payoff=max(boundary_conds-K,0):
[[ 0. 0. 0. 0.]
[ 0. 0. 0. 0.]
[ 0. 0. 0. 0.]
[ 0. 0. 0. 0.]
[ 0. 0. 0. 10.]
[ 0. 0. 0. 20.]
[ 0. 0. 0. 30.]]
Dirichlet boundary condition for call option
[[ 0. 0. 0. 0. ]
[ 0. 0. 0. 0. ]
[ 0. 0. 0. 0. ]
[ 0. 0. 0. 0. ]
[ 0. 0. 0. 10. ]
[ 0. 0. 0. 20. ]
[32.1677 31.4631 30.7407 30. ]]
self.grid[-1, :-1] = self.Smax-self.K* np.exp( -self.r*self.dt*(self.N-self.j_values) )
Option price grid
[[ 0. 0. 0. 0. ]
[ 0. 0. 0. 0. ]
[ 0.5721 0.2284 0. 0. ]
[ 4.874 3.6758 2.175 0. ]
[12.9137 11.866 10.75 10. ]
[22.3525 21.476 20.75 20. ]
[32.1677 31.4631 30.7407 30. ]]
call option price: 4.873984687
Consider the example of a European put option. The underlying stock price is $50 with a volatility of 40 percent(annualized volatility of the underlying stock : sigma=0.4). The strike price of the put option is $50 with an expiration time of 5 months. The risk-free rate is 10 percent.
- option = FDExplicitEu(50, 50, r=0.1, T=5./12.,
- sigma=0.4, Smax=100, M=100, N=1000, is_put=True)
- print(option.price())
What happens when other values of M(stock price levels) and N(number of time steps) are chosen improperly?
- option = FDExplicitEu(50, 50, r=0.1, T=5./12.,
- sigma=0.4, Smax=100, M=80, N=100, is_put=True)
- print(option.price())
when other values of M and N are chosen improperly, it appears that the explicit method of the finite difference scheme suffers from instability problems.
The instability problem of the explicit method can be overcome using the forward difference with respect to time. The implicit method for approximatingƒ(S,t)==> is given by the following equation:
For the implicit method the Black-Scholes-Merton partial differential equation,is discretized using the following formulae
where the indices i and j represent nodes on the pricing grid.
Substituting these approximations into the PDE gives,
Here, it can be seen that the only difference between the implicit and explicit approximating scheme lies in the first difference, where the forward difference with respect to t is used in the implicit scheme. When we rearrange the terms, we get the following expression:
Where:
Here:
The iterative approach of the implicit scheme can be visually represented with the following diagram: ƒ(S,t)
From the preceding diagram, we can note that values of j+1 need to be computed before they can be used in the next iterative step, as the grid traverses backward. In the implicit scheme, the grid can be thought of as representing a system of linear equations at each iteration, as follows:
By rearranging the terms, we get the following equation:
The linear system of equations can be represented in the form of Ax = B, where we want to solve values of x in each iteration. Since the matrix A is tri-diagonal三对角矩阵, we can use the LU factorization, where A=LU, for faster computation. Remember that we solved the linear system of equations using LU decomposition(or also known as lower-upper factorization) in https://blog.csdn.net/Linli522362242/article/details/125546725, The Importance of Linearity in Finance.
The LU decomposition, or also known as lower-upper factorization, is one of the methods that solve square systems of linear equations. As its name implies, the LU factorization decomposes the A matrix into a product of two matrices: a lower triangular matrix, L, and an upper triangular matrix, U. The decomposition can be represented as follows:
We will look at a simple example and write out the row operations.
L : row operation and constraint : column_index= row_index
row_index+1=3 row_index+1=2https://blog.csdn.net/Linli522362242/article/details/125546725
==>(
==>
)==> vector y
==>(
==>
)==>vector x
The Python implementation of the implicit scheme is given in the following FDImplicitEu class. We can inherit the implementation of the explicit method from the FDExplicitEu class we discussed earlier and override the necessary methods of interest, namely, the setup_coefficients and traverse_grid methods:
- import numpy as np
- import scipy.linalg as linalg
-
- # Explicit method of Finite Differences
-
- class FDImplicitEu( FDExplicitEu ):
- def setup_coefficients( self ):
- # self.i_values = np.arange( self.M ) # Smin~Smax: price level indices
- self.a = 0.5*self.dt*( self.r*self.i_values -\
- (self.sigma**2)*(self.i_values**2)
- )
-
- self.b = 1 + self.dt*( self.r +\
- (self.sigma**2)*(self.i_values**2)
- )
- self.c =-0.5*self.dt*( self.r*self.i_values +\
- (self.sigma**2)*(self.i_values**2)
- )
- # since S_min=S_0 = grid[0] : Boundary conditions
- # grid: (M+1) stock price levels X (N+1) time points
- self.coeffs = np.diag( self.a[2:self.M],
- -1 # k<0 for diagonals below the main diagonal.
- )+\
- np.diag( self.b[1:self.M],
- 0 # default k=0 for diagonals
- )+\
- np.diag( self.c[1:self.M-1],
- 1 # k>0 for diagonals above the main diagonal
- )
- def traverse_grid( self ):
- """ Solve using linear systems of equations """
- P, L, U = linalg.lu( self.coeffs )
-
- aux = np.zeros( self.M-1 ) # 1~M-1
-
- for j in reversed( range(self.N) ): # 0~t~N-1 ==> N-1~t~0
- aux[0] = np.dot( -self.a[1], self.grid[0,j] )
- aux[-1] = np.dot( -self.c[-1], self.grid[-1, j] )
-
- # L@y = B ==>y
- y = linalg.solve( L,
- self.grid[1:self.M, j+1] + aux
- )
- # U@x = y ==>x
- x = linalg.solve( U,
- y
- )
- self.grid[1:self.M, j] = x
- print( 'Option price grid', self.grid)

- option = FDImplicitEu(30, 30, r=0.1, T=0.75,
- sigma=0.4, Smax=60, M=6, N=3, is_put=True)
- print("put option price:",option.price())
The boundary(Stock price) at expiry: [ 0. 10. 20. 30. 40. 50. 60.]
self.boundary_conds = np.linspace( 0, Smax, self.M+1 ), t=0
strike price K: 30
self.grid[:,-1] = np.maximum( 0, self.K-self.boundary_conds )
Payoff=max(K-boundary_conds,0):
[[ 0. 0. 0. 30.]
[ 0. 0. 0. 20.]
[ 0. 0. 0. 10.]
[ 0. 0. 0. 0.]
[ 0. 0. 0. 0.]
[ 0. 0. 0. 0.]
[ 0. 0. 0. 0.]
]
Dirichlet boundary condition for put option
self.grid[0, :-1] = self.K*np.exp( -self.r*self.dt*(self.N-self.j_values) )
[[27.8323 28.5369 29.2593 30. ]
[ 0. 0. 0. 20. ]
[ 0. 0. 0. 10. ]
[ 0. 0. 0. 0. ]
[ 0. 0. 0. 0. ]
[ 0. 0. 0. 0. ]
[ 0. 0. 0. 0. ]
]
Option price grid
[[27.8323 28.5369 29.2593 30. ]
[17.8959 18.5711 19.2729 20. ]
[ 8.5912 8.9637 9.4216 10. ]
[ 2.2156 1.7045 0.996 0. ]
[ 0.6429 0.4016 0.1697 0. ]
[ 0.1907 0.1049 0.0367 0. ]
[ 0. 0. 0. 0. ]]
put option price: 2.2155510303909964
- print('coefficients:')
- print('a:',option.a)
- print('b:',option.b)
- print('c:',option.c)
- option = FDImplicitEu(30, 30, r=0.1, T=0.75,
- sigma=0.4, Smax=60, M=6, N=3, is_put=False)
- print("call option price:",option.price())
The boundary(Stock price) at expiry: [ 0. 10. 20. 30. 40. 50. 60.]
self.boundary_conds = np.linspace( 0, Smax, self.M+1 ), t=0
strike price K: 30
self.grid[:,-1] = np.maximum( 0, self.boundary_conds-self.K )
Payoff=max(boundary_conds-K,0):
[[ 0. 0. 0. 0.]
[ 0. 0. 0. 0.]
[ 0. 0. 0. 0.]
[ 0. 0. 0. 0.]
[ 0. 0. 0. 10.]
[ 0. 0. 0. 20.]
[ 0. 0. 0. 30.]]
Dirichlet boundary condition for call option
[[ 0. 0. 0. 0. ]
[ 0. 0. 0. 0. ]
[ 0. 0. 0. 0. ]
[ 0. 0. 0. 0. ]
[ 0. 0. 0. 10. ]
[ 0. 0. 0. 20. ]
[32.1677 31.4631 30.7407 30. ]]
self.grid[-1, :-1] = self.Smax-self.K* np.exp( -self.r*self.dt*(self.N-self.j_values) )
Option price grid
[[0.0000e+00 0.0000e+00 0.0000e+00 0.0000e+00]
[3.8233e-02 1.6884e-02 4.6786e-03 0.0000e+00]
[7.3338e-01 4.0931e-01 1.5331e-01 0.0000e+00]
[4.3584e+00 3.1504e+00 1.7278e+00 0.0000e+00]
[1.2789e+01 1.1849e+01 1.0902e+01 1.0000e+01]
[2.2344e+01 2.1557e+01 2.0771e+01 2.0000e+01]
[3.2168e+01 3.1463e+01 3.0741e+01 3.0000e+01]]
call option price: 4.3584400028969705
Using the same example as the explicit scheme, we can price the European put options using the implicit scheme:
- option = FDImplicitEu(50, 50, r=0.1, T=5./12.,
- sigma=0.4, Smax=100, M=100, N=1000, is_put=True)
- print(option.price())
- option = FDImplicitEu(50, 50, r=0.1, T=5./12.,
- sigma=0.4, Smax=100, M=80, N=100, is_put=True)
- print(option.price())
Given the current parameters and input data, we can see that there are no stability issues with the implicit scheme.
- option = FDImplicitEu(140, 150, r=0.04, T=6/12,
- sigma=0.25, Smax=200, M=200, N=200, is_put=False)
- print("call option price:",option.price())
- option = FDImplicitEu(140, 150, r=0.04, T=6/12,
- sigma=0.25, Smax=200, M=200, N=200, is_put=True)
- print("put option price:",option.price())
Another way of avoiding the instability issue, as seen in the explicit method, is to use the Crank-Nicolson method. The Crank-Nicolson method converges much more quickly using a combination of
the explicitand implicit
methods, taking the average of both. This leads us to the following equation:
and
==>j向左偏移1个单位==>
Crank-Nicolson method:
This equation can also be rewritten as follows:
Where: OR
The iterative approach of the implicit scheme can be visually represented with the following diagram:vs
We can treat the equations as a system of linear equations in a matrix form: https://www.goddardconsulting.ca/option-pricing-finite-diff-crank-nicolson.htmlhttps://quintus-zhang.github.io/post/on_pricing_options_with_finite_difference_methods/==> f_0=0 then j+1 ==> M_1 @ f_j = M_2 @ f_j+1
Where:
We can solve for the matrix M on every iterative procedure.
The Python implementation of the Crank-Nicolson method is given in the following FDCnEu class, which inherits from the FDExplicitEu class and overrides only the setup_coefficients and traverse_grid methods:
- import numpy as np
- import scipy.linalg as linalg
-
- # Crank-Nicolson method of Finite Differences
- class FDCnEu( FDExplicitEu ):
- def setup_coefficients(self):
- self.alpha = 0.25 * self.dt * ( (self.sigma**2)*(self.i_values**2) -\
- self.r * self.i_values
- )
- self.beta = -0.5 * self.dt * ( (self.sigma**2)*(self.i_values**2) +\
- self.r
- )
- self.gamma = 0.25 * self.dt * ( (self.sigma**2)*(self.i_values**2) +\
- self.r * self.i_values
- )
- # since S_min=S_0 = grid[0] : Boundary conditions
- # grid: (M+1) stock price levels X (N+1) time points
- self.M1 = -np.diag( self.alpha[2:self.M],
- -1 # k<0 for diagonals below the main diagonal.
- )+\
- np.diag( 1-self.beta[1:self.M],
- 0 # default k=0 for diagonals
- )-\
- np.diag( self.gamma[1:self.M-1],
- 1 # k>0 for diagonals above the main diagonal
- )
- self.M2 = np.diag( self.alpha[2:self.M],
- -1 # k<0 for diagonals below the main diagonal.
- )+\
- np.diag( 1+self.beta[1:self.M],
- 0 # default k=0 for diagonals
- )+\
- np.diag( self.gamma[1:self.M-1],
- 1 # k>0 for diagonals above the main diagonal
- )
- def traverse_grid(self):
- """ Solve using linear systems of equations """
- P,L,U = linalg.lu( self.M1 )
- for j in reversed( range(self.N) ): # 0~t~N-1 ==> N-1~t~0
- # L @ y = B ==>
- # L @ y = M_2 @ f_j+1 ==> y
- y = linalg.solve( L, # M_1 @ f_j = M_2 @ f_j+1
- np.dot( self.M2, self.grid[1:self.M, j+1] )
- )
- # U @ x = y ==>x
- x = linalg.solve( U,
- y
- )
- self.grid[1:self.M, j] = x
-
- print( 'Option price grid', self.grid)

- option = FDCnEu(140, 150, r=0.04, T=6/12,
- sigma=0.25, Smax=200, M=200, N=2, is_put=False)
- print("Call option price:",option.price())
- option = FDCnEu(140, 150, r=0.04, T=6/12,
- sigma=0.25, Smax=200, M=200, N=2, is_put=True)
- print("Put option price:",option.price())
Using the same examples that we used with the explicit and implicit methods, we can price a European put option using the Crank-Nicolson method for different time point intervals:
- option = FDCnEu(50, 50, r=0.1, T=5./12.,
- sigma=0.4, Smax=100, M=100, N=1000, is_put=True)
- print(option.price())
- option = FDCnEu(50, 50, r=0.1, T=5./12.,
- sigma=0.4, Smax=100, M=80, N=100, is_put=True)
- print(option.price())
From the observed values, the Crank-Nicolson method
When discussing effectiveness of different finite difference methods, we should consider three fundamental properties, which are consistency, stability, and convergence. Before that, we have to notice that though numerical method is good for solving PDE, it also brings the truncation error. Two important sources of error are the truncation error in discretization of stock price and time space.
Finite differences are especially useful in pricing exotic options. The nature of the option will dictate(/ ˈdɪkteɪt 规定) the specifications of the boundary conditions.
In this section, we will take a look at an example of pricing a down-and-out barrier option with the Crank-Nicolson method of finite differences. Due to its relative complexity, other analytical methods, such as Monte Carlo methods, are usually employed in favor of finite difference schemes.
Let's take a look at an example of a down-and-out option. At any time during the life of the option, should the underlying asset price fall below an S barrier barrier price, the option is considered worthless价格低于某个障碍价格时就自动到期无效. Since, in the grid, the finite difference scheme represents all the possible price points, we only need to consider nodes with the following price range:
We can then set up the boundary conditions as follows:
Let's create a class named FDCnDo that inherits from the FDCnEu class we discussed earlier. We will take into account the barrier price in the constructor method, while leaving the rest of the Crank-Nicolson implementation in the FDCnEu class unchanged:
- import numpy as np
-
- # Price a down-and-out option by the
- # Crank-Nicolson method of finite differences.
-
- class FDCnDo( FDCnEu ):
- def __init__( self, S0, K, r=0.05, T=1, sigma=0,
- Sbarrier=0, Smax=1, M=1, N=1, is_put=False
- ):
- # inherit from the 'FDCnEu' class
- # python 2.x
- super( FDCnDo, self ).__init__(
- S0, K, r, T, sigma,
- Smax, M, N, is_put
- )
- self.barrier = Sbarrier
- """
- https://numpy.org/doc/stable/reference/generated/numpy.linspace.html
- numpy.linspace( start, stop, num=50, endpoint=True,
- retstep=False, dtype=None, axis=0 )
- np.linspace(2.0, 3.0, num=5)
- array([2. , 2.25, 2.5 , 2.75, 3. ])
- """
- self.boundary_conds = np.linspace( Sbarrier, Smax, M+1 )
- self.i_values = self.boundary_conds/self.dS
-
- @property
- def dS(self):
- return (self.Smax-self.barrier)/float(self.M)

Let's consider an example of a down-and-out option. The underlying stock price is $50 with a volatility of 40 percent. The strike price of the option is $50 with an expiration time of five months(5/12). The risk-free rate is 10 percent. The barrier price is $40.
We can price a call option and a put down-and-out option with Smax as 100, M as 120, and N as 500
- option = FDCnDo(50, 50, r=0.1, T=5./12., sigma=0.4,
- Sbarrier=40, Smax=100, M=120, N=500)
- print( 'Call option price:', option.price() )
- option = FDCnDo(50, 50, r=0.1, T=5./12., sigma=0.4,
- Sbarrier=40, Smax=100, M=120, N=500, is_put=True)
- print( 'Put option price:', option.price() )
The prices of the down-and-out call and put options are $5.4916 and $0.5414, respectively.
So far, we have priced European options and exotic options. Due to the probability of an early exercise nature in American options, pricing such options is less straightforward. An iterative procedure is required in the implicit Crank-Nicolson method, where the payoffs from earlier exercises in the current period take into account the payoffs of an earlier exercise in the prior period. The Gauss-Siedel iterative method(https://blog.csdn.net/Linli522362242/article/details/125546725) is proposed in the pricing of American options in the Crank-Nicolson method.
Recall that in https://blog.csdn.net/Linli522362242/article/details/125546725, The Importance of Linearity in Finance, we covered the Gauss-Siedel method of solving systems of linear equations in the form of Ax=B. Here, the matrix A is decomposed into A=L+U, where L is a lower triangular matrix and U is an upper triangular matrix. Let's take a look at an example of a 4 x 4 matrix, A:
The solution is then obtained iteratively, as follows:
We can adapt the Gauss-Siedel method to our Crank-Nicolson implementation as follows:
This equation satisfies the early exercise privilege equation:根据无套利原理,在(S,t)空间中任意一点的期权价格不能低于其内在价值(即立刻行使权益的所得收益).对于一般的美式期权,意味着==>在计算得到
后,我们可以检测提前行使权益的概率
Let's create a class named FDCnAm that inherits from the FDCnEu class, which is the Crank-Nicolson method's counterpart for pricing European options. The setup_coefficients method may be reused, while overriding all other methods for the inclusion of payoffs from an earlier exercise, if any.
The constructor __init__() and the setup_boundary_conditions() methods are given in the FDCnAm class:
- import numpy as np
- import sys
-
- """
- Price an American option by the Crank-Nicolson method
- """
-
- class FDCnAM( FDCnEu ):
- def __init__(self, S0, K, r=0.05, T=1, sigma=0,
- Smax=1, M=1, N=1,
- omega=1, tol=0, is_put=False):
- # python 2.x
- # super( FDCnAm, self ).__init__(S0, K, r=r, T=T, sigma=sigma,
- # Smax=Smax, M=M, N=N,
- # is_put=is_put)
- # python 3.x
- super().__init__(S0, K, r=r, T=T, sigma=sigma,
- Smax=Smax, M=M, N=N,
- is_put=is_put)
- self.omega = omega
- self.tol = tol
- self.i_values = np.arange( self.M+1 ) # Smin~Smax: price level indices
- self.j_values = np.arange( self.N+1 ) # 0~T: time point indices
-
- def setup_boundary_conditions( self ):
- if self.is_call:
- # self.boundary_conds = np.linspace( 0, Smax, self.M+1 )
- # so self.boundary_conds[0] == 0s
- self.payoffs = np.maximum( 0, self.boundary_conds[1:self.M]-self.K )
- else:
- self.payoffs = np.maximum( 0, self.K - self.boundary_conds[1:self.M] )
-
- self.past_values = self.payoffs
- self.boundary_values = self.K * np.exp( -self.r*self.dt*(self.N-self.j_values) )

At the start boundaries where the index is 0,the payoffs are calculated with the alpha values omitted. Implement the calculate_payoff_start_boundary() method inside the class:
- def calculate_payoff_start_boundary( self, rhs, old_values ):
- # self.i_values = np.arange( self.M ) # Smin~Smax: price level indices
- # self.j_values = np.arange( self.N ) # 0~T: time point indices
- # def setup_coefficients(self):
- # self.alpha = 0.25 * self.dt * ( (self.sigma**2)*(self.i_values**2) -\
- # self.r * self.i_values
- # )
- # self.beta = -0.5 * self.dt * ( (self.sigma**2)*(self.i_values**2) +\
- # self.r
- # )
- # self.gamma = 0.25 * self.dt * ( (self.sigma**2)*(self.i_values**2) +\
- # self.r * self.i_values
- # )
- # S[0]~S[M] stock price levels, Smin~Smax
- # old_value.shape : M-1, old_value[0]~old_value[M-2] : f[1]~f[M-1] : S[1]~S[M-1]
- # rhs[0]~rhs[M-2] : f[1]~f[M-1]
- # self.beta[0]~[M-1]
- payoff = old_values[0] + self.omega/(1-self.beta[1]) *\
- ( rhs[0] -\
- ( 1-self.beta[1] ) * old_values[0] +\
- self.gamma[1] * old_values[1]
- )
- # self.payoffs : option intrinsic value
- return max( self.payoffs[0], payoff )

At the end boundary where the last index is, the payoffs are calculated with the gamma values omitted. Implement the calculate_payoff_end_boundary() method inside the class:
- def calculate_payoff_end_boundary( self, rhs, old_values, new_values ):
- # old_value.shape : M-1, 0~M stock price levels
- # f_M-1 rhs_M-1
- payoff = old_values[-1] + self.omega/(1-self.beta[-2]) *\
- ( rhs[-1] +\
- self.alpha[-2] * new_values[-2] -\
- ( 1-self.beta[-2] ) * old_values[-1]
- )
- # self.payoffs : option intrinsic value
- return max( self.payoffs[-1], payoff )
For payoffs that are not at the boundaries, the payoffs are calculated by taking into account the alpha and gamma values. Implement the calculate_payoff() method inside the class:
- def calculate_payoff( self, i, rhs, old_values, new_values ):
- # i start from 2
- payoff = old_values[i] + self.omega/(1-self.beta[i+1]) *\
- ( rhs[i] +\
- self.alpha[i+1] * new_values[i-1] -\
- ( 1-self.beta[i+1] ) * old_values[i] +\
- self.gamma[i+1] * old_values[i+1]
- )
- return max( self.payoffs[i], payoff )
Next, implement the traverse_grid() method in the same class:
The tolerance parameter is used in the Gauss-Siedel method as the convergence criterion. The omega variable is the over-relaxation parameter过松弛参数. Higher omega values provide faster convergence, but this also comes with higher possibilities of the algorithm not converging.
https://blog.csdn.net/Linli522362242/article/details/125546725
x or <==
<==
self.boundary_values = self.K * np.exp( -self.r*self.dt*(self.N-self.j_values) )
- def traverse_grid( self ):
- """ Solve using linear systems of equations """
- aux = np.zeros( self.M-1 ) # 1~M-1 ==> [0,M-2]
- new_values = np.zeros( self.M-1 )
-
- # Gauss-Siede
- for j in reversed( range(self.N) ): # T~0
- aux[0] = self.alpha[1] * ( self.boundary_values[j] +\
- self.boundary_values[j+1]
- )
- rhs = np.dot( self.M2, self.past_values ) + aux
-
- # Gauss-Siede
- old_values = np.copy( self.past_values ) # Gauss-Siede
- error = sys.float_info.max
-
- while error > self.tol:
- new_values[0] = self.calculate_payoff_start_boundary( rhs, old_values )
-
- for i in range( self.M-2 )[1:]: # S_1~S[M-3]
- new_values[i] = self.calculate_payoff( i, rhs, old_values, new_values )
-
- new_values[-1] = self.calculate_payoff_end_boundary( rhs, old_values, new_values )
-
- error = np.linalg.norm( new_values - old_values )
-
- old_values = np.copy( new_values )
- self.past_values = np.copy( new_values )
-
- self.values = np.concatenate(( [ self.boundary_values[0] ],
- new_values,
- [0]
- ), axis=0)

In each iterative procedure of the while loop, the payoffs are calculated while taking into account the start and end boundaries. Furthermore, new_values are constantly replaced with new payoff calculations based on existing( new_values[i-1] ) and previous values( old_values ).
Since the new variable, values , contains our terminal payoff values as a one-dimensional array, override the parent interpolate method to account for this change with the following code:
- def interpolate( self ):
- # Use linear interpolation on final values as 1D array
- """
- Use piecewise linear interpolation on the initial
- grid column to get the closest price at S0.
-
- numpy.interp(x, xp, fp, left=None, right=None, period=None)
- left: Value to return for x < xp[0], default is fp[0]
- right: Value to return for x > xp[-1], default is fp[-1]
- One-dimensional linear interpolation for monotonically
- increasing sample points.
- Returns the one-dimensional piecewise linear interpolant to
- a function with given discrete data points (xp, fp),
- evaluated at x.
- xp = [1, 2, 3]
- fp = [3, 2, 0]
- np.interp(2.5, xp, fp) ==> 1.0
-
- self.boundary_conds = np.linspace( 0, Smax, self.M+1 )
- # grid : M+1 stock price levels X N+1 time points
- """
- # self.boundary_conds = np.linspace( 0, Smax, self.M+1 )
- return np.interp( self.S0,
- self.boundary_conds, self.values
- ) # lookup grid to return option present value

Put all together:
- import numpy as np
- import sys
-
- """
- Price an American option by the Crank-Nicolson method
- """
-
- class FDCnAM( FDCnEu ):
- def __init__(self, S0, K, r=0.05, T=1, sigma=0,
- Smax=1, M=1, N=1,
- omega=1, tol=0, is_put=False):
- # python 2.x
- # super( FDCnAm, self ).__init__(S0, K, r=r, T=T, sigma=sigma,
- # Smax=Smax, M=M, N=N,
- # is_put=is_put)
- # python 3.x
- super().__init__(S0, K, r=r, T=T, sigma=sigma,
- Smax=Smax, M=M, N=N,
- is_put=is_put)
- self.omega = omega
- self.tol = tol
- self.i_values = np.arange( self.M+1 ) # Smin~Smax: price level indices
- self.j_values = np.arange( self.N+1 ) # 0~T: time point indices
-
- def setup_boundary_conditions( self ):
- if self.is_call:
- # self.boundary_conds = np.linspace( 0, Smax, self.M+1 )
- # so self.boundary_conds[0] == 0s
- self.payoffs = np.maximum( 0, self.boundary_conds[1:self.M]-self.K )
- else:
- self.payoffs = np.maximum( 0, self.K - self.boundary_conds[1:self.M] )
-
- self.past_values = self.payoffs
- self.boundary_values = self.K * np.exp( -self.r*self.dt*(self.N-self.j_values) )
-
- def calculate_payoff_start_boundary( self, rhs, old_values ):
- # self.i_values = np.arange( self.M ) # Smin~Smax: price level indices
- # self.j_values = np.arange( self.N ) # 0~T: time point indices
- # def setup_coefficients(self):
- # self.alpha = 0.25 * self.dt * ( (self.sigma**2)*(self.i_values**2) -\
- # self.r * self.i_values
- # )
- # self.beta = -0.5 * self.dt * ( (self.sigma**2)*(self.i_values**2) +\
- # self.r
- # )
- # self.gamma = 0.25 * self.dt * ( (self.sigma**2)*(self.i_values**2) +\
- # self.r * self.i_values
- # )
- # S[0]~S[M] stock price levels, Smin~Smax
- # old_value.shape : M-1, old_value[0]~old_value[M-2] : f[1]~f[M-1] : S[1]~S[M-1]
- # rhs[0]~rhs[M-2] : f[1]~f[M-1]
- # self.beta[0]~[M-1]
- payoff = old_values[0] + self.omega/(1-self.beta[1]) *\
- ( rhs[0] -\
- ( 1-self.beta[1] ) * old_values[0] +\
- self.gamma[1] * old_values[1]
- )
- # self.payoffs : option intrinsic value
- return max( self.payoffs[0], payoff )
-
- def calculate_payoff_end_boundary( self, rhs, old_values, new_values ):
- # old_value.shape : M-1, 0~M stock price levels
- # f_M-1 rhs_M-1
- payoff = old_values[-1] + self.omega/(1-self.beta[-2]) *\
- ( rhs[-1] +\
- self.alpha[-2] * new_values[-2] -\
- ( 1-self.beta[-2] ) * old_values[-1]
- )
- # self.payoffs : option intrinsic value
- return max( self.payoffs[-1], payoff )
-
- def calculate_payoff( self, i, rhs, old_values, new_values ):
- # i start from 2
- payoff = old_values[i] + self.omega/(1-self.beta[i+1]) *\
- ( rhs[i] +\
- self.alpha[i+1] * new_values[i-1] -\
- ( 1-self.beta[i+1] ) * old_values[i] +\
- self.gamma[i+1] * old_values[i+1]
- )
- return max( self.payoffs[i], payoff )
-
- def traverse_grid( self ):
- """ Solve using linear systems of equations """
- aux = np.zeros( self.M-1 ) # 1~M-1 ==> [0,M-2]
- new_values = np.zeros( self.M-1 )
-
- # Gauss-Siede
- for j in reversed( range(self.N) ): # T~0
- aux[0] = self.alpha[1] * ( self.boundary_values[j] +\
- self.boundary_values[j+1]
- )
- rhs = np.dot( self.M2, self.past_values ) + aux
-
- # Gauss-Siede
- old_values = np.copy( self.past_values ) # Gauss-Siede
- error = sys.float_info.max
-
- while error > self.tol:
- new_values[0] = self.calculate_payoff_start_boundary( rhs, old_values )
-
- for i in range( self.M-2 )[1:]: # S_1~S[M-3]
- new_values[i] = self.calculate_payoff( i, rhs, old_values, new_values )
-
- new_values[-1] = self.calculate_payoff_end_boundary( rhs, old_values, new_values )
-
- error = np.linalg.norm( new_values - old_values )
-
- old_values = np.copy( new_values )
- self.past_values = np.copy( new_values )
-
- self.values = np.concatenate(( [ self.boundary_values[0] ],
- new_values,
- [0]
- ), axis=0)
-
- def interpolate( self ):
- # Use linear interpolation on final values as 1D array
- """
- Use piecewise linear interpolation on the initial
- grid column to get the closest price at S0.
-
- numpy.interp(x, xp, fp, left=None, right=None, period=None)
- left: Value to return for x < xp[0], default is fp[0]
- right: Value to return for x > xp[-1], default is fp[-1]
- One-dimensional linear interpolation for monotonically
- increasing sample points.
- Returns the one-dimensional piecewise linear interpolant to
- a function with given discrete data points (xp, fp),
- evaluated at x.
- xp = [1, 2, 3]
- fp = [3, 2, 0]
- np.interp(2.5, xp, fp) ==> 1.0
-
- self.boundary_conds = np.linspace( 0, Smax, self.M+1 )
- # grid : M+1 stock price levels X N+1 time points
- """
- # self.boundary_conds = np.linspace( 0, Smax, self.M+1 )
- return np.interp( self.S0,
- self.boundary_conds, self.values
- ) # lookup grid to return option present value

Let's price an American call-and-put option with an underlying asset price of 50 and volatility of 40 percent, a strike price of 50, a risk-free rate of 10 percent, and an expiration date of five months(5./12). We choose a Smax value of 100 , M as 100 , N as 42 , an omega parameter
value of 1.2 , and a tolerance value of 0.001:
- am_call_option = FDCnAM( 50, 50, r=0.1, T=5./12.,
- sigma=0.4, Smax=100, M=100, N=42,
- omega=1.2, tol=0.001, is_put=False
- )
- print( "American call option price:",am_call_option.price() )
- am_put_option = FDCnAM( 50, 50, r=0.1, T=5./12.,
- sigma=0.4, Smax=100, M=100, N=42,
- omega=1.2, tol=0.001, is_put=True
- )
- print( "American put option price:",am_put_option.price() )
The prices of the call and put American stock options by using the Crank-Nicolson method are $6.109 and $4.2778, respectively.
- am_call_option = FDCnAM( 140, 150, r=0.04, T=6./12.,
- sigma=0.25, Smax=200, M=200, N=2,
- omega=1.2, tol=0.001, is_put=False
- )
- print( "American call option price:",am_call_option.price() )
- am_put_option = FDCnAM( 140, 150, r=0.04, T=6./12.,
- sigma=0.25, Smax=200, M=200, N=2,
- omega=1.2, tol=0.001, is_put=True
- )
- print( "American put option price:",am_put_option.price() )
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.
In the option pricing methods we have learned so far, a number of parameters are assumed to be constant许多参数被假定为常数: interest rates, strike prices, dividends, and volatility. Here, the parameter of interest is volatility. In quantitative research, the volatility ratio is used to forecast price trends.
To derive implied volatilities, we need to refer to https://blog.csdn.net/Linli522362242/article/details/125662545, Nonlinearity in Finance, where we discussed the root-finding methods of nonlinear functions. We will use the bisection method二分法 of numerical procedures in our next example to create an implied volatility curve.
Let's consider the option data of the stock Apple (AAPL), which was gathered at the end of the day on October 3, 2014. These details are provided in the following table. The option expires on December 20, 2014. The prices listed are the mid-points of the bid and ask prices:
The last traded price of AAPL was 99.62, with an interest rate of 2.48 percent and a dividend yield of 1.82 percent. The American options expire in 78 days.
Using this information, let's create a new class named ImpliedVolatilityModel that accepts the stock option's parameters in the constructor. If required, import the BinomialLROption class that we created for the LR binomial tree we covered in the earlier section of this chapter, A class for the LR binomial tree option pricing model.
- import math
- # Stores common attributes of stock option
- class StockOption(object):
- def __init__(
- self, S0, K, r =0.05, T=1, N=2, pu=0, pd=0,
- div=0, sigma=0, is_put = False, is_am=False
- ):
- """
- Initialize the stock option base class.
- Defaults to European call unless specified.
- : param S0: initial stock price
- : param K: strike price
- : param r: risk-free interest rate
- : param T: time to maturity
- : param N: number of time steps
- : param pu: probability at up state
- : param pd: probability at down state
- : param div: Dividend yield
- : param is_put: True for a put option,
- False for a call option
- : param is_am: True for an American option,
- False for a European option
- """
- self.S0=S0
- self.K=K
- self.r=r
- self.T=T
- self.N = max(1,N)
- self.STs = [] # Declare the stock prices tree
-
- # Optional parameters used by derived classes
- self.pu, self.pd = pu, pd
- self.div = div
- self.sigma = sigma
- self.is_call = not is_put
- self.is_european = not is_am
- # @property:https://www.programiz.com/python-programming/property
- @property
- def dt(self):
- # Single time step, in years
- return self.T/float(self.N)
-
- @property
- def df(self):
- # The discount factor
- return math.exp( -(self.r - self.div)*self.dt )
-
- ############################################
- import math
- import numpy as np
-
- # Price a European or American option by the binomial tree
- class BinomialTreeOption(StockOption):
- def setup_parameters( self ):
- self.u = 1 + self.pu # Expected value % in the up state, e.g 1+0.2=1.2
- self.d = 1 - self.pd # Expected value % in the down state e.g 1-0.2=0.8
-
- # qu : The risk-neutral (up state) probability
- self.qu = ( math.exp( (self.r-self.div)*self.dt )-self.d
- )/(self.u-self.d)
- # qd : The risk-neutral (down state) probability
- self.qd = 1-self.qu
-
- def init_stock_price_tree(self):
- # Initialize a 2D tree at T=0
- # to store the expected returns of the stock prices for all time steps
- self.STs = [ np.array([self.S0]) ] # [array([50])] # root
-
- # Simulate the possible stock prices path
- # calculate the payoff values
- # from exercising the option at each period
- for i in range( self.N ): # N: number of time steps(branch_nodes)
- prev_branches = self.STs[-1] # at last time step
- st = np.concatenate( # note, cross node: Sud = Sdu
- ( prev_branches*self.u, # go up
- [prev_branches[-1]*self.d] # the last one goes down
- )# array([60., 40.])
- ) # array([72., 48., 32.])
-
- self.STs.append(st) # Add nodes at each time step
-
- # the intrinsic values of the option at maturity
- def init_payoffs_tree(self):
- if self.is_call:# call option
- return np.maximum( 0, self.STs[self.N]-self.K )
- else:# put option
- return np.maximum( 0, self.K-self.STs[self.N] )
- # e.g. payoffs: [ 0. 4. 20.]
-
- # Returns the maximum payoff values between
- # exercising the American option early
- # and not exercising the option at all
- def check_early_exercise( self, payoffs, node ):
- if self.is_call:
- return np.maximum( payoffs, self.STs[node]-self.K )
- else:
- # payoffs: [1.41475309 9.46393007]
- # payoffs at maturity: [-8. 12.]
- # np.maximum ==>[1.41475309 12.]
- return np.maximum( payoffs, self.K-self.STs[node] )
-
- def traverse_tree( self, payoffs ):
- """
- Starting from the time the option expires, traverse
- backwards and calculate discounted payoffs at each node
- """
- 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 # df: The discount factor
- # * math.exp( -(self.r - self.div)*self.dt )
- print("At the time step",i,", payoffs:",payoffs)
- # [1.41475309 9.46393007]
- # [4.19265428]
- # Payoffs from exercising, for American options
- if not self.is_european:
- payoffs = self.check_early_exercise( payoffs, i )
- print("At the time step",i,", max payoffs:",payoffs)
-
- return payoffs
-
- def begin_tree_traversal( self ):
- # the intrinsic values of the option at maturity
- payoffs = self.init_payoffs_tree() # e.g. payoffs: [ 0. 4. 20.]
- print("the payoffs at maturity", payoffs)
- return self.traverse_tree( payoffs )
-
- ###### Entry point of the pricing implementation ######
- def price(self):
- self.setup_parameters()
- self.init_stock_price_tree()
- # self.STs : [array([50]), array([60., 40.]), array([72., 48., 32.])]
- print("Nodes:",self.STs)
- payoffs = self.begin_tree_traversal()
- #print(payoffs)
- return payoffs[0]
-
- ############################################
- import math
-
- # Price an option by the Leisen-Reimer tree
-
- class BinomialLROption( BinomialTreeOption ):
- def pp_2_inversion( self, z, n ):
- # copysign : return the value of the first parameter and the sign of the second parameter
- # math.copysign(4, -1) ==> -4.0
- # math.copysign(-8, 97.21) ==> 8.0
- # math.copysign(-43, -76) ==> -43.0
- return 0.5 + math.copysign(1,z) *\
- math.sqrt( 0.25-0.25*math.exp(
- -( ( z/( n+1./3.+0.1/(n+1) ) )**2.
- ) * ( n+1./6. ) )
- )
-
- def setup_parameters( self ):
- odd_N = self.N if (self.N%2==0) 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) )
- d2 = d1-self.sigma*math.sqrt(self.T)
-
- pbar = self.pp_2_inversion( d1, odd_N )
- self.p = self.pp_2_inversion( d2, odd_N )
-
- # self.dt : self.T/float(self.N)
- # self.df : math.exp( -(self.r - self.div)*self.dt )
- # self.u : Expected value in the up state, e.g 1+0.2=1.2
- self.u = 1/self.df * pbar/self.p
- # self.d : Expected value in the down state e.g 1-0.2=0.8
- self.d = (1/self.df - self.p*self.u)/(1-self.p)
-
- # qu : The risk-neutral (up state) probability
- self.qu = self.p
- # qd : The risk-neutral (down state) probability
- self.qd = 1-self.p

The bisection function we covered in https://blog.csdn.net/Linli522362242/article/details/125662545, Nonlinearity in Finance, is also required.
Import the bisection function we discussed in the previous chapter:
- # The bisection method
- import numpy as np
-
- def bisection(func, a, b, tol=0.1, maxiter=20):
- """
- :param func: The function to solve
- :param a: The x-axis value where f(a)<0
- :param b: The x-axis value where f(b)>0
- :param tol: The precision of the solution
- :param maxiter: Maximum number of iterations
- :return:
- The x-axis value of the root,
- number of iterations used
- """
- c = (a+b)*0.5 # Declare c as the midpoint [a,b]
- n = 1 # start with 1 iteration
- while n<= maxiter:
- c = (a+b)*0.5
- # Root is found or is very close
- if func(c) == 0 or abs(a-b)*0.5 < tol:
- return c,n
- n += 1
-
- # Decide the side to repeat the steps
- if func(c) < 0:
- a=c
- else:
- b=c
-
- return c,n

The option_valuation() method accepts the K strike price and the sigma volatility value to compute the value of the option. In this example, we are using the BinomialLROption pricing method.
The get_implied_volatilities() method accepts a list of strike and option prices to compute the implied volatilities by the bisection method for every price available. Therefore, the length of the two lists must be the same.
The Python code for the ImpliedVolatilityModel class is given as follows:
- """
- Get implied volatilities from a Leisen-Reimer binomial tree
- using the bisection method as the numerical procedure.
- """
-
- class ImpliedVolatilityModel( object ):
- def __init__( self, S0, r=0.05, T=1, div=0,
- N=1, is_put=False ):
- self.S0 = S0
- self.r = r
- self.T = T
- self.div = div
- self.N = N
- self.is_put = is_put
-
- def option_valuation(self, K, sigma):
- # accepts the K strike price and the sigma volatility value
- # to compute the value of the option.
- """ Use the binomial Leisen-Reimer tree """
- lr_option = BinomialLROption( self.S0, K, r=self.r, T=self.T, N=self.N,
- sigma=sigma,
- div=self.div, is_put=self.is_put
- )
- return lr_option.price()
-
- def get_implied_volatilities( self, Ks, opt_prices ):
- impvols = []
-
- for i in range( len(strikes) ):
- # Bind f(sigma) for use by the bisection method
- f = lambda sigma: self.option_valuation( Ks[i], sigma ) - opt_prices[i]
-
- impv = bisection(f, 0.01, 0.99, 0.0001, 100)[0]
- # solution : self.option_valuation( Ks[i], sigma ) == opt_prices[i] ==> sigma
- impvols.append(impv)
-
- return impvols

Using this model, let's find out the implied volatilities of the American put options using this particular set of data:
- strikes = [75, 80, 85, 90, 92.5, 95, 97.5,
- 100, 105, 110, 115, 120, 125]
- put_prices = [0.16, 0.32, 0.6, 1.22, 1.77, 2.54, 3.55,
- 4.8, 7.75, 11.8, 15.96, 20.75, 25.81]
-
- model = ImpliedVolatilityModel( 99.62, r=0.0248, T=78/365., div=0.0182,
- N=77, is_put=True
- )
- impvols_put = model.get_implied_volatilities(strikes, put_prices)
- impvols_put
The implied volatility values are now stored in the impvols_put variable as a list object. Let's plot these values against the strike prices to obtain an implied volatility curve:
- %matplotlib inline
- import matplotlib.pyplot as plt
-
- plt.plot( strikes, impvols_put )
- plt.xlabel('Strike Prices')
- plt.ylabel('Implied Volatilities')
- plt.title('AAPL Put Implied Volatilities expiring in 78 days')
- plt.show()
This would give us the volatility smile, as shown in the following diagram. Here, we have modeled an LR tree with 77 steps, with each step representing one day:
Of course, pricing an option daily may not be ideal since markets change by fractions of a millisecond. We used the bisection method to solve the implied volatility as implied by the binomial tree, as opposed to the realized volatility values directly observed from market prices.
Should we fit this curve against a polynomial curve to identify potential arbitrage opportunities? Or extrapolate the curve to derive further insights on potential opportunities from implied volatilities of far out-of-the-money
(There are also options traded that are far out-of-the-money (index level (PRICE) much lower than option strike(STRIKE)).)
and in-the-money
(there are call options traded and quoted that are far in-the-money (index level (PRICE) much higher than option strike (STRIKE)).)
options? Well, these questions are for option traders like yourself to find out!
In this chapter, we looked at a number of numerical procedures in derivative pricing, the most common being options. One such procedure is the use of trees, with binomial trees being the simplest structure to model asset information, where one node extends to two other nodes in each time step, representing an up state and a down state, respectively. In trinomial trees, each node extends to three other nodes in each time step, representing an up state, a down state, and a state with no movement, respectively. As the tree traverses upwards, the underlying asset is computed and represented at each node. The option then takes on the structure of this tree and, starting from the terminal payoffs, the tree traverses backward and toward the root, which converges to the current discounted option price. Besides binomial and trinomial trees, trees can take on the form of the CRR, Jarrow-Rudd, Tian, or LR parameters.
By adding another layer of nodes around our tree, we introduced additional information from which we can derive the Greeks, such as the delta and gamma, without incurring additional computational costs.
Lattices were introduced as a way of saving storage costs over binomial and trinomial trees. In lattice pricing, nodes with new information are saved only once and reused later on nodes that require no change in the information.
We also discussed the finite difference schemes in option pricing, consisting of terminal and boundary conditions. From the terminal conditions, the grid traverses backward in time using the explicit method, implicit method, and the Crank-Nicolson method. Besides pricing European and American options, finite difference pricing schemes can be used to price exotic options, where we looked at an example of pricing a down-and-out barrier option.
By importing the bisection root-finding method learned about in https://blog.csdn.net/Linli522362242/article/details/125662545, Nonlinearity in Finance, and the binomial LR tree model in this chapter, we used market prices of an American option to create an implied volatility curve for further studies.
In the next chapter, we will take a look at modeling interest rates and derivatives.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。