TL;DR β I built an options pricer in C++17 that prices a stock option two ways (Black-Scholes and Monte Carlo) and checks they agree. The fun part is the engine under it: a hand-rolled PRNG, a multithreaded Monte Carlo simulator with provably non-overlapping random streams, and constant-memory streaming statistics β ~480M simulated paths/sec across 10 cores. It's all written from the ground up, no numerical libraries doing the heavy lifting. Code's on GitHub.
Why I built it
Options pricing is one of those classic quant problems, and Monte Carlo pricing is exactly the kind of workload where C++ earns its keep β millions of independent simulations, tight numerical loops, threads fanning out across cores. I wanted a project that proved I could write that whole layer myself: my own randomness, my own parallelism, my own variance math, all the way down to the bits.
So I reached for C++ from the start and built it the hard way on purpose β not because the math is exotic, but because the engineering around it is where the real craft lives, and that's the part I wanted to get my hands dirty with.
What it does
An option is the right to buy (a call) or sell (a put) a stock at a fixed price before a fixed date. The tool prices it two ways:
- Black-Scholes β the closed-form formula. One exact price, instantly.
- Monte Carlo β simulates millions of random "possible futures" for the stock and averages the payoff.
For a normal option both should agree, and that agreement is the correctness check β two completely unrelated methods arriving at the same number is about as convincing as it gets without a reference answer to peek at. The Monte Carlo estimate lands within ~1 standard error of the analytic price and tightens toward it as you throw more paths at it.
./build/pricer --spot 100 --strike 105 --expiry 0.5 --rate 0.05 --vol 0.2 --type call
It also reports the Greeks (delta, gamma, theta, vega) and can solve backwards for implied volatility via Newton-Raphson.
Using it to spot a good buy
The tool gives a fair price; the signal is comparing it to what you'd actually pay.
AAPL $210 call, 3 months out. Tool says fair value = $8.84.
- Quoted at $7.50 β below fair value β cheap β β buy
- Quoted at $11.00 β above fair value β solve implied vol β 35% vs your expected 30% β overpaying for jumpiness β β pass
It won't tell you which way the stock goes β that's your call. It tells you if the price is fair.
The engine β where the C++ work is
This is the part I'm proud of. The whole point was to build the performance-critical core by hand and get it right.
Hand-rolled randomness β no <random>, no std::normal_distribution. A splitmix64 seed expander feeds a xoshiro256++ generator (~1 ns/draw, passes BigCrush), and a cached Box-Muller transform turns uniforms into normals two at a time. Doing it myself means full control over quality, speed, and β crucially β how the stream gets carved up across threads.
Provably non-overlapping parallel streams β xoshiro's jump() advances the generator 2^128 steps in O(1). Each worker thread gets its own slice of one logical stream via a successive jump, so no two threads can ever draw correlated numbers. That's the subtle bug that quietly poisons parallel Monte Carlo β designed out from the start instead of debugged later.
Constant memory β instead of allocating an N-element payoff array, a Welford accumulator streams the mean and variance in a single pass, and the per-thread accumulators merge with Chan's parallel-variance formula. Memory stays flat whether you run a thousand paths or fifty million.
Bit-for-bit reproducible parallelism β work is split into fixed contiguous blocks, each seeded by its own jump and merged in thread-index order. For a fixed seed and thread count, the result is identical no matter how the OS happens to schedule the threads. Reproducibility and parallelism usually pull against each other; here they don't, and getting that to line up was the most satisfying piece of the whole build.
It also uses antithetic variates (pair each draw Z with -Z) for variance reduction, and the whole thing compiles clean under -Wall -Wextra -Wpedantic, AddressSanitizer/UBSan, and ThreadSanitizer β no data races.
The numbers
Benchmark on a 10-core Apple Silicon machine (make bench):
| sims | threads | Mpaths/sec | abs err | ms |
|---|---|---|---|---|
| 1,000,000 | 1 | 58.2 | 0.0082 | 17.2 |
| 1,000,000 | 10 | 293.9 | 0.0083 | 3.4 (5.05Γ) |
| 10,000,000 | 10 | 432.1 | 0.0025 | 23.1 (5.78Γ) |
| 50,000,000 | 10 | 479.1 | 0.0007 | 104.4 (6.28Γ) |
~480M antithetic paths/sec across all cores, and the estimate tightens toward the Black-Scholes truth as the path count climbs.
Takeaway
What started as "price an option" turned into a proper little engine, and the joy of it was owning every layer β from the bits coming out of the PRNG to the variance formula merging across threads β on a workload where that control actually shows up in the numbers. It's the kind of project that's equal parts finance and systems programming, which is exactly why I enjoyed it.
Technical implementation
For the engineering-minded. C++17, zero dependencies beyond a compiler and a thread library. Makefile (-O3 -march=native) as the zero-config default, CMake for portability, plus ASan/UBSan and TSan build targets.
include/rng.hpp β randomness
splitmix64seed expander βxoshiro256++(Blackman & Vigna), ~1 ns/draw, BigCrush-clean.- Cached Box-Muller normal generation (two normals per pair of uniforms).
jump()for O(1) 2^128-step stream advance β non-overlapping per-thread substreams.
src/mc_engine.cpp β parallel simulator
- GBM terminal-price simulation with antithetic variates.
- Welford single-pass mean/variance; Chan's formula to merge per-thread accumulators β constant memory.
- Fixed contiguous work blocks seeded by successive jumps, merged in thread order β bit-for-bit reproducible for a given seed + thread count.
--threads Nsizes the pool (default = hardware concurrency).
src/bs_engine.cpp β analytic core
- Closed-form call/put prices, all four Greeks, Newton-Raphson implied-vol solver.
N(x)viastd::erf.
Native CLI: all the usual pricing flags plus --threads, --bench (paths/sec, single vs all cores), and --paths-csv for exporting sample GBM paths.
Through-line: build the hot path yourself β your own PRNG, your own threading model, your own streaming statistics β and you get speed and reproducibility on a workload where both matter.
Note: written with the help of an llm from my project + own points, dont mind the emdashes or llm-ish words :>