European option pricing engine based on the Black-Scholes model. Written in modern C++17, with unit tests (Catch2) and Python bindings (pybind11). Includes a second pricing engine based on Monte Carlo simulation, cross-validated against the closed-form Black-Scholes prices. Computes the price of European calls and puts, together with the full set of first-order Greeks (delta, gamma, vega, theta, rho), all from closed-form formulas.
- CMake ≥ 3.15
- A C++17 compiler
- Catch2 and pybind11 (fetched automatically by CMake)
- Python 3 (for the bindings and the demo notebook)
cmake -S . -B build
cmake --build build
ctest --test-dir buildEuropeanOption call{100, 1.0, OptionType::Call}; // strike, maturity (in years), type
MarketData market{100, 0.2, 0.05}; // spot, volatility, rate
PricingResult res = pricing::price(call, market);
double price = res.price;
double delta = res.delta; // also: gamma, vega, theta, rhoimport pricing_engine as pe
call = pe.EuropeanOption(100, 1.0, pe.OptionType.Call)
market = pe.MarketData(100, 0.2, 0.05)
res = pe.price(call, market)
print(res.price, res.delta) # price and GreeksSee notebooks/demo.ipynb for a worked example plotting price and delta vs spot.
include/pricing/ public headers (API): instrument, market, black_scholes
src/ implementation
tests/ unit tests (Catch2)
apps/ demo executable
python/ pybind11 bindings
notebooks/ Python demo (price & delta vs spot)
Separation of concerns. Inputs are split into three structs mirroring the
three parts of a pricing problem: the product (EuropeanOption: strike,
maturity, type), the market state (MarketData: spot, volatility, rate),
and the model (the Black-Scholes formula itself). This keeps the product
description independent from the market data, so a second engine (e.g. Monte
Carlo) can consume the exact same inputs without changing the interface.
Single entry point returning price and Greeks together. price() returns
a PricingResult struct bundling the price and all five Greeks, rather than
five separate calls. The Greeks share intermediate quantities (d1, d2) with
the price, so computing them together avoids recomputation and keeps the API
minimal.
Closed-form Greeks. All Greeks are computed analytically (not by finite differences), which is exact and fast. Finite differences are used only in the tests, as an independent cross-check of the analytical formulas.
Edge-case guards. The degenerate inputs T = 0 and sigma = 0 make the
Black-Scholes formula divide by zero (d1 involves division by sigma*sqrt(T)).
These are short-circuited at the top of price() to return the intrinsic
(or discounted intrinsic) payoff directly, so the engine never returns NaN.
Theta sign convention. Theta is expressed as the derivative with respect to calendar time (time decay), so it is negative for standard options. The finite-difference test negates the derivative w.r.t. maturity T accordingly.
Both engines, share the same interface. The Monte Carlo engine (price_mc) consumes the
exact same EuropeanOption and MarketData structs as the closed-form engine.
Simulation is done under the risk-neutral measure: S_T = S·exp((r − σ²/2)·T + σ·√T·Z),
averaging the discounted payoff over N paths. This is the foundation for pricing
path-dependent payoffs where no closed form exists. Constant terms (drift,
diffusion, discount) are computed outside the simulation loop; the RNG is a
seeded std::mt19937 so results are reproducible and tests are deterministic.
The engine is validated against several independent checks:
- Reference values : two independent parameter sets, cross-checked by hand
- Put-call parity : C − P = S − K·e^(−rT), across multiple inputs
- No-arbitrage bounds : price ≥ 0, call ≤ S, put ≤ K·e^(−rT), lower bound
- Monotonicity : price increases/decreases with spot and volatility as expected
- Greeks vs finite differences — all five Greeks, for both call and put
- Greeks theoretical bounds : delta ranges, gamma ≥ 0, vega ≥ 0
- Limit cases : T = 0 and sigma = 0 return intrinsic payoff with no NaN
- Monte Carlo cross-validation — MC price converges to Black-Scholes (call & put)
- Convergence property — MC error decreases as the number of paths grows
- Constant volatility and interest rate (no volatility smile, no rate curve)
- No dividends
- European exercise only (no early exercise)
- Log-normal price dynamics (no jumps, continuous trading assumed)
- The Monte Carlo engine currently prices the same European payoffs as the closed-form engine (used as a cross-check). It does not yet compute Greeks or handle path-dependent/exotic payoffs — natural next steps.