Modern Black-Scholes-Merton pricing, Greeks, and implied volatility for Python. Rust core. Vectorized. Drop-in replacement for the abandoned py_vollib.
from pyvolr import bs
bs.price("c", S=100, K=105, T=0.5, r=0.05, sigma=0.2) # 4.581680167540007|
|
|
Six libraries on the chart: pyvolr, vollib (resurrected upstream of py_vollib, pure Python), py_vollib_vectorized (numba), blackscholes (pure Python, object-per-call), QuantLib (C++ core, looped scalar), and quantforge (Rust + SIMD).
pyvolr leads the non-SIMD field at every batch size — ~2.4× faster than py_vollib_vectorized (numba), ~4× faster than QuantLib's looped scalar, 10×+ faster than the pure-Python libraries. quantforge (Rust + SIMD) is faster from ~1k strikes up, on the explicit-vectorisation axis fast-vollib takes with Triton kernels — a trade pyvolr deliberately skips. pyvolr is the "Rust-cored CPU option": no unsafe SIMD intrinsics, no GPU dependency, an abi3 wheel in one file, and ~1-ULP accuracy into the deep-OTM tail where quantforge and blackscholes underflow to zero (see Numerical agreement below). For raw batch throughput prefer quantforge; for a correct, dependency-light CPU pricer, pyvolr.
| Scenario | pyvolr | py_vollib | speedup |
|---|---|---|---|
bs.price, scalar |
4.2 µs | 2.2 µs | 0.5× |
bs.price, 1k strikes |
43.3 µs | 2.32 ms | 54× |
bs.price, 10k strikes |
350 µs | 23.32 ms | 67× |
bs.price, 100k strikes |
3.48 ms | 234.91 ms | 68× |
bs.price, 1M strikes |
34.10 ms | 2,350 ms | 69× |
bs.greeks (all 5), 10k |
273 µs | 89.95 ms | 330× |
bs.implied_vol, scalar |
4.4 µs | 15.0 µs | 3.4× |
bs.implied_vol, 10k strikes |
465 µs | 128 ms ¹ | 275× |
black76.price, scalar |
3.7 µs | 2.2 µs | 0.6× |
black76.price, 10k strikes |
346 µs | 23.19 ms | 67× |
black76.implied_vol, scalar |
3.9 µs | 14.7 µs | 3.8× |
¹ py_vollib's implied_volatility is scalar-only; the 10k figure is N × scalar measured via compare_py_vollib.py. pyvolr's vectorised path parallelises automatically above N=1024 via rayon — set RAYON_NUM_THREADS=1 to force serial.
Pricing throughput note: the
bs.price/black76.pricerows use the normalised-Black engine — ~1-ULP accurate into the deep-OTM tail, ~2.3× slower on vectorised pricing than the prior textbookS·Φ(d1) − K·Φ(d2)form. A deliberate accuracy-for-speed trade; Greeks and IV are unaffected.
The table above is the headline-vs-the-abandoned-upstream comparison (py_vollib's last release is broken on Python 3.12+, see docs/why.md). For the workload most people actually run — a smile, an option chain, an IV snapshot — pyvolr is tens of times faster than the abandoned py_vollib upstream and installs cleanly on every modern Python.
bs.greeks returning all five Greeks at once uses a single-pass Rust kernel that shares d1/d2, discount factors, cdf, and pdf across the five outputs — ~3× faster than the equivalent five separate calls. For batches ≥4096 rows, the work also dispatches across CPU cores in parallel.
Numerical agreement: pyvolr matches every library above to f64 precision (~1e-13 relative) on every well-posed input across price + 5 Greeks + IV. At deep-OTM short-expiry corners pyvolr is more precise than the rest — blackscholes and quantforge underflow to zero where pyvolr's erfcx-based cdf retains the ~1e-50 price; QuantLib and the alternatives lose 1-2 digits. Run python bench/sanity_check_competitors.py in each venv to re-validate.
Reproduce the table with python bench/compare_py_vollib.py; reproduce the chart with python bench/compare_competitors.py bench then python bench/compare_competitors.py chart (across the Python 3.11 + 3.12 venvs documented in the script's docstring). Library versions: Apple M4 Pro / Python 3.10.20 / numpy 2.2.6 / pyvolr 0.1.4 / py_vollib 1.0.1 (table) / vollib 1.0.7 / py_vollib_vectorized 0.1.1 / blackscholes 0.2.0 / QuantLib 1.42.1 / quantforge 0.1.1 (chart).
pip install pyvolrOr via uv:
uv pip install pyvolrPre-built wheels are published for Linux (x86_64, aarch64), macOS (Intel, Apple Silicon), and Windows (x86_64) across Python 3.10–3.14, plus a free-threaded build for 3.14t. (3.13t wheels were last published at pyvolr 0.1.3 — cibuildwheel 4 dropped Python 3.13 free-threading, which never left experimental status.)
| 3.10 | 3.11 | 3.12 | 3.13 | 3.14 | |
|---|---|---|---|---|---|
| Linux | ✅ | ✅ | ✅ | ✅ | ✅ |
| macOS | ✅ | ✅ | ✅ | ✅ | ✅ |
| Windows | — | — | ✅ | ✅ | ✅ |
Every push and PR runs the full pytest + cargo test suites across the matrix above. Windows × {3.10, 3.11} are skipped intentionally to keep CI minutes reasonable — the wheels themselves still build for those combinations and are published. The free-threaded wheel (3.14t) is built and exercised through cibuildwheel's in-wheel test pass on every release across Linux/macOS/Windows, and on packaging-touching PRs via the wheel-smoke check.
From source (requires Rust):
git clone https://github.com/yipjunkai/pyvolr
cd pyvolr
uv venv --python 3.12 && source .venv/bin/activate
uv pip install -e ".[dev,test]"
maturin develop --releaseimport numpy as np
from pyvolr import bs
# Scalar
bs.price("c", S=100, K=105, T=0.5, r=0.05, sigma=0.2)
# Vectorized — broadcast over any combination of inputs
strikes = np.linspace(80, 120, 41)
prices = bs.price("c", S=100, K=strikes, T=0.5, r=0.05, sigma=0.2)
# All five Greeks in one call
greeks = bs.greeks("c", S=100, K=strikes, T=0.5, r=0.05, sigma=0.2)
# {"delta": [...], "gamma": [...], "theta": [...], "vega": [...], "rho": [...]}
# Implied volatility from a market price
bs.implied_vol(price=5.20, flag="c", S=100, K=100, T=0.25, r=0.05)
# Broadcasting works in any dimension
strike_grid = np.linspace(80, 120, 5).reshape(-1, 1)
vol_grid = np.linspace(0.10, 0.40, 4).reshape(1, -1)
surface = bs.price("c", S=100, K=strike_grid, T=0.5, r=0.05, sigma=vol_grid)
# shape (5, 4)
# Black-76 for options on futures / forwards — same API, F replaces S, no q.
from pyvolr import black76
black76.price("c", F=100, K=105, T=0.5, r=0.05, sigma=0.2)- Black-Scholes-Merton pricing — calls and puts with continuous dividend yield
- Black-76 pricing — European options on futures/forwards (
pyvolr.black76), same vectorized API asbs - Analytical Greeks — delta, gamma, theta, vega, rho (with documented sign and unit conventions)
- Robust implied volatility — Jäckel "Let's Be Rational" algorithm: rational-cubic initial guess plus Householder order-4 iteration converges to ~1e-13 precision in ≤2 iterations across the full no-arbitrage range
- Automatic parallelism on large batches —
implied_vol(above N≈1,000 rows) and the bundledgreekskernel (above N≈4,000) release the GIL and dispatch per-row work to rayon's global thread pool; setRAYON_NUM_THREADS=1to opt out - Full numpy broadcasting — any combination of inputs in any shape, scalar-in scalar-out
py_vollibdrop-in shim —pyvolr.compat.py_vollibmirrors the upstream module tree (includingpy_vollib.black) for one-import-line migration- Rust core, no compiler needed — abi3 wheels for Python 3.10–3.14 × {Linux, macOS, Windows}
- Free-threaded Python ready — a dedicated 3.14t wheel: with no GIL, every entry point scales across threads. On standard (GIL) builds, the large-batch
implied_vol(≥1k rows) and bundledgreeks(≥4k rows) kernels release the GIL while rayon works;priceand single-Greek calls hold it - Typed end-to-end — pyright-strict library code, full type stubs for the Rust extension
- Drop-in compat shim for
py_vollib_vectorized(vectorized_*API +price_dataframe/get_all_greeks, pandas as soft dep) - Bachelier (normal model, for negative rates)
- Higher-order Greeks (vanna, vomma, charm, speed, zomma, color)
- SIMD batch evaluation
- American options (CRR binomial → finite difference)
- Volatility surface fitting (SVI, SSVI)
Replace your imports — the signatures and 'c'/'p' flag convention are preserved exactly:
# Before
from py_vollib.black_scholes import black_scholes
from py_vollib.black_scholes.greeks.analytical import delta
from py_vollib.black_scholes.implied_volatility import implied_volatility
from py_vollib.black import black # futures options
# After
from pyvolr.compat.py_vollib.black_scholes import black_scholes
from pyvolr.compat.py_vollib.black_scholes.greeks.analytical import delta
from pyvolr.compat.py_vollib.black_scholes.implied_volatility import implied_volatility
from pyvolr.compat.py_vollib.black import black # futures optionsThe compat shim also preserves py_vollib's unit conventions: vega is per-1% vol, theta is per-day, rho is per-1% rate, and implied_volatility takes flag as its last argument. For new code, prefer the modern pyvolr.bs API — it accepts numpy arrays, broadcasts naturally, uses per-unit conventions consistently, and returns all Greeks in a single call.
py_vollib has been broken on Python 3.12+ since the release — a transitive dependency imports DBL_MIN / DBL_MAX from CPython's internal _testcapi test module, which isn't shipped with modern Python distributions. The fix is two lines (sys.float_info.{min,max} are the correct sources), but py_lets_be_rational hasn't released since 2017, py_vollib since 2020, and the maintainers are gone.
Full backstory: docs/why.md.
pyvolr/
├── crates/core/ # Rust numerical core
│ ├── src/
│ │ ├── lib.rs # PyO3 bindings (flat-array entry points)
│ │ ├── bsm.rs # BSM pricing, d1/d2, forward price
│ │ ├── black76.rs # Black-76 (futures options) — delegates to BSM with q=r
│ │ ├── greeks.rs # Delta, gamma, theta, vega, rho
│ │ ├── iv.rs # Jäckel "Let's Be Rational" IV solver (Householder-4, ≤2 iters)
│ │ └── normal.rs # Φ / φ, erfcx (Lentz CF), inverse CDF (Wichura AS241)
│ └── benches/ # criterion: perf-gate contracts (pricing) + experiment harness (experiments)
├── bench/ # Python-level speed/precision scripts (dev-only, not in CI)
│ ├── compare_py_vollib.py # reproduces the perf table
│ ├── compare_competitors.py # reproduces the perf chart (6 libraries)
│ └── sanity_check_competitors.py # cross-validates numerical agreement
├── python/pyvolr/
│ ├── bs.py # BSM public API (numpy-broadcast wrappers)
│ ├── black76.py # Black-76 public API
│ ├── _wrappers.py # Shared FFI helpers (broadcast, flag normalize)
│ ├── _core.pyi # Type stubs for the Rust extension
│ └── compat/py_vollib/ # Drop-in shim mirroring py_vollib's tree
├── tests/ # pytest + hypothesis property tests
├── .github/workflows/ # ci, release, release-please, differential, fuzz, perf, security, scorecard, stale
├── .github/scripts/ # CI helper scripts (perf-gate comparator)
├── Cargo.toml # Rust workspace
└── pyproject.toml # maturin build backend + project config
| Function | Returns | Vectorized over |
|---|---|---|
bs.price(flag, S, K, T, r, sigma, q=0) |
option price | all numeric inputs |
bs.delta(flag, S, K, T, r, sigma, q=0) |
∂Price/∂S | all numeric inputs |
bs.gamma(S, K, T, r, sigma, q=0) |
∂²Price/∂S² | all numeric inputs |
bs.vega(S, K, T, r, sigma, q=0) |
∂Price/∂σ (per unit vol) | all numeric inputs |
bs.theta(flag, S, K, T, r, sigma, q=0) |
−∂Price/∂T (per year) | all numeric inputs |
bs.rho(flag, S, K, T, r, sigma, q=0) |
∂Price/∂r (per unit r) | all numeric inputs |
bs.greeks(flag, S, K, T, r, sigma, q=0) |
dict of all five Greeks |
all numeric inputs |
bs.implied_vol(price, flag, S, K, T, r, q=0) |
σ (NaN on bound violation) | price + numeric inputs |
black76.price(flag, F, K, T, r, sigma) |
option price on a forward | all numeric inputs |
black76.{delta,gamma,vega,theta,rho}(...) |
Greeks for Black-76 | all numeric inputs |
black76.greeks(flag, F, K, T, r, sigma) |
dict of all five Greeks |
all numeric inputs |
black76.implied_vol(price, flag, F, K, T, r) |
σ (NaN on bound violation) | price + numeric inputs |
pyvolr.compat.py_vollib.… |
py_vollib-shaped scalars | n/a (scalar API) |
flag accepts 'c'/'C' (call), 'p'/'P' (put), or an array thereof.
py_vollib died because nobody was paid to maintain it. pyvolr is engineered to outlive its maintainer:
- One-click releases via release-please + PyPI Trusted Publishing — PyPI publication needs no stored credentials (OIDC), and release-please authenticates as a repo-scoped GitHub App rather than a user PAT, so the credential survives a maintainer handoff
- Release-gated differential tests against
py_vollib(Python 3.10 sidecar) — every release is blocked unless pyvolr still matches the reference - Wide CI matrix (Python 3.10–3.14 × Linux/macOS/Windows) — the specific failure mode that killed the predecessor
- All GitHub Actions pinned with weekly Dependabot bumps, hardening against supply-chain attacks
- Hand-off plan documented in GOVERNANCE.md
Commercial sponsorship channels will be added if demand warrants. For now the best support is real-world use, good bug reports, and PRs.
See CONTRIBUTING.md. Particularly welcome: new pricing models (Bachelier, American), higher-order Greeks, SIMD/vectorization work, and property tests for edge cases.
Dual-licensed under MIT or Apache 2.0, at your option.
Algorithms are reimplemented from published references (Hull, Merton, Jäckel); no third-party source code is incorporated.