voly 0.0.8__tar.gz → 0.0.10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {voly-0.0.8/src/voly.egg-info → voly-0.0.10}/PKG-INFO +1 -2
- {voly-0.0.8 → voly-0.0.10}/pyproject.toml +2 -7
- {voly-0.0.8 → voly-0.0.10}/setup.py +1 -2
- {voly-0.0.8 → voly-0.0.10}/src/voly/client.py +5 -5
- {voly-0.0.8 → voly-0.0.10}/src/voly/core/data.py +0 -25
- {voly-0.0.8 → voly-0.0.10}/src/voly/utils/logger.py +0 -4
- {voly-0.0.8 → voly-0.0.10/src/voly.egg-info}/PKG-INFO +1 -2
- {voly-0.0.8 → voly-0.0.10}/src/voly.egg-info/requires.txt +0 -1
- voly-0.0.10/tests/test_client.py +244 -0
- voly-0.0.8/tests/test_client.py +0 -52
- {voly-0.0.8 → voly-0.0.10}/LICENSE +0 -0
- {voly-0.0.8 → voly-0.0.10}/README.md +0 -0
- {voly-0.0.8 → voly-0.0.10}/setup.cfg +0 -0
- {voly-0.0.8 → voly-0.0.10}/src/voly/__init__.py +0 -0
- {voly-0.0.8 → voly-0.0.10}/src/voly/core/__init__.py +0 -0
- {voly-0.0.8 → voly-0.0.10}/src/voly/core/charts.py +0 -0
- {voly-0.0.8 → voly-0.0.10}/src/voly/core/fit.py +0 -0
- {voly-0.0.8 → voly-0.0.10}/src/voly/core/interpolate.py +0 -0
- {voly-0.0.8 → voly-0.0.10}/src/voly/core/rnd.py +0 -0
- {voly-0.0.8 → voly-0.0.10}/src/voly/exceptions.py +0 -0
- {voly-0.0.8 → voly-0.0.10}/src/voly/formulas.py +0 -0
- {voly-0.0.8 → voly-0.0.10}/src/voly/models.py +0 -0
- {voly-0.0.8 → voly-0.0.10}/src/voly/utils/__init__.py +0 -0
- {voly-0.0.8 → voly-0.0.10}/src/voly.egg-info/SOURCES.txt +0 -0
- {voly-0.0.8 → voly-0.0.10}/src/voly.egg-info/dependency_links.txt +0 -0
- {voly-0.0.8 → voly-0.0.10}/src/voly.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: voly
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.10
|
|
4
4
|
Summary: Options & volatility research package
|
|
5
5
|
Author-email: Manu de Cara <manu.de.cara@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -27,7 +27,6 @@ Requires-Dist: websockets>=10.0
|
|
|
27
27
|
Requires-Dist: requests>=2.26.0
|
|
28
28
|
Requires-Dist: loguru>=0.5.3
|
|
29
29
|
Provides-Extra: dev
|
|
30
|
-
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
31
30
|
Requires-Dist: black>=22.1.0; extra == "dev"
|
|
32
31
|
Requires-Dist: isort>=5.10.1; extra == "dev"
|
|
33
32
|
Requires-Dist: mypy>=0.931; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "voly"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.10"
|
|
8
8
|
description = "Options & volatility research package"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -38,7 +38,6 @@ dependencies = [
|
|
|
38
38
|
|
|
39
39
|
[project.optional-dependencies]
|
|
40
40
|
dev = [
|
|
41
|
-
"pytest>=7.0.0",
|
|
42
41
|
"black>=22.1.0",
|
|
43
42
|
"isort>=5.10.1",
|
|
44
43
|
"mypy>=0.931",
|
|
@@ -61,12 +60,8 @@ line_length = 100
|
|
|
61
60
|
multi_line_output = 3
|
|
62
61
|
|
|
63
62
|
[tool.mypy]
|
|
64
|
-
python_version = "0.0.
|
|
63
|
+
python_version = "0.0.10"
|
|
65
64
|
warn_return_any = true
|
|
66
65
|
warn_unused_configs = true
|
|
67
66
|
disallow_untyped_defs = true
|
|
68
67
|
disallow_incomplete_defs = true
|
|
69
|
-
|
|
70
|
-
[tool.pytest.ini_options]
|
|
71
|
-
testpaths = ["tests"]
|
|
72
|
-
python_files = "test_*.py"
|
|
@@ -286,7 +286,7 @@ class VolyClient:
|
|
|
286
286
|
Returns:
|
|
287
287
|
- Implied volatility
|
|
288
288
|
"""
|
|
289
|
-
return iv(option_price, s, k, r,
|
|
289
|
+
return iv(option_price, s, k, r, t, option_type)
|
|
290
290
|
|
|
291
291
|
# -------------------------------------------------------------------------
|
|
292
292
|
# Model Fitting
|
|
@@ -294,7 +294,7 @@ class VolyClient:
|
|
|
294
294
|
|
|
295
295
|
@staticmethod
|
|
296
296
|
def fit_model(market_data: pd.DataFrame,
|
|
297
|
-
|
|
297
|
+
model_name: str = 'svi',
|
|
298
298
|
moneyness_range: Tuple[float, float] = (-2, 2),
|
|
299
299
|
num_points: int = 500,
|
|
300
300
|
plot: bool = False) -> Dict[str, Any]:
|
|
@@ -303,7 +303,7 @@ class VolyClient:
|
|
|
303
303
|
|
|
304
304
|
Parameters:
|
|
305
305
|
- market_data: DataFrame with market data
|
|
306
|
-
-
|
|
306
|
+
- model_name: Name of model to fit (default: 'svi')
|
|
307
307
|
- moneyness_range: (min, max) range for moneyness grid
|
|
308
308
|
- num_points: Number of points for moneyness grid
|
|
309
309
|
- plot: Whether to generate and return plots
|
|
@@ -311,12 +311,12 @@ class VolyClient:
|
|
|
311
311
|
Returns:
|
|
312
312
|
- Dictionary with fitting results and optional plots
|
|
313
313
|
"""
|
|
314
|
-
logger.info(f"Fitting {
|
|
314
|
+
logger.info(f"Fitting {model_name.upper()} model to market data")
|
|
315
315
|
|
|
316
316
|
# Fit the model
|
|
317
317
|
fit_results = fit_model(
|
|
318
318
|
market_data=market_data,
|
|
319
|
-
|
|
319
|
+
model_name=model_name,
|
|
320
320
|
moneyness_range=moneyness_range,
|
|
321
321
|
num_points=num_points
|
|
322
322
|
)
|
|
@@ -45,8 +45,6 @@ async def unsubscribe_channels(ws, channels):
|
|
|
45
45
|
@catch_exception
|
|
46
46
|
async def process_batch(ws, batch: List[str], batch_num: int, total_batches: int) -> List[Dict[str, Any]]:
|
|
47
47
|
"""Process a batch of instruments and return their data"""
|
|
48
|
-
batch_start = time.time()
|
|
49
|
-
|
|
50
48
|
# Create channel subscriptions
|
|
51
49
|
ticker_channels = [f"ticker.{instr}.100ms" for instr in batch]
|
|
52
50
|
book_channels = [f"book.{instr}.100ms" for instr in batch]
|
|
@@ -123,10 +121,6 @@ async def process_batch(ws, batch: List[str], batch_num: int, total_batches: int
|
|
|
123
121
|
|
|
124
122
|
batch_results.append(row)
|
|
125
123
|
|
|
126
|
-
batch_time = time.time() - batch_start
|
|
127
|
-
logger.info(
|
|
128
|
-
f"Batch {batch_num}/{total_batches} completed in {batch_time:.2f}s - {len(batch_results)} instruments processed")
|
|
129
|
-
|
|
130
124
|
return batch_results
|
|
131
125
|
|
|
132
126
|
|
|
@@ -227,7 +221,6 @@ def process_option_chain(df: pd.DataFrame, currency: str, min_dte: float = 2.0)
|
|
|
227
221
|
return None
|
|
228
222
|
|
|
229
223
|
# Apply extraction to create new columns
|
|
230
|
-
logger.info(f"Extracting option details from instrument names...")
|
|
231
224
|
df['details'] = df['instrument_name'].apply(lambda x: extract_details(x))
|
|
232
225
|
df['strike'] = df['details'].apply(lambda x: x['strike'] if x else None)
|
|
233
226
|
df['option_type'] = df['details'].apply(lambda x: x['option_type'] if x else None)
|
|
@@ -240,7 +233,6 @@ def process_option_chain(df: pd.DataFrame, currency: str, min_dte: float = 2.0)
|
|
|
240
233
|
|
|
241
234
|
# Get reference time from timestamp
|
|
242
235
|
reference_time = datetime.datetime.fromtimestamp(df['timestamp'].iloc[0] / 1000)
|
|
243
|
-
logger.info(f"Reference time: {reference_time}")
|
|
244
236
|
|
|
245
237
|
# Calculate days to expiry (DTE)
|
|
246
238
|
df['dte'] = (df['expiry_date'] - reference_time).dt.total_seconds() / (24 * 60 * 60)
|
|
@@ -261,23 +253,6 @@ def process_option_chain(df: pd.DataFrame, currency: str, min_dte: float = 2.0)
|
|
|
261
253
|
df = df.dropna(subset=['mark_iv', 'log_moneyness', 'yte'])
|
|
262
254
|
logger.info(f"Removed {original_rows - len(df)} rows with missing data")
|
|
263
255
|
|
|
264
|
-
# Filter options with DTE > min_dte
|
|
265
|
-
if min_dte > 0:
|
|
266
|
-
original_count = len(df)
|
|
267
|
-
df = df[df['dte'] > min_dte]
|
|
268
|
-
logger.info(f"Filtered out {original_count - len(df)} options with DTE < {min_dte}")
|
|
269
|
-
|
|
270
|
-
# Group by time to expiry and ensure we have enough data points for each expiry
|
|
271
|
-
expiry_counts = df.groupby('yte').size()
|
|
272
|
-
valid_expiries = expiry_counts[expiry_counts >= 5].index
|
|
273
|
-
df = df[df['yte'].isin(valid_expiries)]
|
|
274
|
-
logger.info(f"Filtered to {len(df)} options with at least 5 strikes per expiry")
|
|
275
|
-
|
|
276
|
-
# Report on the maturities we're working with
|
|
277
|
-
maturities = df.groupby(['maturity_name', 'yte']).size().reset_index()
|
|
278
|
-
for _, row in maturities.iterrows():
|
|
279
|
-
logger.info(f"Maturity: {row['maturity_name']}, YTE: {row['yte']:.4f}, Strikes: {row[0]}")
|
|
280
|
-
|
|
281
256
|
return df
|
|
282
257
|
|
|
283
258
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: voly
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.10
|
|
4
4
|
Summary: Options & volatility research package
|
|
5
5
|
Author-email: Manu de Cara <manu.de.cara@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -27,7 +27,6 @@ Requires-Dist: websockets>=10.0
|
|
|
27
27
|
Requires-Dist: requests>=2.26.0
|
|
28
28
|
Requires-Dist: loguru>=0.5.3
|
|
29
29
|
Provides-Extra: dev
|
|
30
|
-
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
31
30
|
Requires-Dist: black>=22.1.0; extra == "dev"
|
|
32
31
|
Requires-Dist: isort>=5.10.1; extra == "dev"
|
|
33
32
|
Requires-Dist: mypy>=0.931; extra == "dev"
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comprehensive tests for the VolyClient class with detailed output.
|
|
3
|
+
This file demonstrates expected values and provides informative output.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import unittest
|
|
7
|
+
import numpy as np
|
|
8
|
+
from voly import VolyClient
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class VolyClientTestCase(unittest.TestCase):
|
|
13
|
+
"""
|
|
14
|
+
Test cases for the VolyClient class with expected values and detailed output.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def setUp(self):
|
|
18
|
+
"""Set up test fixtures."""
|
|
19
|
+
self.voly = VolyClient()
|
|
20
|
+
# Add a divider line before each test for clearer output
|
|
21
|
+
print("\n" + "=" * 80)
|
|
22
|
+
|
|
23
|
+
def test_bs_pricing_with_expected_values(self):
|
|
24
|
+
"""Test Black-Scholes pricing with expected values."""
|
|
25
|
+
print("\nTEST: Black-Scholes Pricing")
|
|
26
|
+
|
|
27
|
+
# Test parameters
|
|
28
|
+
s = 100.0
|
|
29
|
+
k = 100.0
|
|
30
|
+
r = 0.05
|
|
31
|
+
vol = 0.2
|
|
32
|
+
t = 1.0
|
|
33
|
+
|
|
34
|
+
# Expected values (pre-calculated)
|
|
35
|
+
expected_call = 10.45
|
|
36
|
+
expected_put = 5.57
|
|
37
|
+
|
|
38
|
+
# Calculate actual values
|
|
39
|
+
actual_call = self.voly.bs(s=s, k=k, r=r, vol=vol, t=t, option_type='call')
|
|
40
|
+
actual_put = self.voly.bs(s=s, k=k, r=r, vol=vol, t=t, option_type='put')
|
|
41
|
+
|
|
42
|
+
# Print actual vs expected
|
|
43
|
+
print(f"Parameters: S={s}, K={k}, r={r}, vol={vol}, t={t}")
|
|
44
|
+
print(
|
|
45
|
+
f"Call Price: Actual={actual_call:.4f}, Expected={expected_call:.4f}, Diff={abs(actual_call - expected_call):.6f}")
|
|
46
|
+
print(
|
|
47
|
+
f"Put Price: Actual={actual_put:.4f}, Expected={expected_put:.4f}, Diff={abs(actual_put - expected_put):.6f}")
|
|
48
|
+
|
|
49
|
+
# Check put-call parity
|
|
50
|
+
pcp_diff = actual_call - actual_put - s + k * np.exp(-r * t)
|
|
51
|
+
print(f"Put-Call Parity Check: {pcp_diff:.8f} (should be close to 0)")
|
|
52
|
+
|
|
53
|
+
# Assertions with tolerance
|
|
54
|
+
self.assertAlmostEqual(actual_call, expected_call, delta=0.01,
|
|
55
|
+
msg=f"Call price {actual_call:.4f} doesn't match expected {expected_call:.4f}")
|
|
56
|
+
self.assertAlmostEqual(actual_put, expected_put, delta=0.01,
|
|
57
|
+
msg=f"Put price {actual_put:.4f} doesn't match expected {expected_put:.4f}")
|
|
58
|
+
self.assertAlmostEqual(pcp_diff, 0, delta=1e-10,
|
|
59
|
+
msg="Put-call parity violated")
|
|
60
|
+
|
|
61
|
+
def test_delta_values_across_moneyness(self):
|
|
62
|
+
"""Test delta values across different moneyness levels."""
|
|
63
|
+
print("\nTEST: Delta Values Across Moneyness")
|
|
64
|
+
|
|
65
|
+
# Test parameters
|
|
66
|
+
s = 100.0
|
|
67
|
+
r = 0.05
|
|
68
|
+
vol = 0.2
|
|
69
|
+
t = 1.0
|
|
70
|
+
|
|
71
|
+
# Define test cases: strike, expected call delta, expected put delta
|
|
72
|
+
test_cases = [
|
|
73
|
+
(50.0, 0.9999, -0.0001), # Deep ITM call / Deep OTM put
|
|
74
|
+
(75.0, 0.9631, -0.0369), # ITM call / OTM put
|
|
75
|
+
(100.0, 0.6368, -0.3632), # ATM
|
|
76
|
+
(125.0, 0.2219, -0.7781), # OTM call / ITM put
|
|
77
|
+
(150.0, 0.0467, -0.9533) # Deep OTM call / Deep ITM put
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
print(f"Parameters: S={s}, r={r}, vol={vol}, t={t}")
|
|
81
|
+
print("\nDelta Values:")
|
|
82
|
+
print(f"{'Strike':<10} {'Call Delta':<15} {'Expected':<15} {'Put Delta':<15} {'Expected':<15}")
|
|
83
|
+
print("-" * 70)
|
|
84
|
+
|
|
85
|
+
for strike, exp_call_delta, exp_put_delta in test_cases:
|
|
86
|
+
call_delta = self.voly.delta(s=s, k=strike, r=r, vol=vol, t=t, option_type='call')
|
|
87
|
+
put_delta = self.voly.delta(s=s, k=strike, r=r, vol=vol, t=t, option_type='put')
|
|
88
|
+
|
|
89
|
+
print(f"{strike:<10.1f} {call_delta:<15.4f} {exp_call_delta:<15.4f} "
|
|
90
|
+
f"{put_delta:<15.4f} {exp_put_delta:<15.4f}")
|
|
91
|
+
|
|
92
|
+
# Check deltas are within expected range
|
|
93
|
+
self.assertAlmostEqual(call_delta, exp_call_delta, delta=0.01,
|
|
94
|
+
msg=f"Call delta for K={strike} incorrect")
|
|
95
|
+
self.assertAlmostEqual(put_delta, exp_put_delta, delta=0.01,
|
|
96
|
+
msg=f"Put delta for K={strike} incorrect")
|
|
97
|
+
|
|
98
|
+
# Check put-call delta relationship: call_delta - put_delta = 1
|
|
99
|
+
self.assertAlmostEqual(call_delta - put_delta, 1.0, delta=1e-10,
|
|
100
|
+
msg="Delta relationship violated")
|
|
101
|
+
|
|
102
|
+
def test_all_greeks_values(self):
|
|
103
|
+
"""Test all Greeks calculation with expected values."""
|
|
104
|
+
print("\nTEST: All Greeks Values")
|
|
105
|
+
|
|
106
|
+
# Test parameters
|
|
107
|
+
s = 100.0
|
|
108
|
+
k = 100.0
|
|
109
|
+
r = 0.05
|
|
110
|
+
vol = 0.2
|
|
111
|
+
t = 1.0
|
|
112
|
+
|
|
113
|
+
# Calculate all Greeks for a call
|
|
114
|
+
call_greeks = self.voly.greeks(s=s, k=k, r=r, vol=vol, t=t, option_type='call')
|
|
115
|
+
|
|
116
|
+
# Expected values (calculated using standard Black-Scholes formulas)
|
|
117
|
+
expected_greeks = {
|
|
118
|
+
'price': 10.45,
|
|
119
|
+
'delta': 0.637,
|
|
120
|
+
'gamma': 0.019,
|
|
121
|
+
'vega': 0.375,
|
|
122
|
+
'theta': -0.018,
|
|
123
|
+
'rho': 0.52,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
print(f"Parameters: S={s}, K={k}, r={r}, vol={vol}, t={t}")
|
|
127
|
+
print("\nCall Option Greeks:")
|
|
128
|
+
print(f"{'Greek':<10} {'Actual':<15} {'Expected':<15} {'Diff':<15}")
|
|
129
|
+
print("-" * 55)
|
|
130
|
+
|
|
131
|
+
for greek, expected in expected_greeks.items():
|
|
132
|
+
actual = call_greeks[greek]
|
|
133
|
+
print(f"{greek.capitalize():<10} {actual:<15.6f} {expected:<15.6f} {abs(actual - expected):<15.6f}")
|
|
134
|
+
|
|
135
|
+
# Assert with appropriate tolerance
|
|
136
|
+
self.assertAlmostEqual(actual, expected, delta=max(0.01, expected * 0.05),
|
|
137
|
+
msg=f"{greek.capitalize()} value incorrect")
|
|
138
|
+
|
|
139
|
+
# Additional checks for other Greeks
|
|
140
|
+
print("\nAdditional Greeks:")
|
|
141
|
+
for greek in ['vanna', 'volga', 'charm']:
|
|
142
|
+
if greek in call_greeks:
|
|
143
|
+
print(f"{greek.capitalize():<10} {call_greeks[greek]:<15.6f}")
|
|
144
|
+
|
|
145
|
+
# Check basic relationships
|
|
146
|
+
# Gamma should be positive for both calls and puts
|
|
147
|
+
self.assertGreater(call_greeks['gamma'], 0, "Gamma should be positive")
|
|
148
|
+
|
|
149
|
+
# Vega should be positive for both calls and puts
|
|
150
|
+
self.assertGreater(call_greeks['vega'], 0, "Vega should be positive")
|
|
151
|
+
|
|
152
|
+
# Theta is typically negative for calls and puts (time decay)
|
|
153
|
+
self.assertLess(call_greeks['theta'], 0, "Theta should be negative for calls")
|
|
154
|
+
|
|
155
|
+
def test_implied_volatility_calculation(self):
|
|
156
|
+
"""Test implied volatility calculation with known prices."""
|
|
157
|
+
print("\nTEST: Implied Volatility Calculation")
|
|
158
|
+
|
|
159
|
+
# Define test cases
|
|
160
|
+
test_cases = [
|
|
161
|
+
# S, K, r, vol, t
|
|
162
|
+
(100.0, 100.0, 0.05, 0.2, 1.0), # ATM
|
|
163
|
+
(100.0, 90.0, 0.05, 0.25, 0.5), # ITM
|
|
164
|
+
(100.0, 110.0, 0.05, 0.3, 0.25) # OTM
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
print(f"{'S':<8} {'K':<8} {'r':<8} {'t':<8} {'Input Vol':<12} {'Option Price':<15} "
|
|
168
|
+
f"{'Implied Vol':<15} {'Diff':<10}")
|
|
169
|
+
print("-" * 90)
|
|
170
|
+
|
|
171
|
+
for s, k, r, vol, t in test_cases:
|
|
172
|
+
# Calculate option price with known volatility
|
|
173
|
+
call_price = self.voly.bs(s=s, k=k, r=r, vol=vol, t=t, option_type='call')
|
|
174
|
+
|
|
175
|
+
# Calculate implied volatility from the price
|
|
176
|
+
try:
|
|
177
|
+
implied_vol = self.voly.iv(option_price=call_price, s=s, k=k, r=r, t=t, option_type='call')
|
|
178
|
+
vol_diff = abs(vol - implied_vol)
|
|
179
|
+
print(f"{s:<8.1f} {k:<8.1f} {r:<8.3f} {t:<8.2f} {vol:<12.4f} {call_price:<15.6f} "
|
|
180
|
+
f"{implied_vol:<15.6f} {vol_diff:<10.6f}")
|
|
181
|
+
|
|
182
|
+
# Assert implied vol matches input vol
|
|
183
|
+
self.assertAlmostEqual(vol, implied_vol, delta=0.0001,
|
|
184
|
+
msg=f"Implied volatility {implied_vol:.6f} doesn't match input {vol:.6f}")
|
|
185
|
+
except Exception as e:
|
|
186
|
+
print(f"{s:<8.1f} {k:<8.1f} {r:<8.3f} {t:<8.2f} {vol:<12.4f} {call_price:<15.6f} "
|
|
187
|
+
f"ERROR: {str(e)}")
|
|
188
|
+
self.fail(f"Implied volatility calculation failed: {str(e)}")
|
|
189
|
+
|
|
190
|
+
def test_bs_pricing_extreme_cases(self):
|
|
191
|
+
"""Test Black-Scholes pricing under extreme conditions."""
|
|
192
|
+
print("\nTEST: Black-Scholes Pricing - Extreme Cases")
|
|
193
|
+
|
|
194
|
+
# Test zero volatility
|
|
195
|
+
zero_vol_call = self.voly.bs(s=100, k=90, r=0.05, vol=0, t=1, option_type='call')
|
|
196
|
+
zero_vol_put = self.voly.bs(s=100, k=110, r=0.05, vol=0, t=1, option_type='put')
|
|
197
|
+
|
|
198
|
+
print("Zero Volatility:")
|
|
199
|
+
print(f"Call (S=100, K=90): {zero_vol_call:.4f} (should equal intrinsic value 10)")
|
|
200
|
+
print(f"Put (S=100, K=110): {zero_vol_put:.4f} (should equal intrinsic value 10)")
|
|
201
|
+
|
|
202
|
+
self.assertAlmostEqual(zero_vol_call, 10.0, delta=0.01,
|
|
203
|
+
msg="Zero vol ITM call should equal intrinsic value")
|
|
204
|
+
self.assertAlmostEqual(zero_vol_put, 10.0, delta=0.01,
|
|
205
|
+
msg="Zero vol ITM put should equal intrinsic value")
|
|
206
|
+
|
|
207
|
+
# Test zero time to expiry
|
|
208
|
+
zero_time_call = self.voly.bs(s=100, k=90, r=0.05, vol=0.2, t=0, option_type='call')
|
|
209
|
+
zero_time_put = self.voly.bs(s=100, k=110, r=0.05, vol=0.2, t=0, option_type='put')
|
|
210
|
+
|
|
211
|
+
print("\nZero Time to Expiry:")
|
|
212
|
+
print(f"Call (S=100, K=90): {zero_time_call:.4f} (should equal intrinsic value 10)")
|
|
213
|
+
print(f"Put (S=100, K=110): {zero_time_put:.4f} (should equal intrinsic value 10)")
|
|
214
|
+
|
|
215
|
+
self.assertAlmostEqual(zero_time_call, 10.0, delta=0.01,
|
|
216
|
+
msg="Zero time ITM call should equal intrinsic value")
|
|
217
|
+
self.assertAlmostEqual(zero_time_put, 10.0, delta=0.01,
|
|
218
|
+
msg="Zero time ITM put should equal intrinsic value")
|
|
219
|
+
|
|
220
|
+
# Test deep ITM and OTM
|
|
221
|
+
deep_itm_call = self.voly.bs(s=100, k=50, r=0.05, vol=0.2, t=1, option_type='call')
|
|
222
|
+
deep_otm_call = self.voly.bs(s=100, k=200, r=0.05, vol=0.2, t=1, option_type='call')
|
|
223
|
+
|
|
224
|
+
print("\nDeep ITM/OTM:")
|
|
225
|
+
print(f"Deep ITM Call (S=100, K=50): {deep_itm_call:.4f}")
|
|
226
|
+
print(f"Deep OTM Call (S=100, K=200): {deep_otm_call:.4f}")
|
|
227
|
+
|
|
228
|
+
self.assertGreater(deep_itm_call, 50.0,
|
|
229
|
+
msg="Deep ITM call should be greater than intrinsic value")
|
|
230
|
+
self.assertGreater(deep_otm_call, 0.0,
|
|
231
|
+
msg="Deep OTM call should have positive value")
|
|
232
|
+
self.assertLess(deep_otm_call, 5.0,
|
|
233
|
+
msg="Deep OTM call should have small value")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
if __name__ == '__main__':
|
|
237
|
+
# More detailed output
|
|
238
|
+
print("\nVOLY CLIENT DETAILED TEST SUITE")
|
|
239
|
+
print("=" * 80)
|
|
240
|
+
print(f"Testing against expected values for options pricing and Greeks")
|
|
241
|
+
print("-" * 80)
|
|
242
|
+
|
|
243
|
+
# Run tests with more verbose output
|
|
244
|
+
unittest.main(verbosity=2)
|
voly-0.0.8/tests/test_client.py
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Tests for the VolyClient class.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
import numpy as np
|
|
7
|
-
from voly import VolyClient
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def test_client_initialization():
|
|
11
|
-
"""Test that the client initializes correctly."""
|
|
12
|
-
client = VolyClient()
|
|
13
|
-
assert client is not None
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def test_bs_pricing():
|
|
17
|
-
"""Test Black-Scholes pricing function."""
|
|
18
|
-
client = VolyClient()
|
|
19
|
-
|
|
20
|
-
# Test call price
|
|
21
|
-
call_price = client.bs(s=100, k=100, r=0.05, vol=0.2, t=1, option_type='call')
|
|
22
|
-
assert call_price > 0
|
|
23
|
-
assert call_price < 20 # Sanity check
|
|
24
|
-
|
|
25
|
-
# Test put price
|
|
26
|
-
put_price = client.bs(s=100, k=100, r=0.05, vol=0.2, t=1, option_type='put')
|
|
27
|
-
assert put_price > 0
|
|
28
|
-
assert put_price < 20 # Sanity check
|
|
29
|
-
|
|
30
|
-
# Test put-call parity (approximately)
|
|
31
|
-
parity_diff = abs(call_price - put_price - 100 + 100 * np.exp(-0.05))
|
|
32
|
-
assert parity_diff < 1e-10
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def test_greeks_calculation():
|
|
36
|
-
"""Test that all Greeks are calculated correctly."""
|
|
37
|
-
client = VolyClient()
|
|
38
|
-
|
|
39
|
-
# Calculate all Greeks
|
|
40
|
-
greeks = client.greeks(s=100, k=100, r=0.05, vol=0.2, t=1, option_type='call')
|
|
41
|
-
|
|
42
|
-
# Check that all expected Greeks are present
|
|
43
|
-
expected_keys = ['price', 'delta', 'gamma', 'vega', 'theta', 'rho', 'vanna', 'volga', 'charm']
|
|
44
|
-
for key in expected_keys:
|
|
45
|
-
assert key in greeks
|
|
46
|
-
assert isinstance(greeks[key], float)
|
|
47
|
-
|
|
48
|
-
# Basic sanity checks
|
|
49
|
-
assert 0 < greeks['delta'] < 1 # Call delta between 0 and 1
|
|
50
|
-
assert greeks['gamma'] > 0 # Gamma always positive
|
|
51
|
-
assert greeks['vega'] > 0 # Vega always positive
|
|
52
|
-
assert greeks['theta'] < 0 # Call theta typically negative
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|