Path: blob/master/2.1 Black-Scholes PDE and sparse matrices.ipynb
1675 views
The Black-Scholes PDE
In this notebook I want to show how to solve the famous Black-Scholes (BS) Partial Differential Equation (PDE).
I decided to use a fully implicit method, because it is an efficient and stable method. The illustrated approach can be used to solve more complex problems, such as the pricing of different types of derivatives (barrier options, digital options, American options, etc) with small modifications.
Contents
The BS PDE
Ok ok... Nihil novi sub sole. The PDE derivation can be found in every textbooks... I know.
I suggest the interested reader to have a look at [1] for a clear exposition. If you are already familiar with these concepts, maybe it can be interesting for you to have a look at the notebook A3, where I present the derivation of the P(I)DE for general exponential Lévy processes. The BS PDE appears as a special case.
PDE in log-variables
Let us consider the geometric Brownian motion (GBM), described by the SDE:
Let be the value of a European call option.
By the martingale pricing theory, in an arbitrage-free market there exists an equivalent martingale measure such that the discounted option price is a -martingale. (Remember that under the drift is replaced by the risk free drift .)
The PDE for the option price is:
where is the infinitesimal generator of . In the Black-Scholes model, the measure is unique and the infinitesimal generator under this measure is:
In order to simplify the computations, it is better to pass to the log-variable . This change corresponds to the following change in the operators:
In log-variables the BS PDE is:
For an option with strike and maturity , the boundary conditions are:
CALL:
Terminal:
Lateral:
PUT:
Terminal:
Lateral:
Derivative approximation
Finite difference methods are a technique for obtaining numerical solutions of PDEs. The idea underlying finite-difference methods is to replace the partial derivatives occurring in the PDE by finite difference approximations. If we assume that is a smooth function, we can use the Taylor series expansion near the point of interest. For a we can write
An analogous approximation can be done for with .
If we want to approximate the partial derivative with respect to time, we obtain the following finite difference approximation
also called forward difference, since the differencing is in the forward direction. We can also consider the backward difference
and the central difference
The use of the forward and backward difference approximation leads to the explicit and implicit finite difference schemes respectively. The central difference is not used for the time variable because it leads to bad numerical schemes. But it is common to use it for the space variable.
For second order derivatives, such as , we can use the symmetric central difference approximation for a :
If you need more details, have a look at [2].
Implicit discretization
First we have to restrict the theoretical infinite domain to the finite region , with .
The next step is to replace by a discrete grid:
For , define the discrete time step such that . For , define the discrete space step such that .
The grid is divided into equally spaced nodes of distance in the x-axis, and of distance in the t-axis.
The mesh points have the form . At this point we concern ourselves only with the values of on the mesh nodes. We call
We apply the backward discretization (implicit scheme) for the time derivative, and a central discretization for the first order space derivative.
We are interested in the value of V at time . We know the values corresponding to the terminal conditions. The algorithm consists in finding the values given the knowledge of the values .
Comment: A common practice is to invert the time flow i.e. introducing a new time variable and change the variables in the PDE. It follows that the terminal conditions become initial conditions, and the problem becomes a forward problem. However, I am not using this change of variable in this notebook.
The discretized equation becomes
Rearranging the terms:
We can rename the coefficients such that:
and write it in matrix form:
The system
can be solved easily for by inverting the matrix .
Introduction to sparse matrices
A sparse matrix is a matrix in which most of the entries are zeros.
If most of the elements are nonzero, then the matrix is considered dense.
A dense matrix is typically stored as a two-dimensional array. Each entry in the array represents an element of the matrix and is accessed by the two indices i and j.
Since a sparse matrix is almost empthy, storing all the zeros is a waste of memory. For this reason it is more efficient to use different data structures from the usual 2D arrays. More information on these can be found in the wiki page.
Have a look at the manual page of the scipy.sparse library. link.
There are many ways to represent a sparse matrix, Scipy provides seven of them:
Block Sparse Row (BSR)
Coordinate list (COO)
Compressed Sparse Column (CSC)
Compressed Sparse Row (CSR)
DIAgonal storage (DIA)
Dictionary Of Keys (DOK)
List of lists (LIL)
Each format has its pros and cons. But we can roughly divide them in two groups:
CSR and CSC are better for efficient matrix operations.
All the others are better for the initial construction the sparse matrix.
The good thing is that it is very easy to convert one representation into one other. Here I pasted some recomandations from the scipy manual page:
To construct a matrix efficiently, use either dok_matrix or lil_matrix. The lil_matrix class supports basic slicing and fancy indexing with a similar syntax to NumPy arrays. The COO format may also be used to efficiently construct matrices. Despite their similarity to NumPy arrays, it is strongly discouraged to use NumPy functions directly on these matrices because NumPy may not properly convert them for computations, leading to unexpected (and incorrect) results. If you do want to apply a NumPy function to these matrices, first check if SciPy has its own implementation for the given sparse matrix class, or convert the sparse matrix to a NumPy array (e.g. using the toarray() method of the class) first before applying the method. To perform manipulations such as multiplication or inversion, first convert the matrix to either CSC or CSR format. The lil_matrix format is row-based, so conversion to CSR is efficient, whereas conversion to CSC is less so. All conversions among the CSR, CSC, and COO formats are efficient, linear-time operations.
Example: Matrix multiplication and operations
Feel free to convert the sparse matrix into a different representations and pprint it.
Now let's see some math operations.
Depending on what you prefer to use, you can call toarray or todense to convert the sparse matrix in a np.array or in a np.matrix.
Be careful because the the '*' operator represents the element-wise product for numpy arrays and the scalar product for the numpy matrices.
I prefer to use the numpy arrays, and therefore I will use the @ operator for the scalar product operation. This operator handles very well hybrid multiplications between sparse and dense matrices.
Example: Diagonal matrices
A diagonal matrix can be initialized as follows, by giving as the second argument a list of diagonals to be filled, and as first argument the list of values in the respective diagonal. (enter sparse.diags??
in a cell for more information).
Example: Solving the linear system:
Using spsolve
Using splu (based on LU decomposition link )
A SparseEfficiencyWarning is raised if the sparse matrix is not converted in the most efficient format. So it is better to convert from "dia" to "csr".
Numerical solution of the PDE
Ok, let us solve the vector equation derived in the section "Implicit discretization".
In this case we consider a call option with strike , maturity . The stock price is not relevant for the algorithm. We will use it in the end to compute the value of the option for .
A common practice is to choose the computational region between and . Then we have and .
The values of the parameter are:
Comment:
You may be tempted to think:
Why not calculating the inverse of the matrix D? Something like this:
Well... you can try... It works, but it is very slow!
The reason is that inv computes the sparse inverse of A (using the same spsolve method). However the inverse of a tridiagonal matrix, in general, is not sparse!! For this reason, all the efficiency gained by using scipy.sparse is lost.
We will see very soon that the right approach is instead to use splu.
Here we are!
we can now find the value for and plot the curve at .
In the BS_pricer class there are more methods implemented. Let us have a look:
By default the method PDE_price uses the LU solver. Let us compare the execution times of three implementations:
splu
Thomas: I wrote a wrapper of a LAPACK function. code
spsolve
SOR: I wrote the SOR solver in cython, such that it would be easily integrated with the python code. However it is still very slow. Cython code From this cell it is also not possible to modify the SOR parameters (if you want to modify it, you need to enter the BS.PDE_price method in BS_pricer class ).
The SOR and Thomas algorithms are explained in the notebook A1 and the code optimization for the SOR algorithm is the topic of the notebook A2. Further comparisons (with cython and C code) are in the notebook A2.
Thomas and SPLU are super fast!!!
A put option can be priced by changing the terminal and boudary conditions. You can read the implementation code inside "BS_pricer".
References
[1] Björk Tomas (2009). Arbitrage theory in continuous time, Oxford Financial Press.
[2] Wilmott Paul (1994). Option pricing: Mathematical models and computation. Oxford Financial Press.