C++20 · options pricing engine

deriv‑engine

Fifteen independent option-pricing and risk methods, hand-built from the Black-Scholes PDE up through reverse-mode AAD, Heston stochastic vol, and Bates jump-diffusion, calibrated against a real Deribit BTC options chain and backtested against 16 years of real BTC price history — not synthetic data.

CI passing · 34 tests / 76 assertions C++20 · CMake · Catch2 github.com/roberto0607/deriv-engine
01

Headline result

96.8%
of contracts converged during IV calibration
0.3pp
typical (median) gap vs. exchange-quoted IV
904
real BTC contracts pulled from Deribit's public chain
15
independent pricing & risk methods, cross-validated against each other
The calibration result is the load-bearing claim of this project: Newton-Raphson implied-vol solving (with a bisection fallback for the near-zero-vega cases Newton-Raphson can't handle), run against a one-time real snapshot of Deribit's public BTC options chain (data/btc_chain_snapshot.json, 904 contracts), recovers the market's own quoted IV to within 0.3 percentage points for a typical contract (median), converging on 96.8% of contracts. The snapshot is committed so the test suite stays reproducible — no live API calls in CI.
Median, not mean, because the mean is skewed by a small tail of genuinely hard contracts — options expiring within about a day, or far from the money — where Deribit's own quoted IV is likely thin or stale to begin with. Mean gap: 2.2pp. 90% of contracts land within 5.6pp.
02

Fifteen methods, cross-validated

Every method is checked against at least one independent method or real data, not just against itself.

MethodWhat it's forValidated against
Black-ScholesClosed-form European price & Greeks; ground truthDerived directly from the PDE — provably exact under GBM
Binomial treeAmerican exercise, discrete-time convergenceConverges to Black-Scholes: ~2% gap at 10 steps, ~0.1% at 500
Trinomial treeA second, independently-derived lattice (3 branches/node vs. binomial's 2)Agrees with the binomial tree at high step counts; ~5-10% lower error at equal steps, measured directly
Monte CarloGBM path simulation, variance reductionMatches closed-form within sampling error; antithetic variates reduce variance
Reverse-mode AADAll Greeks in ~one backward pass over a hand-built tapeDelta cross-checked against seeded bump-and-revalue (0.6376 vs 0.6363)
Delta-hedging simRealized P&L of a rebalanced hedge vs. theoretical priceMean P&L ≈ 0 across 2,000 paths post-fix (see bug log)
Longstaff-SchwartzAmerican Monte Carlo via regression-based continuation valueLeast-squares fit solved by hand (Gaussian elimination, partial pivoting)
Crank-Nicolson PDEFinite-difference solver, tridiagonal systemStays stable where the explicit scheme provably diverges
IV calibrationNewton-Raphson solve for implied vol using vega, bisection fallback near zero vega96.8% convergence / 0.3pp typical gap vs. real Deribit quotes
Heston (stochastic vol)COS-method pricing under vol-of-vol, mean reversion, and spot-vol correlationCollapses exactly to Black-Scholes in the deterministic-variance limit; put-call parity holds directly on Heston prices (independent of BS)
Bates (Heston + jumps)Adds discontinuous jumps on top of Heston's diffusion, reusing its characteristic functionCollapses exactly to Heston at zero jump intensity; collapses to closed-form Merton (1976) in the deterministic-variance limit; cross-checked against a full SDE Monte Carlo
Historical backtestDo BS/Heston/Bates hedge ratios track real BTC price history, day by dayRolling 30-day delta-hedged calls over 16 years of real BTC prices (195 windows) — try it live below
Portfolio VaR & stress testReal historical simulation and Monte Carlo (Bates SDE) VaR, cross-validated against each otherAgree within ~2.3x at 99%; both catch the delta-normal baseline's blind spot to gamma — try it live below
P&L attributionTaylor-decomposes realized hedge P&L into delta/gamma/vega/theta, the "P&L explain" a sell-side desk runs dailyComponents sum exactly to total P&L by construction; unexplained residual <2% for a small isolated move — try it live below
Basic CVAPrices counterparty default risk via expected exposure simulated under the calibrated Bates SDEZero when recovery=100% or hazard=0 or exposure is one-sided; monotonic in hazard rate — try it live below
03

Volatility smile, recovered from the real chain

Solved IV per strike for the ~99-day expiry, from the calibration pass over the committed Deribit snapshot. Shape reflects the smile/skew Black-Scholes assumes away.

1.00 0.80 0.60 0.40 0.20 20k 60k 100k 150k 230k min IV near ATM
solved_iv vs. strike — deep OTM puts and calls both price in extra premium; the minimum sits near the money, as expected under a fat-tailed underlying like BTC.
04

Three real bugs, found and fixed

Each caught by a test that expected a specific numeric property, not just "does it compile."

numericalT→0 division blew up to NaN
Black-Scholes divides by σ√T inside d1/d2. At expiry, T=0 sends that denominator to zero — caught by a REQUIRE(std::isfinite(price)) test.
Fix: early-return guard at T ≤ 1e-8, returning intrinsic value — which is also the exact mathematical limit of the formula as T→0, not a patch.
logicDelta hedge double-counted the premium
The hedging simulation initialized cash to the theoretical price, then subtracted the grown premium again at settlement — a single path showed −11.12 P&L against a 10.45 theoretical price. Confirmed systematic (not noise) across a 2,000-path distribution.
Fix: track hedge cash from zero and combine premium + cash − obligation exactly once. Post-fix mean P&L across runs: ≈ 0.002–0.019.
reproducibilityUnseeded RNG made the AAD benchmark meaningless
Bump-and-revalue compared a "base" and a "bumped" Monte Carlo run, each spinning up its own unseeded std::mt19937 — the two runs used unrelated random paths, producing a 3.2-unit discrepancy against AAD's delta.
Fix: added an optional seed parameter so both runs share draws. Post-fix: bump-delta 0.6363 vs. AAD-delta 0.6376 — close agreement.
05

The honest AAD finding

⚠

Reverse-mode AAD is the resume-defining piece of this engine — a hand-rolled computation tape with operator overloading, not a library like XAD or CoDiPack, specifically so the mechanism can be defended under questioning rather than cited. It's correct — delta from the tape's backward pass agrees with seeded bump-and-revalue to four significant figures — and its speed advantage only shows up once you need multiple Greeks: bump-and-revalue re-runs the full simulation once per Greek (N+1 simulations for N Greeks), while AAD computes all of them in roughly one extra backward pass, regardless of N. For a single Greek, the tape's overhead makes it a wash.

06

Try it live: pricing methods

This isn't a JS reimplementation — wasm_bridge.cpp compiles the real C++ (Black-Scholes, trees, Monte Carlo, Heston, the Asian/Barrier exotics simulator, and Bates) to WebAssembly via Emscripten. Every number below comes out of your actual compiled engine. One live-data fetch (below) feeds every calculator on this page — pricing, VaR/P&L, and CVA alike — so you only ever look up real numbers once.

pulls real spot price + a real near-the-money option's market-implied vol from Deribit's public API and fills Spot/Volatility on every calculator on this page (pricing, VaR/P&L, CVA) — not just this one
loading engine…
Black-Scholes
—
closed-form, European
Binomial tree
—
500 steps
Trinomial tree
—
500 steps
Monte Carlo
—
200,000 paths, seeded
Heston
—
stochastic vol, COS method
Δ delta
—
Γ gamma
—
V vega
—
Θ theta
—
ρ rho
—
Analytical Greeks are computed from the closed-form solution; the tree and Monte Carlo columns exist to show three independently-derived methods agreeing on the same price.
Heston's kappa/xi/rho use illustrative defaults (calibrated values load below once available).
07

Historical backtest: does jump-awareness actually help?

Rolls a fresh at-the-money call every window_days and delta-hedges it daily against the real BTC price path — not a simulation (src/backtest.cpp). Runs live, right here, over the full committed 16-year history (data/btc_price_history.csv), for all three models.

loading real price history…
Real calibrated Heston structural parameters (kappa/theta/xi/rho) come from tools/calibrate_heston.cpp's fit to the real Deribit snapshot — the same numbers the README's Phase 15 table used, held fixed across the whole backtest while each window re-estimates its own volatility from trailing realized vol.
08

Portfolio risk: VaR, stress testing & P&L attribution

A short strangle/straddle book, partially delta-hedged (make_example_market_maker_book in src/portfolio.cpp) — real short-gamma risk, not a toy. VaR/stress testing measures the risk on this book directly; P&L attribution explains a realized move on the same book after the fact.

waiting on real price history…
Click "Compute VaR" — historical simulation replays every real overlapping move in the price history; Monte Carlo simulates forward under the calibrated Bates SDE; delta-normal is the fast textbook approximation, shown specifically because it can't see the book's gamma.
09

Basic CVA: pricing in counterparty default risk

A single call bought OTC from a risky counterparty (src/cva.cpp) — simulates forward exposure under the calibrated Bates SDE and prices in what their possible default costs you. Deliberately simplified: unilateral (no DVA), a single flat hazard rate, no wrong-way risk, no netting or collateral — see the note below.

uses the same compiled engine as the calculators above
CVA
—
the cost of counterparty default risk
Position value, no credit risk
—
long 1x 90d ATM call, mid-market
Peak expected exposure
—
max over time of E[max(value, 0)]
Avg expected exposure
—
time-average of the same
Hazard rate is derived from the CDS spread via the standard "credit triangle" approximation (hazard ≈ spread / (1 − recovery)) — itself computed by the real compiled engine (hazard_rate_from_cds_spread()), not JavaScript. Set the CDS spread to 0, or recovery to 99%, to confirm CVA collapses to exactly 0 — the same identities the test suite checks. Out of scope, and documented as such: wrong-way risk (a counterparty's default is modeled as independent of your exposure to them, which understates risk for correlated names), netting/collateral, and the other XVA family members (FVA/MVA/KVA) a full desk computes alongside this.
10

Live market check: model vs. the real order book

Every other card above prices whatever numbers you type in — this one pulls Bitcoin's real live price and the real live BTC options order book directly from Deribit's public API (confirmed CORS-open from a browser, no backend or API key needed), finds the closest real, currently-tradeable contract to your target, and reprices it two ways: this engine's calibrated Heston model, with its variance level set from a real SVI curve fit to that whole expiry's live chain (not just this one contract's own quote), and a plain Black-Scholes price at that same fitted curve's vol for this exact strike. This is the actual "does my model agree with the real market" check, not a hypothetical one.

uses the same compiled engine as the calculators above, plus a live client-side call to Deribit's public API
Matched contract
—
closest real, live BTC option to your target
Real market mark price
—
bid / ask shown here too
This engine's Heston price
—
real spot + SVI-fitted variance for this expiry
Heston vs. market gap
—
(model − market) / market
SVI-implied price (curve fit)
—
Black-Scholes at the fitted SVI vol for this exact strike
SVI vs. market gap
—
(SVI-implied − market) / market
Deribit quotes BTC options in BTC, not USD — this card converts using the real live index price it just fetched. The Heston price's v0/theta come from fitting a real SVI curve to every live, liquid contract at this exact expiry (same 5-parameter fit and moneyness/outlier filters as section 11) rather than this one contract's own quote — a real, whole-expiry read on the vol level, not a single noisy point. If that expiry doesn't have enough real, liquid quotes to fit a curve (calibrate_svi's own minimum is 5), this falls back honestly to that one contract's own market IV for Heston, and the SVI-implied row shows "—" rather than a number nobody should trust. Live data requires Deribit's API to be reachable from your network and browser; no login/key needed, and cross-origin access was confirmed open (not assumed). If a request is blocked (corporate network, ad-blocker, VPN), this card reports that plainly rather than failing silently or falling back to fake numbers.
11

Volatility surface: fitting SVI to the whole live chain

Every other card on this page prices off ONE vol number. This one fits a real, separate 5-parameter SVI (Gatheral, 2004) curve to EVERY real expiry currently live on Deribit at once — the same curve-fitting approach real crypto and equity vol desks quote off of day to day, distinct from the stochastic-process story Heston/Bates (sections 06/09) tell. Each fitted curve is checked live against two real no-arbitrage conditions, not assumed valid.

click "Fit live SVI surface" to fetch the real chain and fit every liquid expiry
fetches the real live Deribit chain, groups it by real expiry, and fits one SVI curve per expiry with enough live contracts (moneyness kept within roughly 0.7-1.4x spot, same liquidity filter this project's own test suite uses; a median/MAD filter also trims the handful of extrapolated, essentially untradeable deep-wing quotes Deribit still reports a mark IV for)
Fits directly to Deribit's own reported mark IV per contract (the same number every other live card on this page reads), in total-variance space, per the standard SVI convention — see svi_calibration.hpp. Before fitting, a median/MAD outlier filter (k=4) drops any quote whose mark IV is wildly off the rest of that expiry's chain — a small number of illiquid, deep-wing contracts near expiry get an extrapolated mark IV from Deribit even with no real market behind them, and left in, they can drag a least-squares fit away from the tradeable part of the smile entirely. "Butterfly OK" means the fitted curve never implies a negative risk-neutral density anywhere it was checked; a real, sampled check, not an assumption. Very short-dated expiries (under ~2 weeks) are still the hardest part of any real chain for a 5-parameter curve to fit tightly — if one shows a high error or a failed butterfly check after outlier trimming, that's an honest, documented property of raw SVI on steep, narrow short-dated smiles, not a bug being hidden.