Fifteen Million Was the Easy Part
I built a Black-Scholes options pricer in C++, and this post is the why, the what, and the how. Why I wrote it, what it turned into, and how a clean first version that priced fifteen million options a second became one that prices 215 million. The headline is the speed. The real story is how often I was wrong about where that speed would come from, and how measurement kept correcting me.
The first version was the textbook version: clean, allocation-free, correct, checked against published prices and put-call parity. On a single core it priced about fifteen million options a second. That sounds fast until you learn what a modern core can actually do. Fifteen million was the easy part. Everything worth telling happened on the way to 215 million.
Why I built it#
One side project, three goals. I wanted to relearn the numerical methods behind option pricing, the ones I last touched years ago. I wanted to practice low-latency code, the kind I never get to write in a web backend, where the clock is a network round trip and not a nanosecond. And I wanted an artifact, something I could point at instead of describe.
The engine earns its keep now. The volatility math behind the 3D surfaces in my playground runs on it, cross-checked against a reference before I trusted a single number on screen.
What I built#
At its core the engine prices an option and its full set of Greeks with no memory allocation, verified against known values. Around that core sits the rest of the roadmap: a Monte Carlo engine with the standard variance-reduction tricks, a risk view that revalues a hundred-thousand-position book about a thousand times a second, and a Python binding that holds roughly 133 million options a second even when called from NumPy, about fifty times what a pure-array version manages.
So the shape of it is a small, fast, honest library. Fast enough to be interesting, honest enough that every number came with a test. It is a learning artifact, not a trading system, and the market data it serves is the educational playground on this site, not an execution engine.
How it got fast: three times I was wrong#
The climb from fifteen million to 215 million was not a straight line of clever ideas. It was a sequence of confident assumptions, each killed by a measurement.
The bottleneck was not where the guides said. Every performance guide opens the same way: restructure your data so memory access goes linear. I did it carefully and the loop got slightly slower. The advice was not wrong in general, it was wrong here, because this loop never touched much memory. An option pricer lives inside exponentials, logarithms, and the normal distribution, which is to say inside the math. Reorganizing memory for a loop that is starved on arithmetic solves a problem it does not have. Once I attacked the math instead, swapping one modern library call for a sixty-year-old polynomial and threading a loop that was embarrassingly parallel, the number jumped from fifteen million to about 114 million.
The compiler claimed a win it never delivered. The plan always included SIMD, the vector instructions that handle four numbers at once, and modern advice says the compiler does that for you on clean loops. My build logs agreed and reported the loop vectorized. They were reporting on a different loop, a trivial one at the end of the benchmark. My compiler would not fold a loop full of exp and log calls into vector form. Some toolchains will; mine did not. So the loop that mattered stayed scalar every single time, its failure notice sitting quietly in a report I had not opened. The honest accounting: none of my first big speedup came from SIMD at all.
So I wrote the math by hand. Four-wide exponential and logarithm, built from classic polynomial cores, intrinsics all the way down. The discipline that saved me here was borrowed and simple: test each function in isolation against the standard library before wiring it into anything. Not the pricer against the pricer, the exponential against the exponential. When a bug showed up, the isolated test named the exact function instead of leaving me to hunt a wrong price three layers downstream. With real SIMD underneath, one core gained a clean four times, right at the theoretical width, and across all cores the engine reached about 215 million a second.
What I keep from it#
I set out to relearn some numerical methods and end up with a small, fast library I could point at, and I got both. The number I quote is 215 million a second. The lesson I keep is the ledger of misses. The data layout that every guide recommends did nothing, because the bottleneck was arithmetic, not memory. The compiler reported a victory that belonged to a loop nobody cared about. The single biggest win was trading a modern function for an old approximation I could reason about. Fifteen million a second was free; the next two hundred million were an education in checking what is actually true, one report line at a time.
Two honest caveats belong on the record. Desktop timings wander ten or twenty percent run to run, so read every number here as a shape, not a decimal. And none of this is a trading system. It is a learning artifact that happens to be fast, which was the point.
Related:
- Rotating an Option, the 3D surfaces this engine's volatility math feeds
- How to backtest without fooling yourself, the same measure-before-believing discipline pointed at signals instead of speed
Keep reading
Investment Committee
Score any stock across five weighted dimensions and get a letter grade with a written committee verdict.
Market's Best
The top-graded stocks from the latest market scan. No sign-in needed.
Grade my portfolio
Run a sample portfolio through the investor committee.
Factor-First AI Investment Platform Narrated by a Six-Persona Committee
Grew a single-ticker grader into a full investment platform: a four-factor composite (Quality, Valuation, Momentum, Health) narrated by a six-persona committee, a nightly scan of ~400 large caps, portfolio and net-worth tracking, and a grade scale validated by a daily backtester.
How to backtest without fooling yourself
A backtest's job is not to find an edge. It is to stop you from believing in one that is not there. The toolkit I used to test my own trading engine, and the part where it killed my single best signal.
The Pocket Quant
I built a quant research platform, then built an agent to operate it: a scheduled Claude session that reads the boards, keeps a pre-registered track record, and texts me three times a day without ever saying buy.
Follow the work
New tools and writing as they ship — pick a channel.