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.0
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.0.tar.gz | 34.6 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| polars_vol-0.1.0-cp39-abi3-win_amd64.whl | CPython 3.9 | abi3 | Windows x86-64 | Details |
| polars_vol-0.1.0-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.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.9 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| polars_vol-0.1.0-cp39-abi3-macosx_11_0_arm64.whl | CPython 3.9 | abi3 | macOS 11.0+ ARM64 | Details |
| polars_vol-0.1.0-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.0.tar.gz
| Download URL | polars_vol-0.1.0.tar.gz |
|---|---|
| Size | 34.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
17f5a2f8380db2de7aebfee143c7b49f10a21f6db11081d008788658bac18e48
|
|
BLAKE2b-256 checksum How to use checksums |
e9613e797645d9f89513a262cf2dc68918f5e7402151f858d9a1240dd2673ad1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.15.0
|
Release files / polars_vol-0.1.0-cp39-abi3-win_amd64.whl
| Download URL | polars_vol-0.1.0-cp39-abi3-win_amd64.whl |
|---|---|
| Size | 4.3 MB |
| Tags | CPython 3.9 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
cbaed002c32e108b20459a560bf8799a3c8f2f3bf04227780014d0642b1da067
|
|
BLAKE2b-256 checksum How to use checksums |
8ca7f9a0d7041e9907a1868f5ff23b105199a8a3cd459012071318ebae5b9833
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.15.0
|
Release files / polars_vol-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | polars_vol-0.1.0-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 |
fc418ff8a27d602bbaa1b6c0db1abef450557b65ef061cfab227996ba5306edc
|
|
BLAKE2b-256 checksum How to use checksums |
3523a567376df186c8a4cd0e7edaba39ef369124813bb8e17d9617c1f4f91f0d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.15.0
|
Release files / polars_vol-0.1.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | polars_vol-0.1.0-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 |
aac060eaac47b4acfcc90a9c30e5189ba839ea1f987ad773466832e03b7f3006
|
|
BLAKE2b-256 checksum How to use checksums |
8939fbd0c16d60bb232da3325dd9821446484b02cdcb176b3cf096e056c2e0b5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.15.0
|
Release files / polars_vol-0.1.0-cp39-abi3-macosx_11_0_arm64.whl
| Download URL | polars_vol-0.1.0-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 |
6cbae126806657cc6a91d439239fd6a46e9a091fd13e029431a6d9b47f451742
|
|
BLAKE2b-256 checksum How to use checksums |
0970abc228a733043e75c68aa27c309dcd80b86994c430cceb848062a5de1aa0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.15.0
|
Release files / polars_vol-0.1.0-cp39-abi3-macosx_10_12_x86_64.whl
| Download URL | polars_vol-0.1.0-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 |
472d587f3b9bf955160b5eea7f4c0bb620e3c88648a753c5f78dbfa53080321d
|
|
BLAKE2b-256 checksum How to use checksums |
ed7d269cb4993fbe3f84dfcb43ea456f12c419939a894bf701d33689b1884196
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.15.0
|