Chapter 6. Dynamic Economy

Multiperiod models of securities markets are much more realistic than single period models. In fact, they are extensively used for practical purposes in the financial industry.

Stanley Pliska (1997)

Although markets are not complete at any one time, they are dynamically complete in the sense that any consumption process can be financed by trading the given set of financial securities, adjusting portfolios through time as uncertainty is resolved bit by bit.

Darrell Duffie (1986)

In reality, quantitative information — such as changes in stock prices or interest rates — is revealed gradually over time. While static model economies are an elegant way of introducing fundamental notions in finance, a realistic financial model requires a dynamic representation of the financial world.

The formalism needed to properly model dynamic economies is more involved and cannot be covered in full detail in this chapter. However, the chapter can present two of the most important dynamic model economies based on discrete time dynamics: the Cox-Ross-Rubinstein (1979) binomial option pricing model and the Black-Scholes-Merton (1973) option pricing model in a discrete Monte Carlo simulation version. In this context, discrete time means that the set of relevant dates is extended from just two to a larger, but still finite, number — say, to 5 or 50.

The tools used in this chapter are more or less the same as before: linear algebra, probability theory, and also, like in the previous chapter, stochastic elements to implement Monte Carlo simulation. Duffie (1988) and Pliska (1997) are great resources for dynamic financial modeling in discrete time. Glasserman (2004) is the reference book for Monte Carlo simulation methods in finance.

Topics covered in this chapter are stochastic processes, option pricing in dynamically complete markets, binomial option pricing, Black-Scholes-Merton (1973) dynamic simulation, early exercise and American option pricing, Least-Squares Monte Carlo (LSM) option pricing.

The following table gives an overview of the topics in finance, mathematics and Python found in this chapter.

Finance Mathematics Python

uncertainty

stochastic process, binomial tree

NumPy, ndarray

uncertainty

stochastic process, Monte Carlo simulation

NumPy, ndarray, np.random.standard_normal

European option pricing

inner values, backwards induction, risk-neutral expectation

NumPy, ndarray, np.maximum

American option pricing

inner values, continuation values, OLS regression, backwards induction, risk-neutral expectation

NumPy, ndarray, np.polyval, np.polyfit, np.where

As in Chapter 5, the major goal of this chapter is generalization. While Chapter 5 generalizes the state space, this chapter sets out to generalize the discrete set of relevant points in time at which new information is revealed and economic action takes place. While some additional formalism is needed to do so, the chapter is, on the other hand, less formal since it focuses on two specific models only and does not try to provide a general framework for dynamic economies in discrete time. Such a general framework, including many examples implemented in Python, is found in Hilpisch (2015).

Binomial Option Pricing

The binomial option pricing model became popular immediately after publication in 1979 — both as a numerically efficient method to price European options and American options as well as a teaching tool. While the Black-Scholes-Merton (1973) model relies on continuous time finance and stochastic calculus, the binomial option pricing model is, in a sense, a discrete time version of the BSM model that can be fully understood with elementary mathematics only.

In the Cox-Ross-Rubinstein (1979) model, there are two traded financial assets, a risky one, called stock, and a risk-less one, called bond. The model economy is considered over a finite set of dates ? { t 0 = 0 , t 1 , t 2 , . . . , t M = T } with M + 1 , M > 1 elements.

Given a stock price of S t i , the stock price at the next date S t i+1 can only take on two different values:

S t i+1 = S t i · u S t i · d

u stands for an upward movement and d for a downward movement.

To simplify the handling of dates, assume an evenly spaced time grid with M time intervals of length Δ t = T M each. The finite set of dates can then be written as ? { t 0 = 0 , t 1 = Δ t , t 2 = 2 Δ t , . . . , T } . In addition, define

u e σΔt d e -σΔt = u -1

It turns out, that one consequence of this definition is the property u · d = 1 which will prove convenient in that it creates a so-called recombining binomial tree. σ >0 represents the constant volatility factor.

Assume that the risk-less, constant short rate is given by r 0 . Given a bond price of B t i , the price of the bond one period later is given by

B t i+1 = B t i · e r·(t i+1 -t i )

or

B t+Δt = B t · e r·Δt

for some t ? T .

A central numerical parameter value to be derived, based on the above assumptions, is the martingale probability for an upward movement at any given node. Given that there are only two branches for every node, the downward probability is then known as well. Denote the martingale probability for an upwards movement by q >0 , 0 < q < 1 . One gets from the martingale property for the stock price:

S t = e -rΔt ? Q ( S t+Δt ) = e -rΔt q S t u + ( 1 - q ) S t d 1 = e -rΔt q u + ( 1 - q ) d q = e rΔt -d u-d

This shows that the martingale measure is fixed at every node and consequently for the whole tree.

The basics of the binomial option pricing model are easily translated into Python code.1

In [1]: import math
        import numpy as np

In [2]: S0 = 36.  1
        K = 40.  2
        r = 0.06  3
        T = 1.0  4
        sigma = 0.2  5

In [3]: m = 4  6
        dt = T / m  7
        df = math.exp(-r * dt)  8
        up = math.exp(sigma * math.sqrt(dt))  9
        down = 1 / up  9

In [4]: q = (1 / df - down) / (up - down)  10
1

The initial stock price value.

2

The strike price for the option to be valued.

3

The constant risk-less short rate.

4

The time horizon and option maturity.

5

The constant volatility factor.

6

The number of time intervals.

7

The resulting length of each time interval.

8

The discount factor for the fixed time interval.

9

The upwards and downwards factors.

10

The martingale probability for an upward movement.

The simulation of the stock price process and the valuation of options in this model are a bit more involved. The following presents two different implementations. One based on Python loops which might be easier to understand at the beginning. One based on vectorized NumPy code which is more concise and efficient — but maybe a bit harder to grasp at first.

Simulation & Valuation Based on Python Loops

Even though the implementation in this sub-section uses Python loops, the basic data structure is a NumPy ndarray object.

In [5]: S = np.zeros((m + 1, m + 1))  1
        S  1
Out[5]: array([[0., 0., 0., 0., 0.],
               [0., 0., 0., 0., 0.],
               [0., 0., 0., 0., 0.],
               [0., 0., 0., 0., 0.],
               [0., 0., 0., 0., 0.]])

In [6]: S[0, 0] = S0  2
        S  2
Out[6]: array([[36.,  0.,  0.,  0.,  0.],
               [ 0.,  0.,  0.,  0.,  0.],
               [ 0.,  0.,  0.,  0.,  0.],
               [ 0.,  0.,  0.,  0.,  0.],
               [ 0.,  0.,  0.,  0.,  0.]])

In [7]: z = 1  3
        for t in range(1, m + 1):  4
            for i in range(0, z):  5
                S[i, t] = S[i, t - 1] * up  6
                S[i + 1 ,t] = S[i, t - 1] * down  6
            z += 1  7

In [8]: np.set_printoptions(formatter=
                {'float_kind': lambda x: '%7.3f' % x})

In [9]: S  8
Out[9]: array([[ 36.000,  39.786,  43.970,  48.595,  53.706],
               [  0.000,  32.574,  36.000,  39.786,  43.970],
               [  0.000,   0.000,  29.474,  32.574,  36.000],
               [  0.000,   0.000,   0.000,  26.669,  29.474],
               [  0.000,   0.000,   0.000,   0.000,  24.132]])
1

Initializes the ndarray object.

2

Sets the initial stock price value in the upper left hand corner.

3

Sets a counter z to 1.

4

Iterates from 1 to m+1, that is, over all time steps after 0.

5

Iterates over the relevant nodes for the given time step.

6

Calculates the up and down values and sets them in the ndarray object.

7

Increases the counter by 1 to include more relevant nodes in the next step.

8

The resulting recombining binomial tree.

European Option Pricing

The valuation of a European option based on the available stock price process happens by calculating the inner values of the option at maturity and applying backwards induction. This basically means starting at the end, moving step-by-step backwards to the present and at every node repeatedly applying the risk-neutral pricing paradigm as introduced in the simple static two-state economy of Chapter 2.

The following Python code assumes a European put option payoff.

In [10]: h = np.zeros_like(S)  1

In [11]: z = 1
         for t in range(0, m + 1):
             for i in range(0, z):
                 h[i, t] = max(K - S[i, t], 0)  2
             z += 1

In [12]: h  3
Out[12]: array([[  4.000,   0.214,   0.000,   0.000,   0.000],
                [  0.000,   7.426,   4.000,   0.214,   0.000],
                [  0.000,   0.000,  10.526,   7.426,   4.000],
                [  0.000,   0.000,   0.000,  13.331,  10.526],
                [  0.000,   0.000,   0.000,   0.000,  15.868]])

In [13]: V = np.zeros_like(S)
         V[:, -1] = h[:, -1]
         V
Out[13]: array([[  0.000,   0.000,   0.000,   0.000,   0.000],
                [  0.000,   0.000,   0.000,   0.000,   0.000],
                [  0.000,   0.000,   0.000,   0.000,   4.000],
                [  0.000,   0.000,   0.000,   0.000,  10.526],
                [  0.000,   0.000,   0.000,   0.000,  15.868]])

In [14]: m
Out[14]: 4

In [15]: # European option pricing
         z = 0
         for t in range(m - 1, -1, -1):
             for i in range(0, m - z):
                 V[i, t] = df * (q * V[i, t + 1] +
                             (1-q) * V[i + 1, t + 1])  3
             z += 1

In [16]: V  4
Out[16]: array([[  3.977,   2.190,   0.784,   0.000,   0.000],
                [  0.000,   6.299,   3.985,   1.771,   0.000],
                [  0.000,   0.000,   9.344,   6.830,   4.000],
                [  0.000,   0.000,   0.000,  12.735,  10.526],
                [  0.000,   0.000,   0.000,   0.000,  15.868]])

In [17]: V[0, 0]  5
Out[17]: 3.9771456941187893
1

The ndarray object for the inner values.

2

This calculates the inner values for the relevant nodes.

3

This does the node-wise valuation by applying risk-neutral pricing.

4

The resulting present value binomial tree.

5

The present value of the European put option.

American Option Pricing

One of the major features of the binomial options pricing model is that American options are as easily valued as their European counterparts. An American option can be exercised at any time on and before the maturity date. The adjustment to be made to the backwards valuation algorithm is simple: one just needs to check whether the inner value of the American option is at any given node higher than the continuation value, that is, the present value of not exercising the option. If that is the case, the option is exercised and the value of the American option is set to the inner value. Formally, one gets

V t = max h t , e -rΔt ? Q ( V t+Δt )

where h t is the inner value at time t and e -rΔt ? Q ( V t+Δt ) is the continuation value.

In Python, a single line of code needs to be added.

In [18]: # American option pricing
         z = 0
         for t in range(m - 1, -1, -1):
             for i in range(0, m-z):
                 V[i, t] = df * (q * V[i, t + 1] +
                           (1 - q) * V[i + 1, t + 1])
                 V[i, t] = max(h[i, t], V[i, t])  1
             z += 1

In [19]: V  2
Out[19]: array([[  4.540,   2.307,   0.784,   0.000,   0.000],
                [  0.000,   7.426,   4.249,   1.771,   0.000],
                [  0.000,   0.000,  10.526,   7.426,   4.000],
                [  0.000,   0.000,   0.000,  13.331,  10.526],
                [  0.000,   0.000,   0.000,   0.000,  15.868]])

In [20]: V[0, 0]  3
Out[20]: 4.539560595224299
1

This line checks for the early exercise decision; puts the inner value as the American option value when it is higher than the continuation value.

2

The resulting binomial tree for the values of the American put option.

3

The present value of the American put option which is considerably higher that without early exercise.

Simulation & Valuation Based on Vectorized Code

The algorithm implementation that follows makes systematic use of NumPy vectorization capabilities. The implementation is presented step-by-step, also with some illustrating lines of code not needed for the algorithm implementation itself.

In [21]: u = np.arange(m + 1)  1
         u  1
Out[21]: array([0, 1, 2, 3, 4])

In [22]: u ** 2  2
Out[22]: array([ 0,  1,  4,  9, 16])

In [23]: 2 ** u  3
Out[23]: array([ 1,  2,  4,  8, 16])

In [24]: u = np.resize(u, (m + 1, m + 1))  4
         u
Out[24]: array([[0, 1, 2, 3, 4],
                [0, 1, 2, 3, 4],
                [0, 1, 2, 3, 4],
                [0, 1, 2, 3, 4],
                [0, 1, 2, 3, 4]])

In [25]: d = u.T  5
         d  5
Out[25]: array([[0, 0, 0, 0, 0],
                [1, 1, 1, 1, 1],
                [2, 2, 2, 2, 2],
                [3, 3, 3, 3, 3],
                [4, 4, 4, 4, 4]])

In [26]: (u - 2 * d)  6
Out[26]: array([[ 0,  1,  2,  3,  4],
                [-2, -1,  0,  1,  2],
                [-4, -3, -2, -1,  0],
                [-6, -5, -4, -3, -2],
                [-8, -7, -6, -5, -4]])
1

Creates an ndarray object for the number of upwards movements from 0 to m.

2

Calculating the squares by a vectorized operation.

3

Calculating the powers of 2 by using the u object as a vector exponent.

4

Resizes the u object from one dimension to two dimensions. The number of upwards movements is now stored in each row.

5

Transposes the u object to get a two-dimensional ndarray object d with the number of downwards movements in each column.

6

Combines the u and d objects to arrive at the net number of upwards and downwards movements. For instance, +2 means “two more upwards movements than downwards movements” or -1 means “one more downwards movement than upwards movements”.2

Equipped with a matrix containing the net number of movements in the binomial tree, simulation of the stock price process boils down to a single line of code.

In [27]: S = S0 * np.exp(sigma * math.sqrt(dt) * (u - 2 * d))  1
         S  2
Out[27]: array([[ 36.000,  39.786,  43.970,  48.595,  53.706],
                [ 29.474,  32.574,  36.000,  39.786,  43.970],
                [ 24.132,  26.669,  29.474,  32.574,  36.000],
                [ 19.757,  21.835,  24.132,  26.669,  29.474],
                [ 16.176,  17.877,  19.757,  21.835,  24.132]])
1

The vectorized simulation of the stock price process (binomial tree).

2

Only the numbers on and above the diagonal are relevant.

The valuation of both the European and American put options can also be vectorized to some extent. A single loop over the time steps remains.

In [28]: h = np.maximum(K - S, 0)  1
         h  2
Out[28]: array([[  4.000,   0.214,   0.000,   0.000,   0.000],
                [ 10.526,   7.426,   4.000,   0.214,   0.000],
                [ 15.868,  13.331,  10.526,   7.426,   4.000],
                [ 20.243,  18.165,  15.868,  13.331,  10.526],
                [ 23.824,  22.123,  20.243,  18.165,  15.868]])

In [29]: V = h.copy()  3

In [30]: # European option pricing
         for t in range(m - 1, -1, -1):  4
             V[0:-1, t] = df * (q * V[:-1, t + 1] +
                            (1-q) * V[1:, t + 1])  4

In [31]: V[0, 0]  5
Out[31]: 3.977145694118792

In [32]: # American option pricing
         for t in range(m - 1, -1, -1):  6
             V[0:-1, t] = df * (q * V[:-1, t + 1] +
                            (1-q) * V[1:, t + 1])  6
             V[:, t] = np.maximum(h[:, t], V[:, t])  6

In [33]: V
Out[33]: array([[  4.540,   2.307,   0.784,   0.000,   0.000],
                [ 10.526,   7.426,   4.249,   1.771,   0.000],
                [ 15.868,  13.331,  10.526,   7.426,   4.000],
                [ 20.243,  18.165,  15.868,  13.331,  10.526],
                [ 23.824,  22.123,  20.243,  18.165,  15.868]])

In [34]: V[0, 0]  7
Out[34]: 4.5395605952243
1

The calculation of the inner value of the put option, fully vectorized this time.

2

As before, only the numbers on and above the diagonal are relevant.

3

Creates a copy of the h object.

4

The partly vectorized valuation algorithm for the European put option.

5

The present value of the European put option.

6

The partly vectorized valuation algorithm for the American put option.

7

The present value of the American put option.

European and American Options

The beauty of the (recombining) binomial option pricing model of Cox, Ross, and Rubinstein (1979) not only lies in its simplicity, but also in the fact that it can be used to value both European options as well as American options with high accuracy in an efficient manner. In the limit, making time steps infinitely small, the model converges to the Black-Scholes-Merton (1973) model which is another advantage.

Speed Comparison

Vectorizing code does not only make Python code more concise, it generally allows for significant speed improvements. The following code snippets implement the previous algorithms for a speed comparison based on a larger, more realistic number of time steps. First, the basic numerical parameters need to be adjusted.

In [35]: m = 500  1
         dt = T / m
         df = math.exp(-r * dt)
         up = math.exp(sigma * math.sqrt(dt))
         down = 1 / up
         q = (1 / df - down) / (up - down)
         q
Out[35]: 0.5044724639230862
1

Increases the number of time intervals to a realistic level, yielding rather accurate numerical option values.

The function binomial_looping() integrates all elements of the loop-based implementation of the simulation and valuation algorithm for the American put option.

In [36]: def binomial_looping():
             # stock price simulation in binomial tree
             S = np.zeros((m + 1, m + 1))
             S[0, 0] = S0
             z = 1
             for t in range(1, m + 1):
                 for i in range(0, z):
                     S[i, t] = S[i, t - 1] * up
                     S[i + 1 ,t] = S[i, t - 1] * down
                 z += 1
             # inner value calculation
             h = np.zeros_like(S)
             z = 1
             for t in range(0, m + 1):
                 for i in range(0, z):
                     h[i, t] = max(K - S[i, t], 0)
                 z += 1
             # American option pricing
             V = np.zeros_like(S)
             V[:, -1] = h[:, -1]
             z = 0
             for t in range(m - 1, -1, -1):
                 for i in range(0, m - z):
                     V[i, t] = df * (q * V[i, t + 1] +
                               (1 - q) * V[i + 1, t + 1])
                     V[i, t] = max(h[i, t], V[i, t])
                 z += 1
             return V[0, 0]

The execution takes on the author’s computer less than 200 milliseconds.

In [37]: %time binomial_looping()
         CPU times: user 231 ms, sys: 17.9 ms, total: 249 ms
         Wall time: 177 ms

Out[37]: 4.486374777505983

In [38]: %timeit binomial_looping()
         171 ms ± 2.18 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

The function binomial_vectorization() integrates all elements of the vectorized implementation of the simulation and valuation algorithm for the American put option.

In [39]: def binomial_vectorization():
             u = np.arange(m + 1)
             u = np.resize(u, (m + 1, m + 1))
             d = u.T
             # stock price simulation
             S = S0 * np.exp(sigma * math.sqrt(dt) * (u - 2 * d))
             # inner value calculation
             h = np.maximum(K - S, 0)
             # American option pricing
             V = h.copy()
             for t in range(m-1, -1, -1):
                 V[0:-1, t] = df * (q * V[:-1, t + 1] +
                                (1-q) * V[1:, t + 1])
                 V[:, t] = np.maximum(h[:, t], V[:, t])
             return V[0, 0]

This implementation is about 40 times faster than the loop-based one which impressively illustrates the power of vectorized implementation approaches in terms of performance improvements.

In [40]: %time binomial_vectorization()
         CPU times: user 4.37 ms, sys: 1.98 ms, total: 6.35 ms
         Wall time: 7.99 ms

Out[40]: 4.486374777506075

In [41]: %timeit binomial_vectorization()
         4.31 ms ± 198 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Infrastructure and Performance

All absolute times reported here are dependent both on the hardware and software configuration used. For instance, you can use NumPy in combination with the Math Kernel Library (MKL) from Intel (see https://software.intel.com/en-us/mkl). This combination can speed up numerical operations on Intel processor-based systems significantly. Relative times and speed-up factors should be roughly similar on different infrastructures.

Black-Scholes-Merton Option Pricing

A static simulation version of the Black-Scholes-Merton (1973) model for option pricing is discussed in Chapter 5. This section introduces a dynamic simulation version for their seminal option pricing model. For additional background information on the model, refer to Chapter 5.

The stochastic differential equation for the Black-Scholes-Merton (1973) economy is given by

d S t = r S t d t + σ S t d Z t

where S t >0 is the stock price at time t , r 0 the constant short rate, σ >0 the constant volatility factor and Z t an arithmetic Brownian motion (see Glasserman (2004, ch. 3) and Hilpisch (2018, ch. 12)).

Monte Carlo Simulation of Stock Price Paths

Assume a finite set of relevant points in time ? { t 0 = 0 , t 1 , t 2 , . . . , t M = T } with M + 1 , M > 1 and a fixed interval length of Δ t . The stock price S t , 0 < t T , given the previous stock price S t-Δt , can then be simulated according to the difference equation

S t = S t-Δt · exp r - σ 2 2 Δ t + σ Δ t z

where z is a standard normally distributed random variable. This scheme is called Euler discretization. It is known to be accurate in that it ensures convergence of the discrete time process to the continuous time one for Δ t converging to 0.

The dynamic Monte Carlo simulation is — with the background knowledge from the static simulation in Chapter 5 — straightforward to implement in Python. Figure 6-1 shows 10 simulated stock price paths.

In [43]: S0 = 36.  1
         K = 40.  1
         r = 0.06  1
         T = 1.0  1
         sigma = 0.2  1

In [44]: M = 100  2
         I = 50000  2

In [45]: dt = T / M  2
         dt  2
Out[45]: 0.01

In [46]: df = math.exp(-r * dt)  2
         df  2
Out[46]: 0.9994001799640054

In [47]: np.random.seed(100)

In [48]: rn = np.random.standard_normal((M + 1, I))  3
         rn.round(2)  3
Out[48]: array([[ -1.750,   0.340,   1.150, ...,   0.950,  -0.140,  -0.400],
                [  0.860,  -0.140,  -0.160, ...,  -1.970,   0.540,   0.520],
                [  0.880,   0.930,  -0.270, ...,  -0.500,   0.710,  -0.300],
                ...,
                [  1.570,   0.130,  -1.110, ...,  -0.200,  -0.240,  -0.330],
                [  0.400,   0.840,  -0.750, ...,   1.030,   0.340,  -0.460],
                [  0.890,   0.510,   0.920, ...,   0.490,   0.530,   0.310]])

In [49]: S = np.zeros_like(rn)  4
         S[0] = S0  4
         S  4
Out[49]: array([[ 36.000,  36.000,  36.000, ...,  36.000,  36.000,  36.000],
                [  0.000,   0.000,   0.000, ...,   0.000,   0.000,   0.000],
                [  0.000,   0.000,   0.000, ...,   0.000,   0.000,   0.000],
                ...,
                [  0.000,   0.000,   0.000, ...,   0.000,   0.000,   0.000],
                [  0.000,   0.000,   0.000, ...,   0.000,   0.000,   0.000],
                [  0.000,   0.000,   0.000, ...,   0.000,   0.000,   0.000]])

In [50]: for t in range(1, M + 1):
             S[t] = S[t - 1] * np.exp((r - sigma ** 2 / 2) * dt +
                                    sigma * math.sqrt(dt) * rn[t])  5

In [51]: S  5
Out[51]: array([[ 36.000,  36.000,  36.000, ...,  36.000,  36.000,  36.000],
                [ 36.641,  35.914,  35.899, ...,  34.625,  36.405,  36.389],
                [ 37.307,  36.602,  35.721, ...,  34.296,  36.943,  36.184],
                ...,
                [ 30.807,  44.200,  29.377, ...,  51.134,  43.436,  34.503],
                [ 31.065,  44.968,  28.952, ...,  52.219,  43.753,  34.199],
                [ 31.635,  45.450,  29.502, ...,  52.752,  44.233,  34.424]])

In [52]: from pylab import mpl, plt
         plt.style.use('seaborn')
         mpl.rcParams['font.family'] = 'serif'
         mpl.rcParams['savefig.dpi'] = 300

In [53]: plt.figure(figsize=(10, 6))
         plt.plot(S[:, :10]);  6
1

The financial parameters are as before.

2

These are the Monte Carlo simulation parameters (paths, time steps, length of time interval, discount factor for single time interval).

3

A two-dimensional ndarray object with standard normally distributed random number of appropriate size is generated.

4

Another two-dimensional ndarray object of the same shape is instantiated and the initial values for the single stock price paths are set.

5

The single stock price paths are simulated based on the initial stock prices, the random number matrix and the difference equation for the geometric Brownian motion.

6

Plots the first 10 simulated paths.

ftwp 0601
Figure 6-1. Simulated stock price paths for Black-Scholes-Merton (1973)

As in the static case, the end of period values of the stock price can be visualized in the form of a histogram (see Figure 6-2).

In [54]: ST = S[-1]
         plt.figure(figsize=(10, 6))
         plt.hist(ST, bins=35, color='b', label='frequency');
         plt.axvline(ST.mean(), color='r', label='mean')
         plt.axvline(ST.mean() + ST.std(), color='y', label='sd up')
         plt.axvline(ST.mean() - ST.std(), color='y', label='sd down')
         plt.legend(loc=0);
In [55]: S0 * math.exp(r * T)  1
Out[55]: 38.22611567563295

In [56]: S[-1].mean()  2
Out[56]: 38.19489518113793
1

Mathematically expected value for S T .

2

The average over all simulated values ST.

ftwp 0602
Figure 6-2. Frequency distribution of simulated end of period stock prices for Black-Scholes-Merton (1973)

Monte Carlo Valuation of the European Put Option

The Monte Carlo estimator for the price of the European put option is

P 0 = e -rT 1 I i=1 I max ( K - S T ( i ) , 0 )

where I is the number of simulated price paths. Against this background, European put option pricing boils down to a few lines of Python code only given the simulated stock price paths. Figure 6-3 shows a histogram of the simulated inner values at maturity.

In [57]: h = np.maximum(K - ST, 0)  1
         h  1
Out[57]: array([  8.365,   0.000,  10.498, ...,   0.000,   0.000,   5.576])

In [58]: plt.figure(figsize=(10, 6))
         plt.hist(h, color='b', bins=35);  2
In [59]: math.exp(-r * T) * h.mean()  3
Out[59]: 3.847996127958868
1

Calculates the inner values in vectorized fashion.

2

Plots the frequency distribution of the inner values at maturity, illustrating the highly asymmetric payoff which is typical for an option.

3

Calculates the average over all inner values and discounts the average to the present.3

ftwp 0603
Figure 6-3. Frequency distribution of simulated inner values at maturity for the European put option

Monte Carlo Valuation of the American Put Option

The valuation of American (put) options based on Monte Carlo simulation is a bit more involved. The most popular algorithm in this regard is the Least-Squares Monte Carlo (LSM) algorithm from Longstaff-Schwartz (2001) because it is relatively simple and efficient to apply from a numerical and computational perspective. The scope of this chapter does not allow to go into details. However, it allows to present a concise, highly vectorized Python implementation. For an in-depth treatment of the LSM algorithm applied to the Black-Scholes-Merton (1973) model economy including Python codes refer to Hilpisch (2015, ch. 7).

In [60]: h = np.maximum(K - S, 0)  1

In [61]: # Least-Squares Monte Carlo Valuation (LSM algorithm)
         V = h[-1]  2
         for t in range(M - 1, 0, -1):  3
             reg = np.polyfit(S[t], df * V, deg=5)  4
             C = np.polyval(reg, S[t])  4
             V = np.where(h[t] > C, h[t], df * V)  5

In [62]: df * V.mean()  6
Out[62]: 4.444779798484413
1

Calculates the inner values over the complete stock price path ndarray object.

2

Sets the initial simulated American option price values to the inner values at maturity.

3

The algorithm also works based on backwards induction, starting at T - Δ t and stopping at Δ t .

4

This is the central step of the algorithm during which the continuation values are estimated (approximated) based on the OLS regression of the present simulated option values against the stock price levels.

5

If the inner value is higher than the estimated (approximated) continuation value, exercise takes place and otherwise not.

6

The present value is calculated as the average over the American option price vector at t = Δ t as derived based on the LSM algorithm and discounted for the last remaining time interval to the present t = 0 .

Early Exercise and Monte Carlo Simulation

The efficient, Monte Carlo simulation-based valuation of options and derivatives with early exercise features has been a mainly unsolved problem until the end of the 1990s. At the beginning of the 2000s only, researchers were able to propose computationally efficient algorithms to deal with early exercise in a simulation context. As often in science and finance, once such an algorithm is known — such as the LSM algorithm — the implementation and application almost seems quite natural. After all, only a few lines of Python code are needed to accurately value the American put option in this section based on simulation. Nevertheless, the LSM algorithm must be considered a major breakthrough that helped to kick off the computational period in finance (see Chapter 1).

Conclusions

This chapter presents in a rather informal manner two popular, dynamically complete financial models. The first is the so-called recombining binomial tree model by Cox-Ross-Rubinstein (1979). The beauty of it lies in its simplicity and that it allows to implement European and American option pricing in a numerically efficient way and based on high school mathematics only. It is also a good “teaching and understanding” tool as compared to continuous time financial models that require advanced stochastic calculus.

The second model is a dynamic Monte Carlo simulation version of the Black-Scholes-Merton (1973) continuous time option pricing model. Using NumPy simulation techniques, dynamic Monte Carlo simulation can also be implemented in a numerically efficient manner. Even the computationally demanding Least-Squares Monte Carlo algorithm by Longstaff-Schwartz (2001), involving a time consuming OLS regression step, is quite fast when implemented based on vectorization techniques.

In summary, NumPy with its powerful vectorization capabilities has proven once again that it does not only allow for concise algorithmic code but also for fast execution times — even in the context of more realistic and complex dynamic model economies.

Further Resources

Papers cited in this chapter:

  • Black, Fischer and Myron Scholes (1973): “The Pricing of Options and Corporate Liabilities.” Journal of Political Economy, Vol. 81, No. 3, 638-659.

  • Cox, John, Stephen Ross and Mark Rubinstein (1979): “Option Pricing: A Simplified Approach.” Journal of Financial Economics, Vol. 7, No. 3, 229–263.

  • Duffie, Darrell (1986): “Stochastic Equilibria: Existence, Spanning Number, and the `No Expected Gains from Financial Trade´ Hypothesis.” Econometrica, Vol. 54, No. 5, 1161-1183.

  • Longstaff, Francis and Eduardo Schwartz (2001): “Valuing American Options by Simulation: A Simple Least Squares Approach.” Review of Financial Studies, Vol. 14, No. 1, 113–147.

  • Merton, Robert (1973): “Theory of Rational Option Pricing.” Bell Journal of Economics and Management Science, Vol. 4, 141-183.

Books cited in this chapter:

  • Duffie, Darrell (1988): Security Markets — Stochastic Models. Academic Press, San Diego et al.

  • Glasserman, Paul (2004): Monte Carlo Methods in Financial Engineering. Springer Verlag, New York.

  • Hilpisch, Yves (2018): Python for Finance. 2nd ed., O’Reilly, Sebastopol et al.

  • Hilpisch, Yves (2015): Derivatives Analytics with Python. Wiley Finance.

  • Pliska, Stanley (1997): Introduction to Mathematical Finance. Blackwell Publishers, Malden and Oxford.

1 The parameters assumed in this chapter are from Longstaff and Schwartz (2001, table 1).

2 Note that only the numbers on and above the diagonal are relevant. Numbers below the diagonal can be ignored. They result from the specific vectorized operations implemented on the ndarray object.

3 Here, as also often seen in practice, there is a large number of cases for which the option expires worthless, that is, with a payoff of 0.

..................Content has been hidden....................

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