1. Black-Scholes-Merton PDE to Parabolic Heat Diffusion Transformation
The canonical Black-Scholes-Merton PDE governing option price $V(S, t)$ is:
$$\frac{\partial V}{\partial t} + \frac{1}{2}\sigma^2 S^2 \frac{\partial^2 V}{\partial S^2} + r S \frac{\partial V}{\partial S} - r V = 0$$Under logarithmic coordinate transformations $x = \log(S / K)$, dimensionless time $\tau = \frac{1}{2}\sigma^2(T - t)$, and scaling $V(S, t) = K e^{-\alpha x - \beta \tau} u(x, \tau)$ with $\alpha = \frac{1}{2}\left(\frac{2r}{\sigma^2} - 1\right)$ and $\beta = \alpha^2 + \frac{2r}{\sigma^2}$, the PDE transforms into the dimensionless 1D Parabolic Heat Equation:
$$\frac{\partial u}{\partial \tau} = \frac{\partial^2 u}{\partial x^2}$$2. Crank-Nicolson Discretization for Option Pricing
Discretizing spatial asset price grid $S_j$ and time steps $t_n$, Crank-Nicolson averages spatial central differences implicitly:
$$-\frac{1}{2}\alpha_j V_{j-1}^{n} + (1 - \beta_j) V_j^{n} - \frac{1}{2}\gamma_j V_{j+1}^{n} = \frac{1}{2}\alpha_j V_{j-1}^{n+1} + (1 + \beta_j) V_j^{n+1} + \frac{1}{2}\gamma_j V_{j+1}^{n+1}$$Achieves unconditional numerical stability with second-order convergence $\mathcal{O}((\Delta S)^2 + (\Delta t)^2)$.
3. Projected Successive Over-Relaxation (PSOR) for American Options
American options present a Free Boundary Linear Complementarity Problem (LCP) due to early exercise intrinsic payoff constraint $V(S, t) \ge h(S)$:
$$\min\left( -\frac{\partial V}{\partial t} - \mathcal{L}_{\text{BS}} V, \; V(S, t) - h(S) \right) = 0$$PSOR solves the linear system $\mathbf{A}\mathbf{V}^{n} = \mathbf{b}$ iteratively with over-relaxation factor $\omega \in (1, 2)$ while projecting pointwise onto early exercise payoff $h_j$:
$$V_j^{(k+1)} = \max\left( h_j, \; V_j^{(k)} + \omega \frac{b_j - \sum_{m < j} A_{jm} V_m^{(k+1)} - \sum_{m \ge j} A_{jm} V_m^{(k)}}{A_{jj}} \right)$$4. Python Verification: Crank-Nicolson European Put Option Solver
import numpy as np
from scipy.linalg import solve_banded
from scipy.stats import norm
def verify_crank_nicolson_put(S0: float = 100.0, K: float = 100.0, r: float = 0.05, sigma: float = 0.2, T: float = 1.0):
"""Solves Black-Scholes PDE via Crank-Nicolson finite differences against analytic BS Put."""
M = 200 # Asset price grid points
N_t = 200 # Time steps
S_max = 3.0 * K
dS = S_max / M
dt = T / N_t
S = np.linspace(0, S_max, M + 1)
V = np.maximum(K - S, 0.0) # Put terminal payoff
j = np.arange(1, M)
alpha = 0.25 * dt * (sigma**2 * j**2 - r * j)
beta = -0.5 * dt * (sigma**2 * j**2 + r)
gamma = 0.25 * dt * (sigma**2 * j**2 + r * j)
# Tridiagonal matrices M1 * V_new = M2 * V_old
ab_LHS = np.zeros((3, M - 1))
ab_LHS[0, 1:] = -gamma[:-1]
ab_LHS[1, :] = 1.0 - beta
ab_LHS[2, :-1] = -alpha[1:]
for _ in range(N_t):
rhs = alpha * V[:-2] + (1.0 + beta) * V[1:-1] + gamma * V[2:]
# Boundary conditions at S=0 (V=K*exp(-rt)) and S=S_max (V=0)
rhs[0] += alpha[0] * 2.0 * K
V[1:-1] = solve_banded((1, 1), ab_LHS, rhs)
# Interpolate at S0
pde_price = np.interp(S0, S, V)
# Exact Black-Scholes Put
d1 = (np.log(S0/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
d2 = d1 - sigma*np.sqrt(T)
bs_put = K * np.exp(-r*T) * norm.cdf(-d2) - S0 * norm.cdf(-d1)
print(f"Exact Black-Scholes Put Price: {bs_put:.4f}")
print(f"Crank-Nicolson PDE Solved Price: {pde_price:.4f}")
print(f"Absolute Numerical PDE Error: {abs(pde_price - bs_put):.2e}")
if __name__ == "__main__":
verify_crank_nicolson_put()