voly 0.0.81__py3-none-any.whl → 0.0.82__py3-none-any.whl
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/client.py +28 -0
- voly/core/fit.py +2 -1
- voly/core/interpolate.py +112 -204
- {voly-0.0.81.dist-info → voly-0.0.82.dist-info}/METADATA +1 -1
- {voly-0.0.81.dist-info → voly-0.0.82.dist-info}/RECORD +8 -8
- {voly-0.0.81.dist-info → voly-0.0.82.dist-info}/LICENSE +0 -0
- {voly-0.0.81.dist-info → voly-0.0.82.dist-info}/WHEEL +0 -0
- {voly-0.0.81.dist-info → voly-0.0.82.dist-info}/top_level.txt +0 -0
voly/client.py
CHANGED
|
@@ -420,6 +420,34 @@ class VolyClient:
|
|
|
420
420
|
|
|
421
421
|
return plots
|
|
422
422
|
|
|
423
|
+
# -------------------------------------------------------------------------
|
|
424
|
+
# Interpolate
|
|
425
|
+
# -------------------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
@staticmethod
|
|
428
|
+
def interpolate_model(fit_results: pd.DataFrame,
|
|
429
|
+
list_of_days: List[str] = ['7d', '30d', '90d', '150d', '240d'],
|
|
430
|
+
method: str = 'cubic') -> pd.DataFrame:
|
|
431
|
+
"""
|
|
432
|
+
Interpolate a fitted model to specific days to expiry.
|
|
433
|
+
|
|
434
|
+
Parameters:
|
|
435
|
+
- fit_results: DataFrame with fitting results from fit_model()
|
|
436
|
+
- list_of_days: List of specific days to include (e.g., ['7d', '30d', '90d'])
|
|
437
|
+
- method: Interpolation method ('linear', 'cubic', 'pchip', etc.)
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
- DataFrame with interpolated model parameters for the specified days
|
|
441
|
+
"""
|
|
442
|
+
logger.info(f"Interpolating model with {method} method")
|
|
443
|
+
|
|
444
|
+
# Interpolate the model
|
|
445
|
+
interpolated_results = interpolate_model(
|
|
446
|
+
fit_results, list_of_days, method
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
return interpolated_results
|
|
450
|
+
|
|
423
451
|
# -------------------------------------------------------------------------
|
|
424
452
|
# Risk-Neutral Density (RND)
|
|
425
453
|
# -------------------------------------------------------------------------
|
voly/core/fit.py
CHANGED
|
@@ -93,6 +93,8 @@ def fit_model(option_chain: pd.DataFrame,
|
|
|
93
93
|
# ANSI color codes for terminal output
|
|
94
94
|
GREEN, RED, RESET = '\033[32m', '\033[31m', '\033[0m'
|
|
95
95
|
|
|
96
|
+
s = option_chain['index_price'].iloc[-1]
|
|
97
|
+
|
|
96
98
|
for ytm in unique_ytms:
|
|
97
99
|
# Get data for this maturity
|
|
98
100
|
maturity_data = option_chain[option_chain['ytm'] == ytm]
|
|
@@ -128,7 +130,6 @@ def fit_model(option_chain: pd.DataFrame,
|
|
|
128
130
|
max_error = np.max(np.abs(iv_market - iv_model))
|
|
129
131
|
|
|
130
132
|
# Get or calculate additional required data
|
|
131
|
-
s = maturity_data['index_price'].iloc[0]
|
|
132
133
|
u = maturity_data['underlying_price'].iloc[0]
|
|
133
134
|
|
|
134
135
|
# Aggregate open interest and volume
|
voly/core/interpolate.py
CHANGED
|
@@ -1,221 +1,129 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Model interpolation module for the Voly package.
|
|
3
3
|
|
|
4
|
-
This module handles
|
|
5
|
-
|
|
4
|
+
This module handles interpolating volatility model parameters across different
|
|
5
|
+
days to expiry, allowing for consistent volatility surfaces at arbitrary tenors.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import numpy as np
|
|
9
8
|
import pandas as pd
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
import numpy as np
|
|
10
|
+
import datetime as dt
|
|
11
|
+
from typing import List, Dict, Tuple, Optional, Union, Any
|
|
12
|
+
from scipy import interpolate
|
|
12
13
|
from voly.utils.logger import logger, catch_exception
|
|
13
14
|
from voly.exceptions import VolyError
|
|
14
|
-
from voly.models import SVIModel
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@catch_exception
|
|
18
|
-
def interpolate_surface_time(
|
|
19
|
-
moneyness_grid: np.ndarray,
|
|
20
|
-
expiries: np.ndarray,
|
|
21
|
-
surface_values: np.ndarray,
|
|
22
|
-
target_expiries: np.ndarray,
|
|
23
|
-
method: str = 'cubic'
|
|
24
|
-
) -> np.ndarray:
|
|
25
|
-
"""
|
|
26
|
-
Interpolate the surface across the time dimension.
|
|
27
|
-
|
|
28
|
-
Parameters:
|
|
29
|
-
- moneyness_grid: Array of log-moneyness values
|
|
30
|
-
- expiries: Array of expiry times (in years)
|
|
31
|
-
- surface_values: 2D array of values to interpolate (rows=expiries, cols=moneyness)
|
|
32
|
-
- target_expiries: Array of target expiry times to interpolate to
|
|
33
|
-
- method: Interpolation method ('linear', 'cubic', 'pchip', etc.)
|
|
34
|
-
|
|
35
|
-
Returns:
|
|
36
|
-
- 2D array of interpolated values (rows=target_expiries, cols=moneyness)
|
|
37
|
-
"""
|
|
38
|
-
if len(expiries) < 2:
|
|
39
|
-
raise VolyError("At least two expiries are required for time interpolation")
|
|
40
|
-
|
|
41
|
-
# Initialize the output array
|
|
42
|
-
interpolated_surface = np.zeros((len(target_expiries), len(moneyness_grid)))
|
|
43
|
-
|
|
44
|
-
# For each moneyness point, interpolate across time
|
|
45
|
-
for i in range(len(moneyness_grid)):
|
|
46
|
-
if method == 'pchip':
|
|
47
|
-
# Use PCHIP (Piecewise Cubic Hermite Interpolating Polynomial)
|
|
48
|
-
interpolated_values = pchip_interpolate(expiries, surface_values[:, i], target_expiries)
|
|
49
|
-
else:
|
|
50
|
-
# Use regular interpolation (linear, cubic, etc.)
|
|
51
|
-
interp_func = interp1d(expiries, surface_values[:, i], kind=method, bounds_error=False,
|
|
52
|
-
fill_value='extrapolate')
|
|
53
|
-
interpolated_values = interp_func(target_expiries)
|
|
54
|
-
|
|
55
|
-
interpolated_surface[:, i] = interpolated_values
|
|
56
|
-
|
|
57
|
-
return interpolated_surface
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
@catch_exception
|
|
61
|
-
def create_target_expiries(
|
|
62
|
-
min_dte: float,
|
|
63
|
-
max_dte: float,
|
|
64
|
-
num_points: int = 10,
|
|
65
|
-
specific_days: Optional[List[int]] = None
|
|
66
|
-
) -> np.ndarray:
|
|
67
|
-
"""
|
|
68
|
-
Create a grid of target expiry days for interpolation.
|
|
69
|
-
|
|
70
|
-
Parameters:
|
|
71
|
-
- min_dte: Minimum days to expiry
|
|
72
|
-
- max_dte: Maximum days to expiry
|
|
73
|
-
- num_points: Number of points for regular grid
|
|
74
|
-
- specific_days: Optional list of specific days to include (e.g., [7, 30, 90, 180])
|
|
75
|
-
|
|
76
|
-
Returns:
|
|
77
|
-
- Array of target expiry times in years
|
|
78
|
-
"""
|
|
79
|
-
# Create regular grid in days
|
|
80
|
-
if specific_days is not None:
|
|
81
|
-
# Filter specific days to be within range
|
|
82
|
-
days = np.array([d for d in specific_days if min_dte <= d <= max_dte])
|
|
83
|
-
if len(days) == 0:
|
|
84
|
-
logger.warning("No specific days within range, using regular grid")
|
|
85
|
-
days = np.linspace(min_dte, max_dte, num_points)
|
|
86
|
-
else:
|
|
87
|
-
# Use regular grid
|
|
88
|
-
days = np.linspace(min_dte, max_dte, num_points)
|
|
89
|
-
|
|
90
|
-
# Convert to years
|
|
91
|
-
years = days / 365.25
|
|
92
|
-
|
|
93
|
-
return years
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
@catch_exception
|
|
97
|
-
def interpolate_svi_parameters(
|
|
98
|
-
raw_param_matrix: pd.DataFrame,
|
|
99
|
-
target_expiries_years: np.ndarray,
|
|
100
|
-
method: str = 'cubic'
|
|
101
|
-
) -> pd.DataFrame:
|
|
102
|
-
"""
|
|
103
|
-
Interpolate SVI parameters across time.
|
|
104
|
-
|
|
105
|
-
Parameters:
|
|
106
|
-
- raw_param_matrix: Matrix of SVI parameters with maturity names as columns
|
|
107
|
-
- target_expiries_years: Array of target expiry times (in years)
|
|
108
|
-
- method: Interpolation method ('linear', 'cubic', 'pchip', etc.)
|
|
109
|
-
|
|
110
|
-
Returns:
|
|
111
|
-
- Matrix of interpolated SVI parameters
|
|
112
|
-
"""
|
|
113
|
-
# Get expiry times in years from the parameter matrix
|
|
114
|
-
yte_values = raw_param_matrix.attrs['yte_values']
|
|
115
|
-
dte_values = raw_param_matrix.attrs['dte_values']
|
|
116
|
-
|
|
117
|
-
# Sort maturity names by DTE
|
|
118
|
-
maturity_names = sorted(yte_values.keys(), key=lambda x: dte_values[x])
|
|
119
|
-
|
|
120
|
-
# Extract expiry times in order
|
|
121
|
-
expiry_years = np.array([yte_values[m] for m in maturity_names])
|
|
122
|
-
|
|
123
|
-
# Check if we have enough points for interpolation
|
|
124
|
-
if len(expiry_years) < 2:
|
|
125
|
-
raise VolyError("At least two expiries are required for interpolation")
|
|
126
|
-
|
|
127
|
-
# Create new parameter matrix for interpolated values
|
|
128
|
-
interp_param_matrix = pd.DataFrame(
|
|
129
|
-
columns=[f"t{i:.2f}" for i in target_expiries_years],
|
|
130
|
-
index=SVIModel.PARAM_NAMES
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
# For each SVI parameter, interpolate across time
|
|
134
|
-
for param in SVIModel.PARAM_NAMES:
|
|
135
|
-
param_values = np.array([raw_param_matrix.loc[param, m] for m in maturity_names])
|
|
136
|
-
|
|
137
|
-
if method == 'pchip':
|
|
138
|
-
interpolated_values = pchip_interpolate(expiry_years, param_values, target_expiries_years)
|
|
139
|
-
else:
|
|
140
|
-
interp_func = interp1d(expiry_years, param_values, kind=method, bounds_error=False,
|
|
141
|
-
fill_value='extrapolate')
|
|
142
|
-
interpolated_values = interp_func(target_expiries_years)
|
|
143
|
-
|
|
144
|
-
# Store interpolated values
|
|
145
|
-
for i, t in enumerate(target_expiries_years):
|
|
146
|
-
interp_param_matrix.loc[param, f"t{t:.2f}"] = interpolated_values[i]
|
|
147
|
-
|
|
148
|
-
# Create matching DTE values for convenience
|
|
149
|
-
interp_dte_values = target_expiries_years * 365.25
|
|
150
|
-
|
|
151
|
-
# Store YTE and DTE as attributes in the DataFrame for reference
|
|
152
|
-
interp_param_matrix.attrs['yte_values'] = {f"t{t:.2f}": t for t in target_expiries_years}
|
|
153
|
-
interp_param_matrix.attrs['dte_values'] = {f"t{t:.2f}": t * 365.25 for t in target_expiries_years}
|
|
154
|
-
|
|
155
|
-
return interp_param_matrix
|
|
156
15
|
|
|
157
16
|
|
|
158
17
|
@catch_exception
|
|
159
|
-
def interpolate_model(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
num_points: int = 10,
|
|
163
|
-
method: str = 'cubic'
|
|
164
|
-
) -> Dict[str, Any]:
|
|
18
|
+
def interpolate_model(fit_results: pd.DataFrame,
|
|
19
|
+
list_of_days: List[str] = ['7d', '30d', '90d', '150d', '240d'],
|
|
20
|
+
method: str = 'cubic') -> pd.DataFrame:
|
|
165
21
|
"""
|
|
166
|
-
Interpolate
|
|
22
|
+
Interpolate model parameters across different days to expiry.
|
|
167
23
|
|
|
168
24
|
Parameters:
|
|
169
|
-
- fit_results:
|
|
170
|
-
-
|
|
171
|
-
- num_points: Number of points for regular grid if specific_days is None
|
|
25
|
+
- fit_results: DataFrame with model fitting results, indexed by maturity names
|
|
26
|
+
- list_of_days: list of specific days to interpolate to (e.g., ['7d', '30d', '90d'])
|
|
172
27
|
- method: Interpolation method ('linear', 'cubic', 'pchip', etc.)
|
|
173
28
|
|
|
174
29
|
Returns:
|
|
175
|
-
-
|
|
30
|
+
- DataFrame with interpolated model parameters for the specified days
|
|
176
31
|
"""
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
#
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
for
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
32
|
+
logger.info(f"Interpolating model parameters using {method} method")
|
|
33
|
+
|
|
34
|
+
# Check if fit_results is valid
|
|
35
|
+
if fit_results is None or fit_results.empty:
|
|
36
|
+
raise VolyError("Fit results DataFrame is empty or None")
|
|
37
|
+
|
|
38
|
+
# Extract years to maturity from fit_results
|
|
39
|
+
original_ytms = fit_results['ytm'].values
|
|
40
|
+
|
|
41
|
+
if len(original_ytms) < 2:
|
|
42
|
+
raise VolyError("Need at least two maturities in fit_results to interpolate")
|
|
43
|
+
|
|
44
|
+
# Sort original years to maturity for proper interpolation
|
|
45
|
+
sorted_indices = np.argsort(original_ytms)
|
|
46
|
+
original_ytms = original_ytms[sorted_indices]
|
|
47
|
+
|
|
48
|
+
# Parse days from strings like '7d', '30d', etc.
|
|
49
|
+
target_days = []
|
|
50
|
+
for day_str in list_of_days:
|
|
51
|
+
if not day_str.endswith('d'):
|
|
52
|
+
raise VolyError(f"Invalid day format: {day_str}. Expected format: '7d', '30d', etc.")
|
|
53
|
+
try:
|
|
54
|
+
days = int(day_str[:-1])
|
|
55
|
+
target_days.append(days)
|
|
56
|
+
except ValueError:
|
|
57
|
+
raise VolyError(f"Invalid day value: {day_str}. Expected format: '7d', '30d', etc.")
|
|
58
|
+
|
|
59
|
+
# Convert target days to years for interpolation (to match original implementation)
|
|
60
|
+
target_years = [day / 365.25 for day in target_days]
|
|
61
|
+
|
|
62
|
+
# Check if target years are within the range of original years
|
|
63
|
+
min_original, max_original = np.min(original_ytms), np.max(original_ytms)
|
|
64
|
+
for years in target_years:
|
|
65
|
+
if years < min_original or years > max_original:
|
|
66
|
+
logger.warning(
|
|
67
|
+
f"Target time {years:.4f} years is outside the range of original data [{min_original:.4f}, {max_original:.4f}]. "
|
|
68
|
+
"Extrapolation may give unreliable results.")
|
|
69
|
+
|
|
70
|
+
# Columns to interpolate
|
|
71
|
+
param_columns = ['u', 'a', 'b', 'rho', 'm', 'sigma', 'nu', 'psi', 'p', 'c', 'nu_tilde']
|
|
72
|
+
|
|
73
|
+
# Create empty DataFrame for interpolated results
|
|
74
|
+
interpolated_df = pd.DataFrame(index=[f"{day}d" for day in target_days])
|
|
75
|
+
|
|
76
|
+
# Generate YTM and maturity dates for interpolated results
|
|
77
|
+
interpolated_df['dtm'] = target_days
|
|
78
|
+
interpolated_df['ytm'] = [day / 365.25 for day in target_days]
|
|
79
|
+
|
|
80
|
+
# Calculate maturity dates
|
|
81
|
+
now = dt.datetime.now()
|
|
82
|
+
maturity_dates = []
|
|
83
|
+
for days in target_days:
|
|
84
|
+
maturity_date = now + dt.timedelta(days=days)
|
|
85
|
+
maturity_dates.append(maturity_date)
|
|
86
|
+
|
|
87
|
+
interpolated_df['maturity_date'] = maturity_dates
|
|
88
|
+
|
|
89
|
+
# Sort fit_results by ytm
|
|
90
|
+
sorted_fit_results = fit_results.iloc[sorted_indices]
|
|
91
|
+
|
|
92
|
+
# Interpolate model parameters
|
|
93
|
+
for param in param_columns:
|
|
94
|
+
if param in fit_results.columns:
|
|
95
|
+
try:
|
|
96
|
+
# Create interpolation function using years to expiry
|
|
97
|
+
f = interpolate.interp1d(
|
|
98
|
+
original_ytms,
|
|
99
|
+
sorted_fit_results[param].values,
|
|
100
|
+
kind=method,
|
|
101
|
+
bounds_error=False,
|
|
102
|
+
fill_value='extrapolate'
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Apply interpolation with target years
|
|
106
|
+
interpolated_df[param] = f(target_years)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error(f"Error interpolating parameter {param}: {str(e)}")
|
|
109
|
+
# Fallback to nearest neighbor if sophisticated method fails
|
|
110
|
+
f = interpolate.interp1d(
|
|
111
|
+
original_ytms,
|
|
112
|
+
sorted_fit_results[param].values,
|
|
113
|
+
kind='nearest',
|
|
114
|
+
bounds_error=False,
|
|
115
|
+
fill_value=(sorted_fit_results[param].iloc[0], sorted_fit_results[param].iloc[-1])
|
|
116
|
+
)
|
|
117
|
+
interpolated_df[param] = f(target_years)
|
|
118
|
+
|
|
119
|
+
# Ensure consistent ordering of columns with expected structure
|
|
120
|
+
expected_columns = ['u', 'maturity_date', 'dtm', 'ytm', 'a', 'b', 'rho', 'm', 'sigma',
|
|
121
|
+
'nu', 'psi', 'p', 'c', 'nu_tilde']
|
|
122
|
+
|
|
123
|
+
# Create final column order based on available columns
|
|
124
|
+
column_order = [col for col in expected_columns if col in interpolated_df.columns]
|
|
125
|
+
interpolated_df = interpolated_df[column_order]
|
|
126
|
+
|
|
127
|
+
logger.info(f"Successfully interpolated model parameters for {len(target_days)} target days")
|
|
128
|
+
|
|
129
|
+
return interpolated_df
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
voly/__init__.py,sha256=8xyDk7rFCn_MOD5hxuv5cxxKZvBVRiSIM7TgaMPpwpw,211
|
|
2
|
-
voly/client.py,sha256=
|
|
2
|
+
voly/client.py,sha256=Y6XidftiRTiFHuckv9HT8EyApF6zDjR14mryZeO0rGA,21828
|
|
3
3
|
voly/exceptions.py,sha256=PBsbn1vNMvKcCJwwJ4lBO6glD85jo1h2qiEmD7ArAjs,92
|
|
4
4
|
voly/formulas.py,sha256=wSbGAH6GuQThT9QyQY4Ud2eUf9fo1YFHglUmP6fNris,11871
|
|
5
5
|
voly/models.py,sha256=LXXIlpXZQEfXTuCngxC8Hd3bWtw6wdXDCSGxTLmHM-c,3659
|
|
6
6
|
voly/core/__init__.py,sha256=bu6fS2I1Pj9fPPnl-zY3L7NqrZSY5Zy6NY2uMUvdhKs,183
|
|
7
7
|
voly/core/charts.py,sha256=ycZgbPeROYVRMO61auVXCGLmKZA2NwS3vLOsQ9wqCyo,27526
|
|
8
8
|
voly/core/data.py,sha256=JCPD44UDSJqh83jjapAtXVpJEHm8Hq4JC8lVhl5Zb4A,8935
|
|
9
|
-
voly/core/fit.py,sha256=
|
|
10
|
-
voly/core/interpolate.py,sha256=
|
|
9
|
+
voly/core/fit.py,sha256=lDlw4z5lehJSgpT4-J7NLq3wf42sQQM-0GHSlaATGKA,9223
|
|
10
|
+
voly/core/interpolate.py,sha256=7FBx7EmbgET1I1-MD6o-nxCyGlAqcQJulCXYvzATisU,5243
|
|
11
11
|
voly/core/rnd.py,sha256=8FTU-Qp9epW9yE4XSOdiFGIRXrGyXqF6mVgZn1NMvxk,11813
|
|
12
12
|
voly/utils/__init__.py,sha256=E05mWatyC-PDOsCxQV1p5Xi1IgpOomxrNURyCx_gB-w,200
|
|
13
13
|
voly/utils/logger.py,sha256=4-_2bVJmq17Q0d7Rd2mPg1AeR8gxv6EPvcmBDMFWcSM,1744
|
|
14
|
-
voly-0.0.
|
|
15
|
-
voly-0.0.
|
|
16
|
-
voly-0.0.
|
|
17
|
-
voly-0.0.
|
|
18
|
-
voly-0.0.
|
|
14
|
+
voly-0.0.82.dist-info/LICENSE,sha256=wcHIVbE12jfcBOai_wqBKY6xvNQU5E909xL1zZNq_2Q,1065
|
|
15
|
+
voly-0.0.82.dist-info/METADATA,sha256=uV23GWgeNRpVdmu6psFoJdeJJ7v1CwpoBL-C720BJnQ,4092
|
|
16
|
+
voly-0.0.82.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
|
17
|
+
voly-0.0.82.dist-info/top_level.txt,sha256=ZfLw2sSxF-LrKAkgGjOmeTcw6_gD-30zvtdEY5W4B7c,5
|
|
18
|
+
voly-0.0.82.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|