Implicit Finite Difference Method (FDM) — Part 2
We put our Ax=b solver to work and actually price a European call with Implicit FDM, walking through how the matrix equations generalize as we slice the grid into more pieces!
Last time we picked up some serious firepower!!!! For the systems here — with nonzero pivots — we’ve got a function that solves Ax = b!!!!! Like, we are LOADED. Cavalry’s here lol
So now let’s actually price a European call with Implicit FDM! For the code right now, let’s keep it simple: it’s the 3x3 matrix equation we derived back when we split the grid into 5 points — yeah, that one:

Let’s code this thing up. Wait — not “code it ourselves” exactly. More like, read and understand the code that’s given. Let’s do that, heh. Here’s the code:

I tried it in Python too! heh heh heh


And here’s how I worked through it on my own:


OK, now let’s study the code that generalizes — the one that gives us all the $f_{i,j}$ values. But before that, there’s one thing to go over one more time.
Quick recap. Implicit FDM was: approximate the Black-Scholes PDE with central difference for the first-order derivative in $S$, and forward difference for the first-order derivative in $t$. And when we did that:
![]()
this is what fell out — and the number of unknowns depended on how many pieces we cut $S$ into. The unknowns here aren’t $f_{i+1, j}$ — they’re $f_{i, j+1}$, $f_{i, j}$, $f_{i, j-1}$.
With 5 grid points, those 3 things were the unknowns. 3 unknowns → we needed 3 equations…
Ahahaha, OK, so with 7 grid points we get 5 unknowns, right? And we’d need 5 equations to pin all 5 of them down??
Let’s quickly see where those 5 come from and move on.
![]()
Just write it out and plug stuff in. The boundaries will be $j=1$, $j=7$ and the time step $i+1$~~~ let’s just say that (I’m not going to be precise about $i$). Writing out the 5 equations that pull in boundary values:

So we’ve got these 5 equations. And if we pick out just the unknowns from these:

These are the unknowns, right??? So the unknown $x$-vector is that, and we can write the matrix equation like:

Yeah, something like that… Now that it’s written out, I can kind of see the pattern.
And through this process we find $f_{i, \,\sim}$, then through the next pass we find $f_{i-1, \,\sim}$, and if we keep stepping backward all the way down to $i=0$… boom, we’ve filled out every $f_{i,j}$ on the grid, right??
Oh — and one thing to watch carefully: the $a_j$, $b_j$, $c_j$ we need in the matrix aren’t $j=1$ through $7$, they’re $j=2$ through $6$!!!! Not $j=1$ through $j=7$ — from 2 through 6!!!!!! We need everything from just above the boundary to just below the boundary~~~
Also, the shape of the matrix is worth a careful look.

It’s like $a$, $b$, $c$ laid out along the diagonals like this!!!~~
Before the code, a naming heads-up: the seven-point schematic labels the $f_{i,j+1}$ coefficient $a_j$ and the $f_{i,j-1}$ coefficient $c_j$. In the code below those names are swapped: $A$ multiplies $f_{i,j-1}$ and $C$ multiplies $f_{i,j+1}$. The matrix order follows that code convention.
OK then — let’s study the code that finds all the $f_{i,j}$! heh heh heh


Did this one in Python too! heh heh heh


This is exactly the code!!! And here’s how I read it:



We can also pull out the Greeks pretty easily here, and yeah, we covered the Greeks-extraction part in class too.
Once you know the principle, it’s a piece of cake — there’s no deep interpretation or anything lol More like — “oh nice, we can also check the Greeks this way~~~~” — that level??
For the Greeks in detail, see The Greeks (Derivatives I Studied #20).



Python code
import numpy as np
import math
def Solve_SE(matA, matb):
rowA = len(matA)
colA = len(matA.T)
rowB = len(matb)
if (rowA != colA) | (rowB != colA):
raise NotImplementedError
else:
pass
L = np.zeros((rowA, colA))
U = np.zeros((rowA, colA))
for j in range(0, len(L.T)):
for i in range(0, len(L)):
if j==0:
L[i, j] = matA[i, j]
U[j, i] = matA[j, i] / L[0, 0]
else:
if i < j:
L[i, j] = 0
else:
Lsum = 0
for k in range(0, j):
Lsum = Lsum + L[i, k]*U[k, j]
L[i, j] = matA[i, j] - Lsum
if i < j:
U[j, i] = 0
elif i == j:
U[j, i] = 1
else:
Usum = 0
for k in range(0, i):
Usum = Usum + L[j, k]*U[k, i]
U[j, i] = (matA[j, i] - Usum) /L[j, j]
modB = np.zeros((3, len(matb.T)))
modB[0] = matb[0] / L[0, 0]
for i in range(0, len(modB)):
sumofB = 0
for k in range(0, i):
sumofB = sumofB + L[i, k]*modB[k]
modB[i] = (matb[i] - sumofB) / L[i, i]
x = np.zeros((3, len(modB.T)))
for no in range(len(x)-1, 0-1, -1):
sum=0
for i in range(0, len(x)):
sum = sum + U[no, i] * x[i]
x[no] = modB[no] - sum
return x
k=50; r=0.05; t=0.5; v=0.3; smax=100
m=4; n=4
ds = smax / m
dt = t/n
f = np.zeros((n+1, m+1))
for j in range(1, m):
f[n, j] = max(j*ds - k, 0)
for i in range(0, n+1):
f[i, 0] = max(0, 0)
for i in range(0, n+1):
f[i, m] = max(m*ds - k*math.exp(-r*(n-i)*dt), 0)
j=3
a3 = 0.5 * dt * (r*j - (v**2)*(j**2))
b3 = 1 + ((v*j)**2)*dt + r*dt
c3 = 0.5 * dt * (-r*j - (v**2)*(j**2))
j=2
a2 = 0.5 * dt * (r*j - (v**2)*(j**2))
b2 = 1 + ((v*j)**2)*dt + r*dt
c2 = 0.5 * dt * (-r*j - (v**2)*(j**2))
j=1
a1 = 0.5 * dt * (r*j - (v**2)*(j**2))
b1 = 1 + ((v*j)**2)*dt + r*dt
c1 = 0.5 * dt * (-r*j - (v**2)*(j**2))
matA = np.zeros((3, 3))
matA[0, 0] = b3; matA[0, 1] = a3; matA[0, 2] = 0
matA[1, 0] = c2; matA[1, 1] = b2; matA[1, 2] = a2
matA[2, 0] = 0; matA[2, 1] = c1; matA[2, 2] = b1
matb = np.zeros((3, 1))
matb[0] = f[4, 3] - c3*f[3, 4]
matb[1] = f[4, 2]
matb[2] = f[4, 1] - a1*f[3, 0]
matx = Solve_SE(matA, matb)
for i in range(len(matx)):
print('option price: ', round(float(matx[i, 0]), 4))
import numpy as np
import math
def Solve_SE(matA, matb):
rowA = len(matA)
colA = len(matA.T)
rowB = len(matb)
if (rowA != colA) | (rowB != colA):
raise NotImplementedError
else:
pass
L = np.zeros((rowA, colA))
U = np.zeros((rowA, colA))
for j in range(0, len(L.T)):
for i in range(0, len(L)):
if j==0:
L[i, j] = matA[i, j]
U[j, i] = matA[j, i] / L[0, 0]
else:
if i < j:
L[i, j] = 0
else:
Lsum = 0
for k in range(0, j):
Lsum = Lsum + L[i, k]*U[k, j]
L[i, j] = matA[i, j] - Lsum
if i < j:
U[j, i] = 0
elif i == j:
U[j, i] = 1
else:
Usum = 0
for k in range(0, i):
Usum = Usum + L[j, k]*U[k, i]
U[j, i] = (matA[j, i] - Usum) /L[j, j]
modB = np.zeros((len(matb), 1))
modB[0] = matb[0] / L[0, 0]
for i in range(0, len(modB)):
sumofB = 0
for k in range(0, i):
sumofB = sumofB + L[i, k]*modB[k]
modB[i] = (matb[i] - sumofB) / L[i, i]
x = np.zeros((len(modB), 1))
for no in range(len(x)-1, 0-1, -1):
sum=0
for i in range(0, len(x)):
sum = sum + U[no, i] * x[i]
x[no] = modB[no] - sum
return x
k=50; r=0.05; t=0.5; v=0.3; smax=100
m=10; n=10
ds = smax / m
dt = t/n
matdim=m-2
f = np.zeros((n+1, m+1))
matA = np.zeros((matdim+1, matdim+1))
matb = np.zeros((matdim+1, 1))
A = np.zeros((matdim+1, 1))
B = np.zeros((matdim+1, 1))
C = np.zeros((matdim+1, 1))
for j in range(1, m):
f[n, j] = max(j*ds - k, 0)
for i in range(0, n+1):
f[i, 0] = max(0, 0)
for i in range(0, n+1):
f[i, m] = max(m*ds - k*math.exp(-r*(n-i)*dt), 0)
for j in range(m-1, 1-1, -1):
A[j-1] = 0.5 * dt * (r * j - (v ** 2) * (j ** 2))
B[j-1] = 1 + ((v * j) ** 2) * dt + r * dt
C[j-1] = 0.5 * dt * (-r * j - (v ** 2) * (j ** 2))
for i in range(n-1, 0-1, -1):
mm = matdim
for ii in range(0, matdim+1):
for jj in range(0, matdim+1):
if ii == jj + 1:
matA[ii, jj] = C[mm, 0]
elif ii == jj:
matA[ii, jj] = B[mm, 0]
elif ii+1 == jj:
matA[ii, jj] = A[mm, 0]
else:
matA[ii, jj] = 0
mm=mm-1
matb[0] = f[i+1, m-1] - C[matdim]*f[i,m]
matb[matdim] = f[i+1, 1] - A[0]*f[i,0]
for jj in range(matdim-1, 1-1, -1):
matb[jj] = f[i+1, m-1-jj]
matx = Solve_SE(matA, matb)
jj=m-1
for ii in range(len(matx)):
f[i, jj]=matx[ii, 0]
jj=jj-1
for j in range(m+1):
print("stock price: ", j*ds, "option price: ", round(f[0, j], 2))
Heads-up. The figures here are the original notes, slips and all. A few things to watch for:
- In the general VBA program (typed and handwritten), the right-hand side fills row jj with the wrong grid row: it should be matb(jj) = f(i + 1, m - 1 - jj), because row 0 of the matrix is the top grid row (the handwritten note’s numbering of b is reversed for the same reason). The code in the post is corrected. With it the prices for S = 0, 10, …, 100 are 0.00, 0.00, 0.00, 0.07, 0.79, 4.18, 12.04, 21.45, 31.30, 41.25, 51.23, not the 0.65, 13.98, 4.19, … printed in the screenshots; For comparison, Black-Scholes gives 0.88 at 40, 4.82 at 50 and 12.23 at 60. This coarse grid still has a noticeable error, especially near the strike.
- In the 3 x 3 example (typed and handwritten), the last entry on the right is f4,1 - a1 f3,0, as the code has it, not f4,1 - c1 f3,0 (f3,0 is 0, so the numbers are unaffected); in the row equations for j = 3 and j = 2 the middle coefficients are b3 and b2, not b4 and b3.
- On the matrix size: with m = 10 there are 11 grid points in S and 9 unknowns, so the system is (m - 1) x (m - 1), and matdim = m - 2 = 8 is its last index (arrays start at 0). The code is right.
- In the five-equation schematic, the j = 2 middle coefficient is b2, not b3. The handwritten Greeks program inherits the same reversed middle-RHS indexing as the general VBA program: use f(i + 1, m - 1 - jj). The first three-price example only solves the first backward time step, t = 0.375, not time zero.
Comments
Discussion happens via GitHub Discussions. You'll need a GitHub account to comment.