Metadata-Version: 2.4
Name: pyvolr
Version: 0.1.7
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Rust
Classifier: Topic :: Office/Business :: Financial
Classifier: Topic :: Scientific/Engineering :: Mathematics
Classifier: Typing :: Typed
Requires-Dist: numpy>=1.26
Requires-Dist: maturin>=1.13 ; extra == 'dev'
Requires-Dist: ruff>=0.15 ; extra == 'dev'
Requires-Dist: pyright>=1.1.400 ; extra == 'dev'
Requires-Dist: pre-commit>=4.0 ; extra == 'dev'
Requires-Dist: pandas>=2.2 ; extra == 'pandas'
Requires-Dist: pytest>=9.0 ; extra == 'test'
Requires-Dist: pytest-benchmark>=5.0 ; extra == 'test'
Requires-Dist: hypothesis>=6.150 ; extra == 'test'
Requires-Dist: pandas>=2.2 ; extra == 'test'
Provides-Extra: dev
Provides-Extra: pandas
Provides-Extra: test
License-File: LICENSE-MIT
License-File: LICENSE-APACHE
Summary: Modern Black-Scholes-Merton pricing, Greeks, and implied volatility for Python. Rust core. Drop-in py_vollib replacement.
Keywords: black-scholes,black-scholes-merton,implied-volatility,options,greeks,quantitative-finance,py_vollib,vollib
Home-Page: https://github.com/yipjunkai/pyvolr
Author: pyvolr contributors
License-Expression: MIT OR Apache-2.0
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Changelog, https://github.com/yipjunkai/pyvolr/blob/main/CHANGELOG.md
Project-URL: Homepage, https://github.com/yipjunkai/pyvolr
Project-URL: Issues, https://github.com/yipjunkai/pyvolr/issues
Project-URL: Repository, https://github.com/yipjunkai/pyvolr

# pyvolr

[![PyPI](https://img.shields.io/pypi/v/pyvolr.svg)](https://pypi.org/project/pyvolr/)
[![Python versions](https://img.shields.io/pypi/pyversions/pyvolr.svg)](https://pypi.org/project/pyvolr/)
[![Wheel](https://img.shields.io/pypi/wheel/pyvolr.svg)](https://pypi.org/project/pyvolr/#files)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/yipjunkai/pyvolr/badge)](https://scorecard.dev/viewer/?uri=github.com/yipjunkai/pyvolr)
[![CI](https://github.com/yipjunkai/pyvolr/actions/workflows/ci.yml/badge.svg)](https://github.com/yipjunkai/pyvolr/actions/workflows/ci.yml)
[![License](https://img.shields.io/pypi/l/pyvolr.svg)](#-license)

**Modern Black-Scholes-Merton pricing, Greeks, and implied volatility for Python.** Rust core. Vectorized. Correct in the tails. Drop-in compatible with `py_vollib`/`vollib`.

```python
from pyvolr import bs

bs.price("c", S=100, K=105, T=0.5, r=0.05, sigma=0.2) # 4.581680167540007
```

## ⚡ Performance

<table>
<tr>
<td>

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/yipjunkai/pyvolr/main/docs/assets/perf-competitors-thru-dark.svg">
  <img alt="Throughput: pyvolr vs the active BSM-pricing ecosystem, log-log by array size" src="https://raw.githubusercontent.com/yipjunkai/pyvolr/main/docs/assets/perf-competitors-thru-light.svg">
</picture>

</td>
<td>

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/yipjunkai/pyvolr/main/docs/assets/accuracy-iv-tail-dark.svg">
  <img alt="Implied-vol recovery accuracy (correct digits, higher is better) vs option-price depth: pyvolr stays f64-exact down to prices of 1e-215 while the fast 2026 entrants and QuantLib fall to zero correct digits between 1e-6 and 1e-26" src="https://raw.githubusercontent.com/yipjunkai/pyvolr/main/docs/assets/accuracy-iv-tail-light.svg">
</picture>

</td>
</tr>
</table>

pyvolr **owns implied vol at scale** — 2× faster than fast-vollib's numba backend and 12× faster than opengreeks at 1M solves — and ties fast-vollib on the bundled five-Greeks kernel. Since 0.1.7 its parallel `price` path **leads bulk throughput too** — 1M strikes in ~4 ms, ahead of fast-vollib's multithreaded numba kernel and ~5× ahead of opengreeks — closing the one row it used to concede (at 10k it lands a near-tie 2nd, just behind fast-vollib's numba and ahead of opengreeks). Its scalar-call latency, once 20–30× behind opengreeks' dedicated FFI, closed to under 2.5× with the 0.1.6 scalar fast path (both now sub-microsecond). And it stays **correct in the tail** the whole time: pushed deep out-of-the-money, every fast 2026 entrant starts returning **silently wrong** implied vols (a wrong-constant σ, with onset between prices of ~1e-6 and ~1e-26), while pyvolr — with the other two Let's-Be-Rational descendants, vollib and py_vollib_vectorized — stays f64-exact down to prices of 1e-215.

| Workload                      |       pyvolr | fast-vollib 0.1.6 ¹ | opengreeks 0.2.0 | vollib 1.0.11 ² |
| ----------------------------- | -----------: | ------------------: | ---------------: | --------------: |
| `bs.price`, scalar ⁴          |      0.25 µs |               88 µs |      **0.13 µs** |          1.5 µs |
| `bs.price`, 10k strikes       |       189 µs |          **174 µs** |           223 µs |         14.7 ms |
| `bs.price`, 1M strikes        |   **4.2 ms** |              4.4 ms |          21.9 ms |          1.53 s |
| `bs.implied_vol`, scalar      |      0.50 µs |              156 µs |      **0.21 µs** |         13.2 µs |
| `bs.implied_vol`, 10k         |   **449 µs** |              727 µs |          3.35 ms |         97.7 ms |
| `bs.implied_vol`, 1M          |  **27.1 ms** |             53.2 ms |           331 ms |        ≈9.7 s ³ |
| `bs.greeks` (all 5), 10k      |       251 µs |          **191 µs** |           566 µs |         47.0 ms |
| `bs.greeks` (all 5), 1M       |   **5.0 ms** |              5.2 ms |          56.1 ms |        ≈4.7 s ³ |

¹ The numba backend — fast-vollib's fast CPU path (a plain `pip install fast-vollib` runs its numpy backend, 5–7× slower). One-time ~0.4 s JIT warmup per function excluded.
² vollib (the revived `py_vollib` upstream — see [docs/why.md](docs/why.md)) is scalar-only pure Python: the migration baseline, not a vectorization competitor. pyvolr runs its workloads 45–360× faster in batch.
³ Extrapolated ×10 from the measured 100k-row time (scalar loop).
⁴ pyvolr's `price` uses the normalised-Black engine — ~1-ULP into the deep-OTM tail, ~2.3× slower per core than the textbook `S·Φ(d1) − K·Φ(d2)` form (that is opengreeks' per-core price edge, which pyvolr's rayon parallelism overtakes at scale — see the 1M row). A deliberate trade for tail accuracy; Greeks and IV are unaffected.

Also on the throughput chart: [`py_vollib_vectorized`](https://pypi.org/project/py_vollib_vectorized/) (numba, unmaintained since 2021), [`blackscholes`](https://pypi.org/project/blackscholes/) (pure Python), [`QuantLib`](https://pypi.org/project/QuantLib/) (C++ core, looped scalar), and [`quantforge`](https://pypi.org/project/quantforge/) (Rust + rayon, reduced-precision `fast_erf`; unmaintained since Sep 2025). pyvolr parallelises via rayon above per-endpoint thresholds — `implied_vol` ≥1k, bundled `greeks` ≥4k, `price` ≥8k, single Greeks ≥16k (each gate set above its measured serial-vs-rayon break-even); `RAYON_NUM_THREADS=1` forces serial.

**Numerical agreement:** pyvolr matches every library above to f64 precision (~1e-13) on all well-posed inputs across price + 5 Greeks + IV (`bench/sanity_check_competitors.py`). The edges differ: `blackscholes` underflows deep-OTM prices to zero and `quantforge` hard-clamps Φ at ±8σ where pyvolr's `erfcx`-based cdf keeps the ~1e-50 price; the IV tail is the right chart, methodology in `bench/compare_tail_accuracy.py` (a known-σ ladder priced through pyvolr's mpmath-golden-pinned forward map).

Reproduce it all with **`just all`**, or per-chart via the recipes in the [`justfile`](justfile) (needs [`just`](https://just.systems) + [`uv`](https://docs.astral.sh/uv); uv builds the pinned environments on demand). Measured on an Apple M4 Pro: `price` vector rows on pyvolr 0.1.7 (the parallel path), scalar rows on 0.1.6 (the scalar fast path), IV and Greeks rows and the accuracy chart unchanged from 0.1.5 (those code paths are untouched); the throughput chart is regenerated for the 0.1.7 parallel `price`. Competitor versions are pinned in the justfile.

## 📦 Install

```bash
pip install pyvolr
```

Or via [`uv`](https://github.com/astral-sh/uv):

```bash
uv pip install pyvolr
```

Pre-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.)

### Tested on

|         | 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):

```bash
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 --release
```

## 🚀 Quick start

```python
import 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)
```

## ✨ Features

- **Black-Scholes-Merton pricing** — calls and puts with continuous dividend yield
- **Black-76 pricing** — European options on futures/forwards (`pyvolr.black76`), same vectorized API as `bs`
- **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** — every array endpoint releases the GIL and dispatches per-row work to rayon's global thread pool above its own measured threshold: `implied_vol` ≥1k rows, bundled `greeks` ≥4k, `price` ≥8k, single Greeks ≥16k (each gate sits above that endpoint's serial-vs-rayon break-even, so small calls never pay rayon's overhead); set `RAYON_NUM_THREADS=1` to opt out
- **Full numpy broadcasting** — any combination of inputs in any shape, scalar-in scalar-out
- **`py_vollib` drop-in shims** — `pyvolr.compat.py_vollib` mirrors the upstream module tree (including `py_vollib.black`) for one-import-line migration; `pyvolr.compat.py_vollib_vectorized` mirrors the vectorized API (`vectorized_*`, `get_all_greeks`, `price_dataframe`)
- **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, every array endpoint releases the GIL above its threshold (`implied_vol` ≥1k, bundled `greeks` ≥4k, `price` ≥8k, single Greeks ≥16k) so rayon can work while other Python threads run; below the gate, and for scalar calls, the work is too small to bother and the GIL is held
- **Typed end-to-end** — pyright-strict library code, full type stubs for the Rust extension

## 🗺️ Coming soon

- [ ] Bachelier (normal model, for negative rates) — with analytic implied-normal-vol inversion
- [ ] Higher-order Greeks (vanna, vomma, charm, speed, zomma, color)
- [ ] American options (Andersen-Lake-Offengenden spectral collocation)
- [ ] Volatility surface fitting (arbitrage-free eSSVI)

SIMD batch evaluation used to be on this list and was deliberately dropped: no vectorized math library currently meets pyvolr's precision bar on the erfc-dependent tails (~1 ULP against 60-digit references), and the crate forbids `unsafe` code. Batch throughput comes from rayon parallelism instead.

## 🔄 Migrating from py_vollib

Replace your imports — the signatures and `'c'`/`'p'` flag convention are preserved exactly:

```python
# 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 options
```

The 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.

### Migrating from `py_vollib_vectorized`

`pyvolr.compat.py_vollib_vectorized` mirrors the vectorized API — the `vectorized_*` functions, `get_all_greeks`, and `price_dataframe` — with the same (quirky) argument orders, `return_as` values, and `"Price"`/`"IV"`/greek column names:

```python
# Before
from py_vollib_vectorized import vectorized_black_scholes, vectorized_implied_volatility

# After
from pyvolr.compat.py_vollib_vectorized import vectorized_black_scholes, vectorized_implied_volatility
```

Two deliberate differences: importing the shim does **not** monkeypatch `py_vollib` (call the `vectorized_*` functions directly), and `pandas` is an optional dependency — `return_as="dataframe"`/`"series"` need it (`pip install pyvolr[pandas]`) and otherwise fall back to numpy with a one-time warning.

## 🤔 Why pyvolr exists

`py_vollib` spent six years abandoned (2020–2026) and hard-broken on Python 3.12+ — a transitive dependency imported `DBL_MIN` / `DBL_MAX` from CPython's internal `_testcapi` test module. pyvolr was built so that failure mode cannot recur: the numerical core is Rust and touches no CPython internals, and the release pipeline is engineered to survive its maintainer ([GOVERNANCE.md](GOVERNANCE.md)).

In April 2026 the upstream was revived as [`vollib`](https://pypi.org/project/vollib/), which fixes the import error. It doesn't change the math: vollib remains scalar-only pure Python — 45–360× slower on the batch workloads in the table above — with no Python 3.14 or free-threaded wheels.

Full backstory, including the revival: [docs/why.md](docs/why.md).

## 📁 Project structure

Directory-level map; each module documents itself in its docstring/rustdoc header.

```text
pyvolr/
├── crates/core/     # Rust numerical core (BSM, Black-76, Greeks, Jäckel IV, Φ/erfcx) + criterion benches
├── python/pyvolr/   # numpy-broadcasting public API, type stubs, py_vollib(+vectorized) compat shims
├── tests/           # pytest + hypothesis property tests
├── bench/           # dev-only speed/accuracy benchmarks vs the competitor field (not in CI)
├── fuzz/            # cargo-fuzz targets for the numerical core
├── tools/           # regenerable mpmath golden generator
├── docs/            # backstory (why.md) + chart assets
├── .github/         # CI/release workflows (all SHA-pinned) + helper scripts
└── justfile         # benchmark-reproduction recipes (`just all`, `just perf-stat`, ...) via uv
```

## 📚 API reference

| 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)       |
| `pyvolr.compat.py_vollib_vectorized.…`         | arrays / DataFrame         | all numeric inputs     |

`flag` accepts `'c'`/`'C'` (call), `'p'`/`'P'` (put), or an array thereof.

Every function also accepts a keyword-only `return_as`: `"numpy"` (default — array, or scalar for scalar input), `"dict"` (`{name: value}`), or `"dataframe"` (a pandas DataFrame; `pandas` is an optional dependency, installed only if you use this mode).

`implied_vol` additionally accepts a keyword-only `on_error` for unsolvable inputs (price outside the no-arbitrage bounds, non-positive `T`/`S`/`K`, or non-finite input): `"warn"` (default — emit an `ImpliedVolWarning` and return NaN), `"raise"` (raise `ImpliedVolError`), or `"ignore"` (return NaN silently).

## 🛡️ Sustainability

`py_vollib` went dark for six years because nobody was paid to maintain it — its 2026 revival came only after the ecosystem had moved on. 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](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.

## 🤝 Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md). Particularly welcome: new pricing models (Bachelier, American), higher-order Greeks, and property tests for edge cases.

## 📄 License

Dual-licensed under [MIT](LICENSE-MIT) or [Apache 2.0](LICENSE-APACHE), at your option.

Algorithms are reimplemented from published references (Hull, Merton, Jäckel); no third-party source code is incorporated.

