A standalone options pricing engine in modern C++17 with zero external dependencies — three independent pricing methods (closed-form Black-Scholes, Cox-Ross-Rubinstein binomial tree, Monte Carlo with Longstaff-Schwartz), the five standard Greeks, and a Newton-Raphson implied volatility solver, all cross-validated against each other by an automated convergence test suite (47 checks, all passing).
A pricing library: give it an option's terms and market inputs, it tells you what the option is worth and how that value reacts to market moves. Every number it produces is verified by at least one independent computation — the three pricing methods share no numerical code, so their agreement across in/out/at-the-money, short- and long-dated options is the proof the math is right. It is not a trading system: no market data connection, no broker, no strategy, no claim of generating alpha.
An option is a contract giving the right (not the obligation) to buy (call) or sell (put) an asset at a fixed strike price before or at an expiry date. The right has value — how much is one of the founding problems of quantitative finance, and its 1973 solution (Black-Scholes) won a Nobel Prize. Every derivatives desk, market maker, and risk system prices options thousands of times per second; volatility itself is quoted in Black-Scholes terms. Implementing the standard methods correctly — and proving they agree — is table stakes for working in derivatives.
| Method | What it is | When you'd use it | Complexity |
|---|---|---|---|
| Black-Scholes | Closed-form formula for European options under lognormal prices with constant volatility | Whenever it applies: it's exact (within its assumptions) and takes ~75 ns. Vol surface fitting, risk scans, real-time quoting | O(1) |
| Binomial tree (CRR) | Discretize time into N steps; price flows backward through a lattice of up/down moves | American exercise — at every node the tree checks "is exercising now worth more than holding?", which has no closed form | O(N²) time, O(N) memory |
| Monte Carlo | Simulate ~100,000 random price paths, average the discounted payoffs | Payoffs too complex for the other two: path-dependence, multiple underlyings. Slowest, but the only method that scales to hard payoffs | O(paths), error shrinks as 1/√paths |
Design details that matter:
- The tree is iterative (one rolling array), not recursive — a naive recursive tree is O(2^N) and dies around 30 steps. Node prices are precomputed with O(N) exponentials instead of O(N²).
- Monte Carlo uses antithetic variates: every random draw Z is paired with −Z. The pair average is unbiased (Z and −Z have the same distribution) and lower-variance (the payoffs are negatively correlated), and the standard error is computed over pair averages so it isn't understated.
- American options in Monte Carlo use Longstaff-Schwartz: regress realized continuation values on polynomials {1, x, x², x³} of moneyness (in-the-money paths only), and exercise when immediate payoff beats the fitted continuation value. The 4×4 least-squares system is solved by hand-written Gaussian elimination with partial pivoting — no Eigen, no QuantLib, deliberately.
The sensitivities a risk desk actually watches, all in closed form:
- Delta — how much the option price moves per $1 move in the underlying.
- Gamma — how much delta itself moves per $1 move (the curvature).
- Vega — price change per 1 percentage point of volatility (20% → 21%).
- Theta — value lost per calendar day just from time passing.
- Rho — price change per 1 percentage point of the risk-free rate.
Every closed form is cross-checked against central finite differences (bump the input, reprice, measure the slope) — two independent computations that agree to better than 1e-6 on every test set (tolerance 1e-4, asserted in debug builds and enforced by the test suite).
Volatility is the one Black-Scholes input nobody can observe. The market runs the formula backwards: option prices are quoted, and the volatility implied by them is the market's consensus forecast of how much the underlying will move. This solver inverts Black-Scholes with Newton-Raphson using the analytic vega as the derivative (3–6 iterations in practice), and falls back to bisection when vega collapses toward zero — which genuinely happens for deep in/out-of-the-money options. Prices below the no-arbitrage floor (where no implied vol exists) are rejected with a clear exception rather than garbage output.
From an actual run of convergence_test (Monte Carlo: 100,000 paths,
seed 42; tree: 500 steps). All three methods agree on every set:
| Parameter set | Type | Black-Scholes | Monte Carlo | |MC−BS| | 3·SE gate | Tree (500) | |Tree−BS| |
|---|---|---|---|---|---|---|---|
| ATM, 1y (S=100, K=100, σ=20%) | Call | 10.4506 | 10.4520 | 0.0015 | 0.0984 | 10.4466 | 0.0040 |
| Put | 5.5735 | 5.5770 | 0.0035 | 0.0627 | 5.5695 | 0.0040 | |
| ITM call, 1y (S=110, K=100) | Call | 17.6630 | 17.6598 | 0.0031 | 0.0931 | 17.6651 | 0.0021 |
| Put | 2.7859 | 2.7850 | 0.0009 | 0.0524 | 2.7880 | 0.0021 | |
| OTM call, 1y (S=90, K=100) | Call | 5.0912 | 5.0906 | 0.0006 | 0.0833 | 5.0896 | 0.0016 |
| Put | 10.2142 | 10.2154 | 0.0012 | 0.0504 | 10.2125 | 0.0016 | |
| Short-dated, 1 month (σ=25%) | Call | 3.0852 | 3.0866 | 0.0014 | 0.0315 | 3.0838 | 0.0014 |
| Put | 2.6694 | 2.6710 | 0.0017 | 0.0269 | 2.6680 | 0.0014 | |
| Long-dated, 3y (σ=30%, q=2%) | Call | 21.6071 | 21.6015 | 0.0056 | 0.3298 | 21.5977 | 0.0094 |
| Put | 16.1227 | 16.1313 | 0.0086 | 0.1006 | 16.1133 | 0.0094 |
American-exercise checks (the reason the tree exists):
- ATM 1y American put: 6.0888 vs European 5.5695 — early exercise adds $0.52, and Longstaff-Schwartz independently prices it at 6.0729 ± 0.0135.
- ITM American put (S=90): premium $1.28 over European, LSM agrees within its statistical gate.
- ATM American call with no dividends: exactly equals the European price (early exercise is provably never optimal there) — the tree reproduces this to 12 decimal places.
- Implied vol round-trips on all 10 option/parameter combinations recover the true volatility to < 1e-6 in 3–6 Newton iterations.
A note on test discipline: an earlier version of the Longstaff-Schwartz regression used a quadratic basis, and this suite caught it underpricing the deep-ITM American put by $0.086. Upgrading to the cubic basis fixed it. The gate exits non-zero on any failure — it is the CI merge gate for this repo.
Measured on the development machine (Windows 11, MSVC 19.51, Release /O2),
single-threaded:
| Method | Workload | Time | Throughput |
|---|---|---|---|
| Black-Scholes | 1,000,000 prices | 74.9 ms | 13.3M prices/sec (74.9 ns each) |
| Monte Carlo (European) | 100,000 paths | 3.1 ms | 32.6M paths/sec |
| Monte Carlo (American, LSM) | 100,000 paths × 50 dates | 344 ms | — |
| Binomial tree, 100 steps | European / American | 0.007 / 0.014 ms | — |
| Binomial tree, 500 steps | European / American | 0.128 / 0.260 ms | — |
| Binomial tree, 1000 steps | European / American | 0.457 / 0.997 ms | — |
| Binomial tree, 5000 steps | American | 33.3 ms | — |
Tree scaling 500 → 1000 steps: measured 3.6× vs the 4.0× that O(N²) predicts. (The first-run 5000-step European sample was polluted by a one-off OS interruption; the benchmark harness now does an untimed warm-up pass and averages multiple repetitions.)
Requirements: CMake ≥ 3.16 and any C++17 compiler. No libraries to install — the project uses only the C++ standard library.
Windows (Visual Studio + Ninja, from a x64 Native Tools developer prompt):
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
build\convergence_test.exe :: the correctness gate — expect 47/47 PASS
build\benchmark_main.exe :: timing table
build\pricer_demo.exe :: the demo below(Or without Ninja: cmake -S . -B build then
cmake --build build --config Release. Linux/macOS: same two CMake
commands, no dev prompt needed. ctest --test-dir build runs the gate.)
Demo (defaults: ATM call, S=K=100, r=5%, σ=20%, T=1y):
> pricer_demo
=== Options Pricing Engine ===
Option: European Call | Spot: 100.00 | Strike: 100.00 |
Rate: 5.00% | Vol: 20.00% | Time: 1.00y | Div yield: 0.00%
Black-Scholes Price: $10.4506 (<0.001ms)
Monte Carlo Price: $10.4520 +/- 0.0328 (100000 paths, 3.1ms)
Binomial Tree Price: $10.4466 (500 steps, 0.1ms)
Greeks (Black-Scholes closed form):
Delta: 0.6368 (price move per $1 spot move)
Gamma: 0.0188 (delta move per $1 spot move)
Vega: 0.3752 (price move per 1% vol move)
Theta: -0.0176 per day
Rho: 0.5323 (price move per 1% rate move)
Try --type put --style american for early exercise,
--market 10.50 to back out an implied volatility from a market price, or
--help for all flags (spot/strike/rate/vol/time/dividend/paths/steps/seed).
Being honest about scope — this is a pricing library, not a trading system:
- Dividends: continuous flat yield only; no discrete dividend dates.
- Payoffs: vanilla calls and puts only — no barriers, Asians, or other exotics (the Monte Carlo engine is the natural extension point).
- Volatility: constant per pricing call; no local vol, no stochastic vol (Heston), no volatility surface.
- Market data: none. Inputs are command-line parameters, not live feeds.
- Parallelism: single-threaded by design; the benchmark numbers are one core's work.