🇬🇧 English · 🇫🇷 Version française
Option pricing, Greeks and hedging in Python, CPU-optimised and validated against QuantLib and market data.
European, Asian and American options (with discrete dividends): closed-form formulas, Monte Carlo with variance reduction, quasi-Monte Carlo, Longstaff–Schwartz and binomial tree. Arbitrage-free SVI smile calibration on real option chains, and backtesting of hedging strategies. Code vectorised with NumPy, compiled and parallelised with Numba.
The numerical methods follow G. Pagès, Numerical Probability (Springer, 2018); the hedging part follows Bouchard & Chassagneux, Fundamentals and Advanced Techniques in Derivatives Hedging (Springer, 2016).
- At a glance
- Features
- Results
- Project structure
- Installation
- Quick start
- Numerical methods
- Tests
- External validation
- Known limitations
- References
| European call, quasi-Monte Carlo | 10.45055 ± 0.00006 vs. an exact value of 10.45058 |
| Price + 5 Greeks from a single simulation | 2M paths, error vs. closed form < 0.1% |
| Real AAPL option chain, American pricer + SVI smile | 94.3% of options repriced within the bid-ask spread |
| Hedging, unknown realised vol between 15% and 25% | P&L standard deviation divided by 8 (17% → 2% of the premium) with a gamma-neutral hedge |
| Test suite | 233 pytest tests, including property-based tests with Hypothesis |
| Option | Price | Greeks (Δ, Γ, Vega, ρ, Θ) |
|---|---|---|
| European | Closed form, Monte Carlo (antithetic + control variate), quasi-Monte Carlo (scrambled Sobol) | Closed form; Monte Carlo pathwise and likelihood ratio |
| Arithmetic Asian | Numba Monte Carlo + geometric control variate | Pathwise / likelihood ratio, with control variate |
| Geometric Asian | Closed form | Derivatives of the closed form |
| American | Longstaff–Schwartz (Numba), CRR binomial tree | CRR tree; finite differences with common random numbers |
| American with discrete dividends | CRR tree, "spot" and "escrowed" models | CRR tree |
Also included:
- 95% confidence interval on every Monte Carlo estimate (
MCResult). - SVI volatility smile (Gatheral) with no-arbitrage checks: butterfly (Durrleman condition), calendar, Roger Lee bound on the wings.
- Market data validation: quote cleaning, implied borrow cost calibrated per expiry, implied vol inversion, repricing within the bid-ask spread.
- Hedging backtest: discrete delta hedging, misspecified vol, uncertain vol (Black–Scholes–Barenblatt), gamma-neutral hedging, P&L CVaR.
Measured on a call with S = K = 100, r = 5%, σ = 20%, T = 1 year (Linux, 2 cores).
Timings are indicative: rerun bench.py and bench_greeks.py on your own machine.
Monte Carlo error decreases as σ/√N: dividing the standard deviation by 10 is equivalent to dividing the number of simulations needed by 100.
| Method (≈ 2M draws) | Time | Price ± 95% CI half-width |
|---|---|---|
| Plain Monte Carlo | 0.064 s | 10.4556 ± 0.0204 |
| + antithetic + control variate | 0.047 s | 10.4480 ± 0.0038 |
| Quasi-Monte Carlo (scrambled Sobol) | 0.091 s | 10.45055 ± 0.00006 |
| Closed form | 10.45058 |
For the arithmetic Asian option (52 dates), the geometric control variate shrinks the confidence interval by a factor of 36 (± 0.022 → ± 0.0006) at equal computing time.
A single simulation yields the price and all 5 Greeks (2M paths):
| Greek | Monte Carlo ± 95% CI | Exact |
|---|---|---|
| Delta | 0.63697 ± 0.00028 | 0.63683 |
| Gamma | 0.01874 ± 0.00005 | 0.01876 |
| Vega | 37.486 ± 0.091 | 37.524 |
| Rho | 53.251 ± 0.030 | 53.232 |
| Theta (per year) | −6.411 ± 0.009 | −6.414 |
| Method | Price |
|---|---|
| Longstaff–Schwartz, 200,000 paths × 50 dates | 6.0803 ± 0.0316 |
| CRR tree, 2,000 steps | 6.0900 |
| European put (lower bound) | 5.5735 |
valider_marche.py inverts the American pricer (discrete dividends) on every quote,
fits an SVI smile per expiry and measures the share of options repriced within the
bid-ask spread.
- Synthetic chain priced by QuantLib (independent engine), 6 expiries from 17 to 290 days: 100% of the 322 retained options repriced within the spread, median smile − market gap of 0.04 vol points, borrow cost recovered (0.30% target, 0.21% to 0.39% fitted depending on expiry), no butterfly or calendar arbitrage.
- Real AAPL chain (Yahoo Finance, delayed data, 7 October 2026 during market hours): 94.3% of the 350 retained options repriced within the spread, across 6 expiries from 7 days to 14 months; median smile − market gap of 0.15 vol points, median call/put vol gap at the same strike of 0.94 points, no butterfly or calendar arbitrage.
6-month ATM call sold at the Black–Scholes price with 20% vol, delta-hedged 52 times. The realised vol is unknown, drawn uniformly between 15% and 25% on each path (100,000 paths):
| Strategy | Mean P&L | P&L std. dev. | 5% CVaR |
|---|---|---|---|
| Delta only | +0.004 ± 0.010 | 1.091 (17.1% of the premium) | −2.57 |
| Delta + gamma-neutral (second option) | +0.000 ± 0.001 | 0.136 (2.1% of the premium) | −0.29 |
The script also checks the theory point by point: zero mean P&L under the correct model, standard deviation scaling as 1/√N (measured log-log slope −0.49, 1 to 4% off the first-order theory), the formula ½ ∫ e^{−rt}(σ̃² − σ²) S² Γ dt for a misspecified vol, and that hedging at σ_max never loses on average when the vol is uncertain.
File names are in French; here is what each one does.
options-pricing/
├── pricer.py Prices: Black–Scholes, MC, QMC, Asian, American (LSM)
├── greeks.py Greeks: closed form, pathwise, LR, finite differences, CRR
├── dividendes.py American CRR tree with discrete dividends (spot / escrowed)
├── svi.py SVI smile: fitting, butterfly and calendar arbitrage checks
├── couverture.py Hedging backtest: delta hedging, gamma-neutral, P&L
├── bench.py Price benchmark
├── bench_greeks.py Greeks benchmark
├── valider_pricer.py crr_american vs QuantLib, theoretical properties
├── adaptateur_crr.py Adapter for crr_american used by valider_pricer.py
├── valider_lsm.py Longstaff–Schwartz vs QuantLib
├── valider_dividendes.py crr_american_div vs QuantLib
├── valider_marche.py Test on a real option chain (Yahoo Finance or CSV)
├── valider_couverture.py Validation of the hedging backtest against theory
├── generer_chaine_test.py Synthetic chain (QuantLib) for offline testing
├── docs/ README charts
├── pyproject.toml Project and pytest configuration
└── tests/
├── conftest.py
├── test_closed_form.py Closed forms, parities, Black–Scholes PDE
├── test_monte_carlo.py MC estimators, variance reduction, LSM
├── test_greeks_mc.py MC Greeks, finite differences, CRR tree
├── test_proprietes.py Arbitrage properties and cross-method consistency
├── test_dividendes.py Discrete-dividend tree vs QuantLib
├── test_svi.py SVI fitting and arbitrage checks
└── test_valider_marche.py Time measurement, borrow calibration, implied vols
flowchart LR
P[pricer.py] --> G[greeks.py]
G --> D[dividendes.py]
P --> C[couverture.py]
S[svi.py] --> M[valider_marche.py]
D --> M
P --> B["bench*.py"]
G --> B
G --> V["valider_pricer / lsm / dividendes<br/>(QuantLib)"]
D --> V
C --> VC[valider_couverture.py]
Python 3.10 or later.
git clone https://github.com/matthieu-briche/python-options-pricing.git
cd python-options-pricing
python -m pip install numpy scipy numba pytest pytest-cov hypothesisFor external validation (optional):
python -m pip install QuantLib pandas matplotlib yfinancefrom pricer import BlackScholes, bs_price, mc_european, rqmc_european, mc_asian_arithmetic, lsm_american_put
m = BlackScholes(s0=100, r=0.05, sigma=0.2) # continuous dividend q = 0 by default
print(bs_price(m, K=100, T=1)) # 10.4506 (closed form)
print(mc_european(m, K=100, T=1)) # 10.447964 ± 0.003823 (95% CI [...], N=2,000,000)
print(rqmc_european(m, K=100, T=1)) # 10.450554 ± 0.000055
print(mc_asian_arithmetic(m, K=100, T=1, n_steps=52))
print(lsm_american_put(m, K=100, T=1)) # 6.080280 ± 0.031559bs_price is vectorised: you can pass an array of strikes or maturities.
from greeks import bs_greeks, mc_european_greeks, crr_american
exact = bs_greeks(m, K=100, T=1) # closed form
mc = mc_european_greeks(m, K=100, T=1) # price + 5 Greeks, with CI
print(mc["delta"]) # 0.636969 ± 0.000282
amer = crr_american(m, K=100, T=1, kind="put") # American, CRR treefrom dividendes import crr_american_div
divs = [(0.25, 1.5), (0.50, 1.5), (0.75, 1.5)] # (date in years, amount)
print(crr_american_div(m, K=100, T=1, kind="put", dividends=divs, model="escrowed"))import numpy as np
from pricer import BlackScholes
from couverture import simuler_couverture, OptionCouverture
m = BlackScholes(s0=100, r=0.03, sigma=0.20) # hedging vol
sigma_reelle = np.random.default_rng(0).uniform(0.15, 0.25, 100_000) # realised vol per path
delta = simuler_couverture(m, K=100, T=0.5, n_reb=52, sigma_reelle=sigma_reelle)
gamma0 = simuler_couverture(m, K=100, T=0.5, n_reb=52, sigma_reelle=sigma_reelle,
couverture_gamma=OptionCouverture(K=100, T=1.0))
print(delta.resume()["ecart_type_rel"], gamma0.resume()["ecart_type_rel"]) # ≈ 0.17 0.02| Method | Function | Pagès, chapter |
|---|---|---|
| Exact simulation of S_T (no discretisation bias) | mc_european |
2 |
| Confidence interval, online block-wise standard error | MCResult, _RunningStats |
2.1 |
| Antithetic variates | mc_european, mc_european_greeks |
3.1.2 |
| Control variate with optimal β | mc_european, mc_asian_arithmetic, mc_asian_greeks |
3.2 |
| Randomised quasi-Monte Carlo (scrambled Sobol) | rqmc_european |
4.4 |
| Pathwise Greeks (tangent process) | mc_european_greeks, mc_asian_greeks |
2.2.4, 10 |
| Gamma: mixed pathwise / likelihood ratio estimator | same | 2.2.3, 10 |
| Finite differences with common random numbers | fd_greeks_crn |
10 |
| Longstaff–Schwartz (polynomial regression) | lsm_american_put |
12 |
| Cox–Ross–Rubinstein binomial tree | crr_american, crr_american_div |
12 |
Why a mixed estimator for Gamma? The pathwise Delta of a call, e^{−rT} 1{S_T > K} S_T / S_0, is discontinuous at K: it cannot be differentiated a second time path by path. The likelihood ratio method is therefore applied to the pathwise Delta, which amounts to weighting it by Z/(σ√T) − 1.
Why common random numbers? A finite difference (V(S+h) − V(S−h)) / 2h computed with two independent simulations is drowned in Monte Carlo noise. Using the same seed for both prices makes the noise cancel out.
Why SVI rather than a polynomial? Wings linear in variance (consistent with Roger Lee's theorem), sound extrapolation beyond quoted strikes, interpretable parameters and verifiable no-arbitrage conditions.
| Technique | Effect |
|---|---|
| Variance reduction | up to ×36 on the confidence interval at equal time |
| One simulation for the price and all 5 Greeks | avoids re-pricing by finite differences |
| NumPy vectorisation | no Python loop over paths |
Numba @njit(parallel=True) kernels |
Asian, Asian Greeks, LSM in multi-core machine code |
| Block computation + online statistics | bounded memory, data stays in CPU cache |
PCG64 generator + SeedSequence |
reproducible results |
The first call to a Numba function compiles it (a few seconds); the compiled code is then cached on disk.
python -m pytest # 233 tests (≈ 1 min)
python -m pytest -m "not slow" # 220 tests, without the heavy Monte Carlo runs| File | Tests | What is checked |
|---|---|---|
test_closed_form.py |
42 | Reference values, put-call parity, arbitrage bounds, Black–Scholes PDE (Θ + ½σ²S²Γ + (r−q)SΔ − rV = 0) |
test_monte_carlo.py |
31 | Unbiasedness, 1/√N error, actual coverage of the 95% CI, reproducibility |
test_greeks_mc.py |
43 | Each MC estimator vs. the exact formula, finite differences, CRR tree |
test_proprietes.py |
59 | Properties holding for any parameter set (Hypothesis): monotonicity, convexity in strike, bounds, tree convergence |
test_dividendes.py |
13 | Discrete-dividend tree vs. QuantLib reference values |
test_svi.py |
18 | SVI fitting, butterfly and calendar conditions |
test_valider_marche.py |
27 | Time measurement (time zone, ACT/365), borrow calibration, vol inversion |
Monte Carlo tests use fixed seeds: an estimate passes if its distance to the reference is less than 4 standard errors.
python valider_pricer.py --module adaptateur_crr # CRR tree vs QuantLib
python valider_lsm.py # Longstaff–Schwartz vs QuantLib
python valider_dividendes.py # discrete dividends vs QuantLib
python valider_couverture.py # hedging vs theory (≈ 20 s)
# Real chain (Yahoo Finance, delayed data)
python valider_marche.py --ticker AAPL --taux 0.04 # set today's risk-free rate
# Offline: synthetic chain priced by QuantLib
python generer_chaine_test.py
python valider_marche.py --chaine chaine_test.csv --dividendes dividendes_test.csv \
--spot 227.5 --date 2026-10-06 --taux 0.04Output files (rapport_*.csv, resultats_marche_*.csv, smiles_*.png…) can be
regenerated and are not version-controlled.
- Black–Scholes pricer: constant volatility and rates. The SVI smile is used for market validation (implied vol per strike); there is no local or stochastic volatility in the pricer.
- Longstaff–Schwartz gives a lower bound on the price (estimated exercise policy); for vanilla American options, the CRR tree is more accurate.
- Time measurement in
valider_marche.py: ACT/365 to the second, without an exchange calendar (weekends, holidays); AM-settled index options are out of scope. - Hedging: no transaction costs or market impact.
- G. Pagès, Numerical Probability: An Introduction with Applications to Finance, Universitext, Springer, 2018.
- B. Bouchard, J.-F. Chassagneux, Fundamentals and Advanced Techniques in Derivatives Hedging, Universitext, Springer, 2016.
- J. Gatheral, "A parsimonious arbitrage-free implied volatility parameterization", Global Derivatives, 2004.
- R. Lee, "The Moment Formula for Implied Volatility at Extreme Strikes", Mathematical Finance, 14(3), 2004.
- F. Longstaff, E. Schwartz, "Valuing American Options by Simulation: A Simple Least-Squares Approach", Review of Financial Studies, 14(1), 2001.
- J. Cox, S. Ross, M. Rubinstein, "Option Pricing: A Simplified Approach", Journal of Financial Economics, 7(3), 1979.
- P. Glasserman, Monte Carlo Methods in Financial Engineering, Springer, 2003.
Matthieu Briche · LinkedIn

