Finite Difference — Explicit Scheme
Motivation: why this matters in quant finance
Finite difference methods (FDM) discretise both space (asset price) and time (calendar) and march the option value backwards from expiry. They handle early exercise cleanly (compare hold value to exercise value at every node), they handle barriers exactly (kill the value at barrier nodes), and they handle local volatility surfaces transparently.
The informal idea
The Black-Scholes PDE for a European option :
with terminal condition payoff.
Discretise: for ; for . Let .
Approximate derivatives with central differences in and forward difference in (looking backward from expiry):
Substituting into the PDE and rearranging gives an explicit recurrence:
where are coefficients depending on . Each grid value at time depends only on three values at time . Computing one time step is .
Formal scheme
Defining the index-based form: at grid point , the coefficients are
So .
Boundary conditions:
- for a call (deeply ITM).
- for a call.
Terminal condition: .
March backwards from to .
Stability constraint
Equivalently, (von Neumann analysis).
For a typical setup with : . For , that's time steps just to stay stable. Halving requires quadrupling — fine grids in force tiny time steps.
Algorithm and example
import numpy as np
def fdm_explicit_call(S0, K, T, r, sigma, M=200, S_max=300, theta=1.0):
"""Explicit FDM for European call. Returns V(S0, 0)."""
dS = S_max / M
# CFL-safe dt
dt = 0.9 * 1.0 / (sigma**2 * M**2)
N = int(np.ceil(T / dt))
dt = T / N # adjust for clean stepping
# Grid
S = np.linspace(0, S_max, M+1)
V = np.maximum(S - K, 0) # terminal condition
# Coefficients (vectorised)
i = np.arange(1, M) # interior indices
a = 0.5 * dt * (sigma**2 * i**2 - r * i)
b = 1 - dt * (sigma**2 * i**2 + r)
c = 0.5 * dt * (sigma**2 * i**2 + r * i)
for n in range(N):
V_new = V.copy()
V_new[1:M] = a * V[0:M-1] + b * V[1:M] + c * V[2:M+1]
V_new[0] = 0 # boundary at S = 0
V_new[M] = S_max - K * np.exp(-r * (T - (n+1)*dt)) # boundary at S_max
V = V_new
return np.interp(S0, S, V)
price = fdm_explicit_call(100, 100, 1.0, 0.05, 0.2)
print(f"FDM explicit price: {price:.4f}")
# FDM explicit price: 10.4530
# Black-Scholes: 10.4506Within \0.003M = 200$.
Key properties
- Conditionally stable. Need for stability — a strong constraint that often dominates compute time.
- Easy to code. Three lines per time step. No matrix inversions.
- Accuracy: . First-order in time, second-order in space.
- Greeks for free. Once the value grid is computed, comes from finite differences on the grid; from second differences. No additional simulation needed.
- Boundary conditions matter. Wrong truncation, or wrong upper boundary value, can introduce large errors. Truncation reflection: place for typical vol.
- American options. Modify the update: . This is a simple obstacle PDE ("LCP").
Worked example: convergence rates
Same call as before, vary grid size:
for M in [50, 100, 200, 400, 800]:
p = fdm_explicit_call(100, 100, 1.0, 0.05, 0.2, M=M, S_max=400)
print(f"M={M:>4d}: price = {p:.6f}, error = {abs(p - 10.4506):.6f}")M= 50: price = 10.4624, error = 0.0118
M= 100: price = 10.4537, error = 0.0031
M= 200: price = 10.4513, error = 0.0007
M= 400: price = 10.4508, error = 0.0002
M= 800: price = 10.4506, error = 0.0000Halving reduces error by — confirms second-order spatial convergence. Time-step is automatically scaled by CFL.
Common confusions and pitfalls
- Forgetting CFL. Setting too large produces oscillations that grow exponentially. Result: . Always check stability before running.
- Wrong upper boundary. is approximated; using instead of for a call gives nonsense for close to .
- Coordinates in vs . The PDE has which makes coefficients depend on . Transforming to gives a constant-coefficient diffusion equation, which is much easier to discretise. Production code uses log-coordinates.
- Greeks from coarse grids. Computing delta from finite differences requires a finer grid than computing the price alone — the price is a smooth function, so coarse grids price well; the gradient amplifies grid noise.
- American vs European code reuse. American is European with one extra line:
V_new = np.maximum(V_new, payoff)at every step. Don't write two separate routines. - Multi-dimensional curse. For a 2D PDE (basket option, stoch vol), the grid has nodes and the explicit scheme requires — quickly becomes prohibitive. ADI (alternating direction implicit) or splitting methods are required.
Where this goes next
- FDM implicit/Crank-Nicolson — unconditionally stable, dominant in production.
- American options via projected SOR or PSOR.
- ADI for multi-asset PDEs.