convergence-lab / methods C++20 · MIT

The Black–Scholes PDE, solved four ways

Any of these methods will land near the analytic price. That is a weak test. What separates a correct implementation from a plausible one is the rate.

A scheme with the drift term mis-signed often prices close too, if you only ever run it at one grid size. So nothing here is checked at one grid size. Every method is refined across a range of resolutions, and the observed order is fitted as the slope of log₂(error) against log₂(resolution). A scheme that happens to be right at one resolution will not hold the right slope across all of them.

The resolutions plotted here are exactly those the C++ test suite asserts on. Fitting an order is sensitive to which points you fit it on, so sampling a different range for the picture would put a second, disagreeing set of numbers on this page.

Crank–Nicolson does not deliver its second order

This is the result the repository exists for. Crank–Nicolson is second-order accurate for smooth data, and a vanilla payoff is not smooth — it has a kink at the strike. That kink excites high-frequency modes, and Crank–Nicolson's amplification factor tends to -1 as frequency rises, so those modes are never damped. They only alternate sign.

time steps (log) — error vs the closed form (log)

The middle line is Crank–Nicolson as textbooks present it. It tracks the first-order guide, alongside implicit Euler, rather than the second-order one — and its error is the worst of the three at every resolution tested.

Two fully implicit half-steps at the start recover the missing order.

Implicit Euler damps those modes completely, so once they are gone the rest of the run is second order as advertised. At ten time steps that single change takes the error from 1.95 x 10^-1 to 8.09 x 10^-3 — a factor of twenty-four.

This is Rannacher start-up, and it is not a refinement. It is a correctness fix for a scheme that is otherwise quietly wrong on exactly the payoffs anyone would price with it. Both variants are kept in the test suite, so the failure reproduces, not only the fix:

FdConfig cfg;
cfg.scheme = FdScheme::CrankNicolson;
cfg.rannacher_steps = 0;   // 2 by default

Each method is documented in detail on its own page — choose one below, or read the full comparison table.

Lattices Tree methods Binomial CRR, Jarrow-Rudd, Tian / Boyle trinomial / American exercise Finite differences theta-family Explicit / Crank-Nicolson / Implicit / Rannacher start-up / Thomas solver Finite elements Galerkin P1 Weak form / P1 hat basis / Consistent vs lumped mass / FD equivalence Monte Carlo Simulation Exact / Euler / Milstein / Antithetic / Control variate / 95% interval

Every method, measured the same way

Method Observed Expected Status
Every plotted value, and the fitted order for each series

What is implemented

Three decisions worth the words

Everything is solved in log-spot. In S the PDE coefficients carry S and S^2, so truncation error varies across the grid and the matrix must be rebuilt as the grid changes. Substituting x = ln S makes every coefficient constant: one tridiagonal matrix serves every time step, and the scheme is uniformly accurate.

Monte Carlo refuses American exercise. Forward simulation cannot value an optimal stopping problem — that needs Longstaff-Schwartz regression. It throws rather than returning a number that looks like a price.

American finite differences use explicit projection, not a full linear-complementarity solve, so they are first order in time near the free boundary even under Crank-Nicolson. Stated because it is a real limitation, not omitted because it is inconvenient.

Running it

cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build
./build/convergence_tests      # 28 checks
./build/convergence_demo       # prices one contract every way, with errors

Header-only, no dependency beyond a C++20 compiler. Without CMake, build.sh and build.bat do the same job. Every figure and every point on this page is emitted by tools/convergence_study.cpp into convergence.csv and plotted from that file, so nothing shown here is a number typed in by hand.

Source on GitHub -> / Lattices Finite differences Finite elements Monte Carlo
<- All work