polars-vol
Black-Scholes pricing, analytical Greeks and implied volatility as Rust-powered Polars expressions — elementwise, columnar, and streaming-parallel.
Installation
From PyPI, with pip or uv:
pip install polars-vol
uv add polars-vol # in a uv-managed project
uv pip install polars-vol # with the uv pip interface
From source (requires a Rust toolchain and maturin):
git clone https://github.com/p3i0t/polars_vol && cd polars_vol
pip install maturin # or: uv pip install maturin
maturin develop --release
Wheels are abi3 (CPython ≥ 3.9) for macOS, Linux and Windows; polars ≥ 1.3 is installed automatically as a dependency.
Features
The following list are implemented as polars.Expr:
black_scholes(strike, spot, vol, time_to_expiry, rate, is_call)— European call/put priceblack_scholes_with_greeks(strike, spot, vol, ...)— price and analytical Greeks (model_price, delta, gamma, vega, theta, rho) in a single passimplied_vol(option_price, ...)— implied volatility from market prices, via bracketed Newton or Halley (method="halley") iterationiv_and_greeks(option_price, ...)— the solved implied volatility plus all Greeks in one expression
Thanks to the polars collect(engine="streaming"), we can easily get >5x speedup than vectorized numpy.
Quick Start
import polars as pl
import polars_vol as pv
df = pl.DataFrame({
"strike": [100, 105, 110],
"spot": [100, 100, 100],
"time_to_expiry": [0.5, 0.5, 0.5]
}).with_columns(rate=pl.lit(0.05), vol=pl.lit(0.2), is_call=pl.lit(True))
df = df.with_columns(
option_price=pv.black_scholes(
"strike", "spot", "vol", "time_to_expiry", "rate", "is_call"
)
)
print(df)
# backfill iv from option price
iv = df.select(pv.implied_vol(
"option_price", "strike", "spot", "time_to_expiry", "rate", "is_call"
).alias('iv'))
print(iv)
# backfill iv and greeks from option price
df.select(result=pv.iv_and_greeks(
"option_price", "strike", "spot", "time_to_expiry", "rate", "is_call", method="newton" # or "halley"
)).unnest("result")
Examples
SPX backfill
You may also want to see a real example on SPX examples/spx_backfill.py, where it fetches the SPX chain
from Yahoo, solves the implied volatility of every option from its market
mid with implied_vol, and checks the result against Yahoo's own
impliedVolatility: correlation ≈ 0.997, median gap ≈ 0.003 vol points. (needs pip install yfinance).
Speed — vs vectorized numpy
Same data, same math — a hand-vectorized numpy/scipy implementation vs the plugin on Polars' streaming engine:
import timeit
import numpy as np
import polars as pl
from scipy.special import ndtr
from polars_vol import black_scholes
vol, t, r = 0.25, 1.0, 0.05
sq = vol * t**0.5
n = 20_000_000
df = pl.select(
strike=90 + (pl.int_range(n) % 21).cast(pl.Float64),
spot=(100 + pl.int_range(n) % 40).cast(pl.Float64),
is_call=(pl.int_range(n) % 2 == 0),
)
k, s, ic = df["strike"].to_numpy(), df["spot"].to_numpy(), df["is_call"].to_numpy()
lf = df.lazy().with_columns(
price=black_scholes("strike", "spot", pl.lit(vol), pl.lit(t), pl.lit(r), "is_call")
)
def numpy_price():
d1 = (np.log(s / k) + (r + vol * vol / 2) * t) / sq
d2 = d1 - sq
return np.where(
ic,
s * ndtr(d1) - k * np.exp(-r * t) * ndtr(d2),
k * np.exp(-r * t) * ndtr(-d2) - s * ndtr(-d1),
)
def plugin_price():
return lf.collect(engine="streaming").get_column("price").to_numpy()
t_np = min(timeit.repeat(numpy_price, number=1, repeat=3))
t_pl = min(timeit.repeat(plugin_price, number=1, repeat=3))
diff = (plugin_price() - numpy_price()).__abs__().max()
print(
f"numpy {t_np:.2f}s | polars_vol {t_pl:.2f}s | {t_np / t_pl:.0f}x | max diff {diff:.1e}"
)
# -> numpy 0.46s | polars_vol 0.05s | 8x | max diff 1.5e-05 (the A&S 7.1.26 CDF error)
check examples/speed_vs_numpy.py for the full code.
MIT license.
Metadata
Release files for polars-vol 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| polars_vol-0.1.1.tar.gz | 34.6 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| polars_vol-0.1.1-cp39-abi3-win_amd64.whl | CPython 3.9 | abi3 | Windows x86-64 | Details |
| polars_vol-0.1.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.9 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| polars_vol-0.1.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.9 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| polars_vol-0.1.1-cp39-abi3-macosx_11_0_arm64.whl | CPython 3.9 | abi3 | macOS 11.0+ ARM64 | Details |
| polars_vol-0.1.1-cp39-abi3-macosx_10_12_x86_64.whl | CPython 3.9 | abi3 | macOS 10.12+ x86-64 | Details |
Total release size: 22.4 MB
Release files / polars_vol-0.1.1.tar.gz
| Download URL | polars_vol-0.1.1.tar.gz |
|---|---|
| Size | 34.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bf4861820204e3af9cf0f0fe3f0365dd7035c30f1a21b824c7a92f82ec014a9a
|
|
BLAKE2b-256 checksum How to use checksums |
16dbcb7e0212711d6cd2132f9a4abf0541c85676d46d5f467a6c44b9bfb9bd3e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.15.0
|
Release files / polars_vol-0.1.1-cp39-abi3-win_amd64.whl
| Download URL | polars_vol-0.1.1-cp39-abi3-win_amd64.whl |
|---|---|
| Size | 4.3 MB |
| Tags | CPython 3.9 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
2e1e4af12dfad50df30f1d90ef6a9d6c83112891d5d5b3e1c84f4d6e253b2280
|
|
BLAKE2b-256 checksum How to use checksums |
b380ef8796783b86a0023b5a2ab30213c121910688854a439311d4bff2636f9f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.15.0
|
Release files / polars_vol-0.1.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | polars_vol-0.1.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 4.9 MB |
| Tags | CPython 3.9 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
fa0a2e7746f71f00a0baad56ead89384e5ff33394653d0d181988a49adc38788
|
|
BLAKE2b-256 checksum How to use checksums |
cf3893208017e96f80b534b1f413fd2f93016db86c8cd6dd4d6bd1922a11e520
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.15.0
|
Release files / polars_vol-0.1.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | polars_vol-0.1.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 4.8 MB |
| Tags | CPython 3.9 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
9149f562efe586798cbbc00cbd0a3a52161b03bb9066323921800b9ce1758cb5
|
|
BLAKE2b-256 checksum How to use checksums |
e5acda918137b79d40fa94ef5e0d5c8e69743eeec15271381e27838b4ee3633b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.15.0
|
Release files / polars_vol-0.1.1-cp39-abi3-macosx_11_0_arm64.whl
| Download URL | polars_vol-0.1.1-cp39-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 4.1 MB |
| Tags | CPython 3.9 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
baf2cd2a0323f096b0a725a3c4dbc7ca12748946260d1162c336c5f929dfa096
|
|
BLAKE2b-256 checksum How to use checksums |
06507049bf60f68823e0ede69b5e45d1314425ad16d40267ce3f35d96f62e167
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.15.0
|
Release files / polars_vol-0.1.1-cp39-abi3-macosx_10_12_x86_64.whl
| Download URL | polars_vol-0.1.1-cp39-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 4.2 MB |
| Tags | CPython 3.9 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
1b552ea82519fbe529ca1d6ea0350c95a883bfa0b5d51a28fad7951079e102bc
|
|
BLAKE2b-256 checksum How to use checksums |
d26a3f8a4e89cd50b4a60ec1c245e52ed4a8b0328dd9e818e68e10d49e028402
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.15.0
|