Processing math: 100%

The Crank-Nicolson Method

For the Crank-Nicolson method we shall need:

  • All parameters for the option, such as X and S0 etc.
  • The number of divisions in stock, jMax, and divisions in time iMax
  • The size of the divisions ΔS and Δt
  • Vectors to store:
    • stock price
    • old option values
    • new option values
    • three diagonal elements (a, b, and c)
    • the right hand side of the matrix equation

Again, the easiest thing to do is just to declare these at the top of the program before you start doing anything with them.

A sample program structure is given in the example code click here and the final solution is click here. I have initialised the variables and put the loop in to save time. At each timestep we must solve the matrix equation AV=b. Since the matrix is tridiagonal we need only use three vectors to store all values in the matrix A.

Example - A European Put

Now, assuming that our vectors are indexed from 0 to iMax (and therefore have iMax+1 elements) I will go through an example for a European put with σ=0.4, r=0.05, X=2, dS=1, dt=0.25, and jMax=4. Note here that these calculations could have been done by hand, and trying to replicate an example of this scale with a code is a good way to start, and also to check for bugs/errors.

  1. First copy the example code from the web, check initial values and the initial setup phase to assign values to vectors such as stock values (that remain constant throughout) and the final payoff condition to the option values are all working correctly.
  2. Check that you understand the time loop from the example code, inside the loop the code is broken into two sections, matrix setup and matrix solver, both of which may be written into functions later. The following section will outline with an example how to setup and solve the matrix equations.
In [1]:
#include <iostream>
#include <fstream>
#include <cmath>
#include <vector>
using namespace std;
In [2]:
// declare and initialise Black Scholes parameters
double S0=1.974,X=2.,T=1.,r=0.05,sigma=0.4;
// declare and initialise grid paramaters 
int iMax=4,jMax=4;
// declare and initialise local variables (ds,dt)
double S_max=2*X;
double dS=S_max/jMax;
double dt=T/iMax;
// create storage for the stock price and option price (old and new)
vector<double> S(jMax+1),vOld(jMax+1),vNew(jMax+1);
// setup and initialise the stock price 
for(int j=0;j<=jMax;j++)
{
  S[j] = j*dS;
}
// setup and initialise the final conditions on the option price 
for(int j=0;j<=jMax;j++)
{
    vOld[j] = max(X-S[j],0.);
    vNew[j] = max(X-S[j],0.);
    cout << iMax << " " << j << " " << S[j] << " " << vNew[j] << " " << vOld[j] << endl;
}
// start looping through time levels
for(int i=iMax-1;i>=0;i--)
{
    // declare vectors for matrix equations
  
    // set up matrix equations a[j]=
  
    // solve matrix equations with SOR
    
    // output results
    for(int j=0;j<=jMax;j++)
    {
        cout << i << " " << j << " " << S[j] << " " << vNew[j] << " " << vOld[j] << endl;
    }
    // set old=new 
    
}
// finish looping through time levels
  
// output the estimated option price
4 0 0 2 2
4 1 1 1 1
4 2 2 0 0
4 3 3 0 0
4 4 4 0 0
3 0 0 2 2
3 1 1 1 1
3 2 2 0 0
3 3 3 0 0
3 4 4 0 0
2 0 0 2 2
2 1 1 1 1
2 2 2 0 0
2 3 3 0 0
2 4 4 0 0
1 0 0 2 2
1 1 1 1 1
1 2 2 0 0
1 3 3 0 0
1 4 4 0 0
0 0 0 2 2
0 1 1 1 1
0 2 2 0 0
0 3 3 0 0
0 4 4 0 0

I tend to use a, b and c to represent the tridagonal matrix A, and d for the right hand side of the equation. Just remember that it makes things easier if you are consistent with your notation.

  1. Declare vector storage for the terms a, b, c and d, and initialise them to be jMax+1 in size.
  2. Set up the matrix values according to the problem.
In [3]:
for(int i=iMax-1;i>=0;i--)
{
    // declare vectors for matrix equations
    vector<double> a(jMax+1),b(jMax+1),c(jMax+1),d(jMax+1);
    // set up matrix equations a[j]=
    a[0] = 0.;b[0] = 1.;c[0] = 0.;
    d[0] = X*exp(-r*(iMax-i)*dt);
    for(int j=1;j<jMax;j++)
    {
        a[j] = 0.25*(sigma*sigma*j*j-r*j);
        b[j] =-0.5*sigma*sigma*j*j - 0.5*r - 1./dt;
        c[j] = 0.25*(sigma*sigma*j*j+r*j);
        d[j] =-a[j]*vOld[j-1] - (b[j]+2./dt)*vOld[j] - c[j]*vOld[j+1];
    }
    a[jMax] = 0.;b[jMax] = 1.;c[jMax] = 0.;
    d[jMax] = 0.;
    // output matrix
    for(int j=0;j<=jMax;j++)
    {
        cout << j << " " << a[j] << " " << b[j] << " " << c[j] << " " << d[j] << endl;
    }
    // just check output at first step
    break;
}
0 0 1 0 1.97516
1 0.0275 -4.105 0.0525 -3.95
2 0.135 -4.345 0.185 -0.135
3 0.3225 -4.745 0.3975 -0
4 0 1 0 0

Matrix Solver

Still inside the time loop, we now need to solve the matrix equation. This can be done using either iterative methods (SOR) or direct methods (Thomas's solver). We shall go through the SOR method here as it is more easily adapted to American options.

The method is as follows:-

  1. update the value of Vij for each j using the SOR formula
  2. check whether the residual is less than the tolerance:
    • Yes - exit as we have found the solution
    • No - go back to step one

The SOR Loop

Create a loop in your code after the matrix has been setup, to iterate with SOR until a solution is found.

// store the value of sor OUTSIDE the loop to check on convergence
int sor;
for(sor=0;sor<iterMax;sor++)
{
//implement sor in here...

}

where iterMax is the maximum number of iterations you permit (may need to be as high as 10000). Like the simple example from before, you need to write an equation to update the value at the boundary j=0, then a loop of equations to update all values in between 1j<jMax and then another at the boundary j=jMax.

In [4]:
// reset initial condition
for(int j=0;j<=jMax;j++)
{
    vOld[j] = max(X-S[j],0.);
    vNew[j] = max(X-S[j],0.);
}
// run through timesteps
for(int i=iMax-1;i>=0;i--)
{
    cout << "## Solving at timestep "<< i << "\n";
    // declare vectors for matrix equations
    vector<double> a(jMax+1),b(jMax+1),c(jMax+1),d(jMax+1);
    // set up matrix equations a[j]=
    a[0] = 0.;b[0] = 1.;c[0] = 0.;
    d[0] = X*exp(-r*(iMax-i)*dt);
    for(int j=1;j<jMax;j++)
    {
        a[j] = 0.25*(sigma*sigma*j*j-r*j);
        b[j] =-0.5*sigma*sigma*j*j - 0.5*r - 1./dt;
        c[j] = 0.25*(sigma*sigma*j*j+r*j);
        d[j] =-a[j]*vOld[j-1] - (b[j]+2./dt)*vOld[j] - c[j]*vOld[j+1];
    }
    a[jMax] = 0.;b[jMax] = 1.;c[jMax] = 0.;
    d[jMax] = 0.;
    // solve with SOR method
    int sor,iterMax=10;
    double tol = 1.e-8,omega=1.;
    for(sor=0;sor<iterMax;sor++)
    {
        double error=0.;
        cout << sor << " == \n";
        // implement sor in here
        {
            double y = (d[0] - c[0]*vNew[1])/b[0];
            vNew[0] = vNew[0] + omega*(y-vNew[0]); 
            cout << " ( " << vNew[0] << " , ";
        }
        for(int j=1;j<jMax;j++)
        {
            double y = (d[j] - a[j]*vNew[j-1] - c[j]*vNew[j+1])/b[j];
            vNew[j] = vNew[j] + omega*(y-vNew[j]); 
            cout << vNew[j] << " , ";
        }
        {
            double y = (d[jMax] - a[jMax]*vNew[jMax-1])/b[jMax];
            vNew[jMax] = vNew[jMax] + omega*(y-vNew[jMax]); 
            cout << vNew[jMax] << " )\n";
        }
        // calculate residual norm ||r|| as sum of absolute values
        error += fabs(d[0] - b[0]*vNew[0] - c[0]*vNew[1]);
        for(int j=1;j<jMax;j++)
            error += fabs(d[j] - a[j]*vNew[j-1] - b[j]*vNew[j] - c[j]*vNew[j+1]);
        error += fabs(d[jMax] - a[jMax]*vNew[jMax-1] - b[jMax]*vNew[jMax]);
        // make an exit condition when solution found
        if(error<tol)
          break;
    }
    cout << " Solved after " << sor << " iterations.\n";
    // set old=new
    vOld=vNew;
}
## Solving at timestep 3
0 == 
 ( 1.97516 , 0.975473 , 0.0613783 , 0.00417166 , 0 )
1 == 
 ( 1.97516 , 0.976258 , 0.0615803 , 0.00418539 , 0 )
2 == 
 ( 1.97516 , 0.976261 , 0.061581 , 0.00418543 , 0 )
3 == 
 ( 1.97516 , 0.976261 , 0.061581 , 0.00418543 , 0 )
 Solved after 3 iterations.
## Solving at timestep 2
0 == 
 ( 1.95062 , 0.954192 , 0.112138 , 0.0146782 , 0 )
1 == 
 ( 1.95062 , 0.954839 , 0.112605 , 0.0147099 , 0 )
2 == 
 ( 1.95062 , 0.954845 , 0.112606 , 0.01471 , 0 )
3 == 
 ( 1.95062 , 0.954845 , 0.112606 , 0.01471 , 0 )
 Solved after 3 iterations.
## Solving at timestep 1
0 == 
 ( 1.92639 , 0.934851 , 0.15469 , 0.0282579 , 0 )
1 == 
 ( 1.92639 , 0.935389 , 0.155283 , 0.0282983 , 0 )
2 == 
 ( 1.92639 , 0.935397 , 0.155285 , 0.0282984 , 0 )
3 == 
 ( 1.92639 , 0.935397 , 0.155285 , 0.0282984 , 0 )
 Solved after 3 iterations.
## Solving at timestep 0
0 == 
 ( 1.90246 , 0.917166 , 0.190595 , 0.0429205 , 0 )
1 == 
 ( 1.90246 , 0.917618 , 0.191231 , 0.0429637 , 0 )
2 == 
 ( 1.90246 , 0.917626 , 0.191233 , 0.0429639 , 0 )
3 == 
 ( 1.90246 , 0.917626 , 0.191233 , 0.0429639 , 0 )
 Solved after 3 iterations.

Once we have a solution in terms of S and V at particular points, we can get the value of the option at any value of S using interpolation. So to get the value at S0=1.974 we do:

In [5]:
double optionValue;
{
    int jStar=S0/dS;
    double sum=0.;
    sum+=(S0 - S[jStar])/dS * vNew[jStar+1];
    sum+=(S[jStar+1] - S0)/dS * vNew[jStar];
    optionValue = sum;
}
cout << "Value of the option V(S="<<S0<<") = " <<  optionValue << "\n";
Value of the option V(S=1.974) = 0.21012

and if we want the value at another value (say S0=2.141) we have

In [6]:
S0=2.141;
{
    int jStar=S0/dS;
    double sum=0.;
    sum+=(S0 - S[jStar])/dS * vNew[jStar+1];
    sum+=(S[jStar+1] - S0)/dS * vNew[jStar];
    optionValue = sum;
}
cout << "Value of the option V(S="<<S0<<") = " <<  optionValue << "\n";
Value of the option V(S=2.141) = 0.170327

Finally, you want to wrap all this code up into a function so that it can be used again and again.