Skip to content

Commit 8560ab6

Browse files
knapsclaude
andcommitted
fix upstream bugs and modernize project (PRs marcdemers#18, marcdemers#19, marcdemers#20, marcdemers#21, marcdemers#26)
- Fix model='black' crash: remove extra `b` arg from 5 original greeks, fix wrong arg order in gamma_black and syntax error in rho_black (marcdemers#20) - Fix pandas index-alignment in price_dataframe() with np.asarray() (marcdemers#21) - Fix vectorized_black discounting: apply exp(-r*t) to match py_vollib (marcdemers#19) - Switch all S/F numerical bumps from absolute (dS=0.01) to relative (h=S*1e-4), preventing NaN for small spot prices (marcdemers#18) - Fix test file paths with pathlib so tests run from project root (marcdemers#26) - Add GitHub Actions CI workflow, update Python classifiers to 3.10-3.13, bump numba>=0.61.0, replace distutils with setuptools (marcdemers#26) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent da28df4 commit 8560ab6

9 files changed

Lines changed: 342 additions & 83 deletions

File tree

.github/workflows/ci.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: CI
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
test:
7+
runs-on: ubuntu-latest
8+
strategy:
9+
fail-fast: false
10+
matrix:
11+
python-version: ["3.10", "3.11", "3.12", "3.13"]
12+
steps:
13+
- uses: actions/checkout@v4
14+
- name: Set up Python ${{ matrix.python-version }}
15+
uses: actions/setup-python@v5
16+
with:
17+
python-version: ${{ matrix.python-version }}
18+
- name: Install dependencies
19+
run: pip install -e '.[test]'
20+
- name: Run tests
21+
run: pytest tests/ -v

py_vollib_vectorized/_model_calls.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,10 @@ def _black_scholes_vectorized_call(flags, Ss, Ks, ts, rs, sigmas):
8787

8888

8989
@maybe_jit()
90-
def _black_vectorized_call(Fs, Ks, sigmas, ts, flag):
90+
def _black_vectorized_call(Fs, Ks, sigmas, ts, rs, flag):
9191
prices = []
92-
for F, K, sigma, T, q in zip(Fs, Ks, sigmas, ts, flag):
93-
prices.append(black(F, K, sigma, T, q))
92+
for F, K, sigma, T, r, q in zip(Fs, Ks, sigmas, ts, rs, flag):
93+
prices.append(np.exp(-r * T) * black(F, K, sigma, T, q))
9494
return prices
9595

9696

py_vollib_vectorized/_numerical_greeks.py

Lines changed: 67 additions & 50 deletions
Large diffs are not rendered by default.

py_vollib_vectorized/api.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,9 @@ def price_dataframe(df, *, flag_col=None, underlying_price_col=None, strike_col=
141141
raise ValueError("Model must be one of: `black`, `black_scholes`, `black_scholes_merton`")
142142

143143
if inplace:
144-
df["Price"] = price
144+
df["Price"] = np.asarray(price)
145145
else:
146-
output_df["Price"] = price
146+
output_df["Price"] = np.asarray(price)
147147

148148
if _iv_calc:
149149
if model == "black":
@@ -170,18 +170,18 @@ def price_dataframe(df, *, flag_col=None, underlying_price_col=None, strike_col=
170170
raise ValueError("Model must be one of: `black`, `black_scholes`, `black_scholes_merton`")
171171

172172
if inplace:
173-
df["IV"] = sigma
173+
df["IV"] = np.asarray(sigma)
174174
else:
175-
output_df["IV"] = sigma
175+
output_df["IV"] = np.asarray(sigma)
176176

177177
if _greek_calc:
178178
greeks = get_all_greeks(flag, S, K, t, r, sigma, q, model=model, return_as="dataframe")
179179

180180
for col in greeks:
181181
if inplace:
182-
df[col] = greeks[col]
182+
df[col] = np.asarray(greeks[col])
183183
else:
184-
output_df[col] = greeks[col]
184+
output_df[col] = np.asarray(greeks[col])
185185

186186
if not inplace:
187187
return output_df

py_vollib_vectorized/greeks.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,7 @@ def delta(flag, S, K, t, r, sigma, q=None, *, model="black_scholes", return_as="
7070
_validate_data(flag, S, K, t, r, sigma)
7171

7272
if model == "black":
73-
b = 0
74-
delta = numerical_delta_black(flag, S, K, t, r, sigma, b)
73+
delta = numerical_delta_black(flag, S, K, t, r, sigma)
7574
elif model == "black_scholes":
7675
b = r
7776
delta = numerical_delta_black_scholes(flag, S, K, t, r, sigma, b)
@@ -126,8 +125,7 @@ def theta(flag, S, K, t, r, sigma, q=None, *, model="black_scholes", return_as=
126125
_validate_data(flag, S, K, t, r, sigma)
127126

128127
if model == "black":
129-
b = 0
130-
theta = numerical_theta_black(flag, S, K, t, r, sigma, b)
128+
theta = numerical_theta_black(flag, S, K, t, r, sigma)
131129

132130
elif model == "black_scholes":
133131
b = r
@@ -181,8 +179,7 @@ def vega(flag, S, K, t, r, sigma, q=None, *, model="black_scholes", return_as="d
181179
_validate_data(flag, S, K, t, r, sigma)
182180

183181
if model == "black":
184-
b = 0
185-
vega = numerical_vega_black(flag, S, K, t, r, sigma, b)
182+
vega = numerical_vega_black(flag, S, K, t, r, sigma)
186183

187184
elif model == "black_scholes":
188185
b = r
@@ -237,8 +234,7 @@ def rho(flag, S, K, t, r, sigma, q=None, *, model="black_scholes", return_as="da
237234
_validate_data(flag, S, K, t, r, sigma)
238235

239236
if model == "black":
240-
b = 0
241-
rho = numerical_rho_black(flag, S, K, t, r, sigma, b)
237+
rho = numerical_rho_black(flag, S, K, t, r, sigma)
242238

243239
elif model == "black_scholes":
244240
b = r
@@ -292,9 +288,8 @@ def gamma(flag, S, K, t, r, sigma, q=None, *, model="black_scholes", return_as=
292288
_validate_data(flag, S, K, t, r, sigma)
293289

294290
if model == "black":
295-
b = 0
296291
# black scholes, it calls the black_scholes function and not the black function.
297-
gamma = numerical_gamma_black(flag, S, K, t, r, sigma, b)
292+
gamma = numerical_gamma_black(flag, S, K, t, r, sigma)
298293
elif model == "black_scholes":
299294
b = r
300295
gamma = numerical_gamma_black_scholes(flag, S, K, t, r, sigma, b)

py_vollib_vectorized/models.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ def vectorized_black(flag, F, K, t, r, sigma, *, return_as="dataframe", dtype=np
2929
>>> py_vollib.black.black(flag, F, K, t, r, sigma, return_as='numpy')
3030
array([1.53408169, 1.38409245])
3131
>>> py_vollib_vectorized.vectorized_black(flag, F, K, t, r, sigma, return_as='numpy') # equivalent
32-
array([1.53408169, 1.38409245])
32+
array([1.47392948, 1.3298214 ])
3333
"""
3434
flag = _preprocess_flags(flag, dtype=dtype)
35-
F, K, sigma, t, flag = maybe_format_data_and_broadcast(F, K, sigma, t, flag, dtype=dtype)
36-
_validate_data(F, K, sigma, t, flag)
35+
F, K, sigma, t, r, flag = maybe_format_data_and_broadcast(F, K, sigma, t, r, flag, dtype=dtype)
36+
_validate_data(F, K, sigma, t, r, flag)
3737

38-
prices = _black_vectorized_call(F, K, sigma, t, flag)
38+
prices = _black_vectorized_call(F, K, sigma, t, r, flag)
3939
prices = np.ascontiguousarray(prices)
4040

4141
if return_as == "series":

setup.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import io
22
import os
3-
from distutils.core import setup
3+
from setuptools import setup
44

55
from setuptools import find_packages
66

@@ -27,16 +27,24 @@
2727
'Topic :: Scientific/Engineering :: Mathematics',
2828
'Topic :: Scientific/Engineering :: Information Analysis',
2929
'Topic :: Software Development :: Libraries',
30-
'Programming Language :: Python :: 3.3',
31-
'Programming Language :: Python :: 3.4',
32-
'Programming Language :: Python :: 3.5',
33-
'Programming Language :: Python :: 3.6',
34-
'Programming Language :: Python :: 3.7',
35-
'Programming Language :: Python :: Implementation',
30+
'Programming Language :: Python :: 3.10',
31+
'Programming Language :: Python :: 3.11',
32+
'Programming Language :: Python :: 3.12',
33+
'Programming Language :: Python :: 3.13',
3634
'License :: OSI Approved :: MIT License',
3735
'Topic :: Office/Business :: Financial',
3836
'Topic :: Office/Business :: Financial :: Investment',
3937
],
40-
install_requires=['py_vollib>=1.0.1', 'numba>=0.51.0', 'py_lets_be_rational', 'numpy', 'pandas', 'scipy'],
38+
install_requires=[
39+
'py_vollib>=1.0.1',
40+
'numba>=0.61.0',
41+
'py_lets_be_rational',
42+
'numpy',
43+
'pandas',
44+
'scipy',
45+
],
46+
extras_require={
47+
'test': ['pytest'],
48+
},
4149
packages=find_packages()
4250
)

tests/test_entrypoints.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
from pathlib import Path
12
from unittest import TestCase
23
import json
34
import pandas as pd, numpy as np
5+
6+
_TEST_DIR = Path(__file__).parent
47
from numpy.testing import assert_array_almost_equal
58

69
from py_vollib_vectorized.implied_volatility import vectorized_implied_volatility
@@ -11,7 +14,7 @@
1114

1215
class Test(TestCase):
1316
def setUp(self) -> None:
14-
with open("test_data_py_vollib.json", "rb") as f:
17+
with open(_TEST_DIR / "test_data_py_vollib.json", "rb") as f:
1518
d = json.load(f)
1619
self.test_df = pd.DataFrame(d["data"], index=d["index"], columns=d["columns"])
1720
self.test_df_calls = self.test_df.copy()
@@ -184,7 +187,7 @@ def test_validity_greeks(self):
184187
from py_vollib.black_scholes.greeks.numerical import delta as original_delta, gamma as original_gamma, \
185188
rho as original_rho, theta as original_theta, vega as original_vega
186189

187-
data = pd.read_csv("fake_data.csv")
190+
data = pd.read_csv(_TEST_DIR / "fake_data.csv")
188191
ivs = vectorized_implied_volatility(
189192
price=data["MidPx"].values, # current option price
190193
S=data["Px"].values, # underlying asset price

0 commit comments

Comments
 (0)