builder, creator, pokemon master & raptors

what am i up to rn?
stuff ive built before my recent reads

projects

← back to projects

yapper lottery

random impromptu topic yapping generator

?
← back

reads

← all projects

c++ monte carlo options pricer

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

  • splitmix64 seed 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 N sizes 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) via std::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 :>

Mikey