Skip to content

BrandonKKY/cpp-options-pricer

Repository files navigation

cpp-options-pricer

CI

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).

What This Is

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.

Why This Matters for Quant Trading

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.

The Three Pricing Methods

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 Greeks

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).

Implied Volatility

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.

Convergence Proof

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.

Performance Benchmarks

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.)

How to Build and Run

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).

What's Not Included

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.

About

No description, website, or topics provided.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors