Tested Python package for options pricing — Black-Scholes & Greeks, variance-reduced Monte Carlo (benchmarked ~12x), and implied-volatility surface construction. Installable, CI-tested (13 tests), reproducible figures.
Author: Hatef Tabbakhian (Leo) · GitHub · LinkedIn
A derivatives pricing and volatility-analytics engine in Python. It implements closed-form Black-Scholes valuation, a variance-reduced Monte Carlo engine for path-dependent exotics, and implied-volatility surface construction from an option chain.
The goal of the repo is to demonstrate quant-trading fundamentals — option pricing, Greeks, simulation, variance reduction, and implied-vol inversion — with code that is tested, packaged, and reproducible. It is an analytics project: it does not model order books, latency, or exchange connectivity (see Scope & limitations).
- Prices European, Asian, Barrier (knock-in/out) and Digital options.
- Computes analytical Greeks (delta, gamma, vega, theta, rho) in desk conventions, and validates Monte Carlo against the closed form.
- Reduces Monte Carlo variance with antithetic and control variates, with measured speed-ups (see the benchmark table below).
- Inverts a market option chain into an implied-vol smile/skew and 3-D surface.
For the ATM 1-year call (S=100, K=100, r=3%, σ=20%), the analytical price is 9.4134. The variance-reduced Monte Carlo estimate lands at 9.412 with a 95% confidence interval that contains the analytical value — the two methods agree, which is the main correctness check for the simulation engine.
Pricing the same ATM call with and without variance reduction. The
variance-reduction ratio is Var(plain) / Var(reduced) at equal path
counts — i.e. how many times more plain-MC paths you would need to match the
reduced estimator's precision. Runtimes are from a single laptop-class core and
are indicative, not a latency claim.
| Paths | Plain SE | Reduced SE | Variance-reduction ratio | Reduced price | Abs. error vs BS | Plain runtime | Reduced runtime |
|---|---|---|---|---|---|---|---|
| 1,000 | 0.4272 | 0.1296 | 10.9x | 9.2725 | 0.1409 | ~0 ms | 1 ms |
| 10,000 | 0.1399 | 0.0409 | 11.7x | 9.4115 | 0.0019 | 1 ms | 2 ms |
| 100,000 | 0.0445 | 0.0129 | 11.9x | 9.3835 | 0.0299 | 16 ms | 24 ms |
| 500,000 | 0.0199 | 0.0058 | 11.8x | 9.3948 | 0.0186 | 83 ms | 121 ms |
Tradeoff, stated honestly: the reduced estimator costs ~1.5x the wall-clock
time per nominal path (antithetic doubles the path count and the control
variate adds an O(n) regression), but it buys ~12x lower variance. Net, to hit
a target standard error it is far cheaper — you reach it with roughly an order
of magnitude fewer effective paths. Reproduce with python scripts/benchmark.py.
The synthetic chain (see Data) is built to show a realistic equity skew — downside puts are bid up — that flattens with maturity and curves up at the wings for longer tenors.
The antithetic + control-variate estimator (blue) tracks the Black-Scholes value with far fewer paths than plain Monte Carlo (red).
A few choices worth calling out, since the why matters more than the what:
- Exact log-Euler GBM, not naive Euler. GBM has a closed-form solution, so
I simulate
S_T = S_0 exp((r − q − ½σ²)t + σ√t Z)rather than stepping the SDE. This is unbiased at any step size; the multi-step grid is kept only because Asian and Barrier payoffs need the whole path. - Control variate = discounted terminal price. It is a martingale with a
known mean (
S₀e^{−qT}), perfectly correlated with the European payoff's main risk factor, and free to compute. The optimal coefficientβ*is estimated by regression (Cov(Y,X)/Var(X)) rather than hard-coded. - Brent for implied vol, with arbitrage guards. Price is monotone in vol, so
a bracketed root-finder is robust and never diverges. If a quote sits outside
no-arbitrage bounds the solver returns
NaNinstead of a misleading number — this is exercised by tests (test_implied_vol_returns_nan_*). - Greeks in desk conventions. Vega per 1 vol point, theta per calendar day, rho per 1% rate move — so the magnitudes match how a trader reasons, not the raw partial derivatives.
- Seed-per-engine reproducibility. Each
MonteCarloEnginere-seeds its RNG, so pricing knock-in, knock-out and vanilla on three fresh engines reuses identical paths. That is what makes the knock-in + knock-out = vanilla parity hold to machine precision (test_barrier_in_out_parity), not just within MC noise. (Caught a real bug while writing that test — calling.price()twice on one engine advances the RNG and breaks the path-by-path identity.)
Options-Monte-Carlo-Pricer/
├── README.md
├── pyproject.toml # installable package (pip install -e .)
├── requirements.txt
├── LICENSE
├── .gitignore
├── .github/workflows/ci.yml # GitHub Actions: pytest on 3.9 / 3.11 / 3.12
├── src/
│ └── options_pricer/
│ ├── __init__.py # public API
│ ├── black_scholes.py # closed-form pricing + analytical Greeks
│ ├── monte_carlo.py # GBM engine, antithetic + control variates
│ ├── payoffs.py # European / Asian / Barrier / Digital payoffs
│ ├── implied_vol.py # IV root-finder + VolSurface
│ ├── visualization.py # matplotlib helpers (house style)
│ └── utils.py # logging + numerical helpers
├── notebooks/
│ ├── 01_black_scholes_and_greeks.ipynb
│ ├── 02_monte_carlo_pricing.ipynb
│ └── 03_implied_vol_surface.ipynb
├── scripts/
│ ├── generate_outputs.py # reproduces every figure + dataset
│ └── benchmark.py # variance-reduction benchmark table
├── tests/
│ └── test_pricing.py # 13 pytest checks
├── data/
│ ├── sample_option_chain.csv # synthetic market snapshot (with skew)
│ ├── exotic_book_prices.csv # MC-priced exotic book
│ └── benchmark.csv # benchmark results
└── images/ # generated plots used in this README
git clone https://github.com/Leotaby/Options-Monte-Carlo-Pricer.git
cd Options-Monte-Carlo-Pricer
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]" # editable install + dev tools (pytest, jupyter)
pytest -q # 13 tests
python scripts/generate_outputs.py # regenerate figures + datasets
python scripts/benchmark.py # regenerate the benchmark table
jupyter lab notebooks/from options_pricer import BlackScholesInputs, bs_price, bs_greeks, OptionType
from options_pricer import MonteCarloEngine, MonteCarloConfig
from options_pricer.payoffs import AsianPayoff
market = BlackScholesInputs(spot=100, strike=100, maturity=1.0, rate=0.03, volatility=0.20)
print(bs_price(market, OptionType.CALL)) # 9.4134
print(bs_greeks(market, OptionType.CALL)["delta"]) # 0.5987
engine = MonteCarloEngine(market, MonteCarloConfig(n_paths=100_000, n_steps=126, antithetic=True))
result = engine.price(AsianPayoff(strike=100, option_type=OptionType.CALL))
print(result.price, result.confidence_interval_95)The option chain in data/sample_option_chain.csv is synthetic, and
deliberately so. It is generated from a known implied-vol function (a
parametric equity skew that flattens with maturity), then converted to prices
via Black-Scholes. This has two advantages over a one-off vendor snapshot:
- Reproducibility — anyone can regenerate it offline with a fixed seed; no API keys or paid market-data feed required for CI or for a reviewer.
- A ground-truth check — because the input vol is known, notebook 03 verifies that the implied-vol inversion recovers it (max abs error is reported in the notebook). That would be impossible with real quotes.
The code is structured so a real chain (same columns: strike, maturity, price, option_type) can be dropped in unchanged.
Being explicit about what this is and is not:
- It is options analytics, not an HFT system. There is no order-book simulation, market-data replay, latency budget, or matching engine. Runtimes in the benchmark are indicative of algorithmic cost, not a low-latency claim.
- Black-Scholes is the pricing model. Constant vol, no jumps, no stochastic vol. The implied-vol surface captures the smile empirically, but the simulator itself does not yet price under that surface (no local/stochastic vol).
- Barriers are discretely monitored. With finite steps the knock probability is biased low versus continuous monitoring; a Brownian-bridge correction would reduce this.
- Greeks are analytical (Black-Scholes) only. Monte Carlo Greeks (pathwise / likelihood-ratio) are not yet implemented.
- Single-threaded NumPy. Fine for the path counts here; large books would want batching or a GPU/Numba path.
- Heston / local-vol simulation so the engine prices consistently with the smile.
- SVI or SABR parametric, arbitrage-free surface fits.
- Quasi-Monte Carlo (Sobol) and pathwise Greeks.
- American exercise via Longstaff-Schwartz least-squares Monte Carlo.
Relevant to quant trading, derivatives, options market-making, and quant dev interviews:
- Comfort with both analytical and simulation pricing, cross-validated.
- Variance reduction framed as what it is — a regression/statistics problem — with measured results rather than assertions.
- Implied-vol inversion under no-arbitrage constraints (root-finding).
- Engineering basics: a
pip-installable package, a green CI matrix, type hints, logging, tests including edge cases, and reproducible figures.
It is not positioned as an HFT/low-latency project; that would need order-book and microstructure work, which I'm building separately.
MIT — see LICENSE.



