voly 0.0.1__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.
@@ -0,0 +1,221 @@
1
+ """
2
+ Surface interpolation module for the Voly package.
3
+
4
+ This module handles interpolation of implied volatility surfaces
5
+ across both moneyness and time dimensions.
6
+ """
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+ from typing import List, Tuple, Dict, Optional, Union, Any
11
+ from scipy.interpolate import interp1d, pchip_interpolate
12
+ from voly.utils.logger import logger, catch_exception
13
+ from voly.exceptions import InterpolationError
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 InterpolationError("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 InterpolationError("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
+
157
+
158
+ @catch_exception
159
+ def interpolate_model(
160
+ fit_results: Dict[str, Any],
161
+ specific_days: Optional[List[int]] = None,
162
+ num_points: int = 10,
163
+ method: str = 'cubic'
164
+ ) -> Dict[str, Any]:
165
+ """
166
+ Interpolate a fitted model to specific days to expiry.
167
+
168
+ Parameters:
169
+ - fit_results: Dictionary with fitting results from fit_model()
170
+ - specific_days: Optional list of specific days to include (e.g., [7, 30, 90, 180])
171
+ - num_points: Number of points for regular grid if specific_days is None
172
+ - method: Interpolation method ('linear', 'cubic', 'pchip', etc.)
173
+
174
+ Returns:
175
+ - Dictionary with interpolation results
176
+ """
177
+ # Extract required data from fit results
178
+ raw_param_matrix = fit_results['raw_param_matrix']
179
+ moneyness_grid = fit_results['moneyness_grid']
180
+
181
+ # Get min and max DTE from the original data
182
+ dte_values = list(raw_param_matrix.attrs['dte_values'].values())
183
+ min_dte = min(dte_values)
184
+ max_dte = max(dte_values)
185
+
186
+ # Create target expiry grid
187
+ target_expiries_years = create_target_expiries(min_dte, max_dte, num_points, specific_days)
188
+
189
+ # Interpolate SVI parameters
190
+ interp_param_matrix = interpolate_svi_parameters(raw_param_matrix, target_expiries_years, method)
191
+
192
+ # Generate implied volatility surface from interpolated parameters
193
+ iv_surface = {}
194
+ for maturity_name, yte in interp_param_matrix.attrs['yte_values'].items():
195
+ svi_params = interp_param_matrix[maturity_name].values
196
+ w_svi = [SVIModel.svi(x, *svi_params) for x in moneyness_grid]
197
+ iv_surface[yte] = np.sqrt(np.array(w_svi) / yte)
198
+
199
+ # Convert to Jump-Wing parameters for the interpolated maturities
200
+ jw_param_matrix = pd.DataFrame(
201
+ columns=interp_param_matrix.columns,
202
+ index=SVIModel.JW_PARAM_NAMES
203
+ )
204
+
205
+ for maturity_name, yte in interp_param_matrix.attrs['yte_values'].items():
206
+ a, b, sigma, rho, m = interp_param_matrix[maturity_name].values
207
+ nu, psi, p, c, nu_tilde = SVIModel.svi_jw_params(a, b, sigma, rho, m, yte)
208
+ jw_param_matrix[maturity_name] = [nu, psi, p, c, nu_tilde]
209
+
210
+ # Copy attributes from param matrix
211
+ jw_param_matrix.attrs = interp_param_matrix.attrs.copy()
212
+
213
+ return {
214
+ 'moneyness_grid': moneyness_grid,
215
+ 'target_expiries_years': target_expiries_years,
216
+ 'target_expiries_days': target_expiries_years * 365.25,
217
+ 'interp_param_matrix': interp_param_matrix,
218
+ 'interp_jw_param_matrix': jw_param_matrix,
219
+ 'iv_surface': iv_surface,
220
+ 'method': method
221
+ }
voly/core/rnd.py ADDED
@@ -0,0 +1,389 @@
1
+ """
2
+ This module handles calculating risk-neutral densities from
3
+ fitted volatility models and converting to probability functions.
4
+ """
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ from typing import Dict, List, Tuple, Optional, Union, Any
9
+ from voly.utils.logger import logger, catch_exception
10
+ from voly.exceptions import RNDError
11
+ from voly.models import SVIModel
12
+ from voly.formulas import rnd_base
13
+
14
+
15
+ @catch_exception
16
+ def calculate_risk_neutral_density(
17
+ log_moneyness: np.ndarray,
18
+ params: List[float],
19
+ model: Any = SVIModel
20
+ ) -> np.ndarray:
21
+ """
22
+ Calculate the risk-neutral density (RND) from model parameters.
23
+
24
+ Parameters:
25
+ - log_moneyness: Array of log-moneyness values
26
+ - params: Model parameters (e.g., SVI parameters [a, b, sigma, rho, m])
27
+ - model: Model class to use (default: SVIModel)
28
+
29
+ Returns:
30
+ - Array of RND values corresponding to each log-moneyness value
31
+ """
32
+ # Adjust parameter order to match function signatures
33
+ a, b, sigma, rho, m = params
34
+
35
+ # Calculate total variance
36
+ total_var = np.array([model.svi(x, a, b, sigma, rho, m) for x in log_moneyness])
37
+
38
+ # Calculate risk-neutral density using the base RND function
39
+ rnd_values = np.array([rnd_base(x, var) for x, var in zip(log_moneyness, total_var)])
40
+
41
+ return rnd_values
42
+
43
+
44
+ @catch_exception
45
+ def calculate_rnd_for_all_expiries(
46
+ moneyness_grid: np.ndarray,
47
+ param_matrix: pd.DataFrame,
48
+ model: Any = SVIModel
49
+ ) -> Dict[str, np.ndarray]:
50
+ """
51
+ Calculate RND for all expiries in the parameter matrix.
52
+
53
+ Parameters:
54
+ - moneyness_grid: Grid of log-moneyness values
55
+ - param_matrix: Matrix containing model parameters for each expiry
56
+ - model: Model class to use (default: SVIModel)
57
+
58
+ Returns:
59
+ - Dictionary mapping maturity names to RND arrays
60
+ """
61
+ rnd_surface = {}
62
+
63
+ # Get YTE values from the parameter matrix attributes
64
+ yte_values = param_matrix.attrs['yte_values']
65
+
66
+ # Calculate RND for each expiry
67
+ for maturity_name, yte in yte_values.items():
68
+ params = param_matrix[maturity_name].values
69
+
70
+ # Calculate RND
71
+ rnd_values = calculate_risk_neutral_density(
72
+ moneyness_grid,
73
+ params,
74
+ yte,
75
+ model=model
76
+ )
77
+
78
+ rnd_surface[maturity_name] = rnd_values
79
+
80
+ return rnd_surface
81
+
82
+
83
+ @catch_exception
84
+ def calculate_probability_thresholds(
85
+ moneyness_grid: np.ndarray,
86
+ rnd_values: np.ndarray,
87
+ thresholds=None
88
+ ) -> Dict[str, float]:
89
+ """
90
+ Calculate probabilities at specific log-moneyness thresholds.
91
+
92
+ Parameters:
93
+ - moneyness_grid: Grid of log-moneyness values
94
+ - rnd_values: Risk-neutral density values
95
+ - thresholds: Log-moneyness thresholds to calculate probabilities for
96
+
97
+ Returns:
98
+ - Dictionary mapping thresholds to their probabilities
99
+ """
100
+ # Calculate step size for integration
101
+ if thresholds is None:
102
+ thresholds = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0]
103
+
104
+ dx = moneyness_grid[1] - moneyness_grid[0]
105
+
106
+ # Normalize the RND for proper probability
107
+ total_density = np.sum(rnd_values) * dx
108
+ normalized_rnd = rnd_values / total_density if total_density > 0 else rnd_values
109
+
110
+ # Calculate cumulative distribution function (CDF)
111
+ cdf = np.cumsum(normalized_rnd) * dx
112
+
113
+ # Initialize probability results dictionary
114
+ result = {}
115
+
116
+ # Calculate probabilities for each threshold
117
+ for threshold in thresholds:
118
+ # Find the nearest index
119
+ idx = np.abs(moneyness_grid - threshold).argmin()
120
+
121
+ # Get exact threshold value (may be slightly different from requested)
122
+ actual_threshold = moneyness_grid[idx]
123
+
124
+ # Calculate probability P(X ≤ threshold)
125
+ if idx < len(cdf):
126
+ prob = cdf[idx]
127
+ else:
128
+ prob = 1.0
129
+
130
+ # Calculate probability of exceeding positive thresholds
131
+ # and probability of going below negative thresholds
132
+ if threshold >= 0:
133
+ # P(X > threshold) = 1 - P(X ≤ threshold)
134
+ result[f"p_above_{threshold}"] = 1.0 - prob
135
+ else:
136
+ # P(X < threshold) = P(X ≤ threshold)
137
+ result[f"p_below_{threshold}"] = prob
138
+
139
+ return result
140
+
141
+
142
+ @catch_exception
143
+ def calculate_moments(
144
+ moneyness_grid: np.ndarray,
145
+ rnd_values: np.ndarray) -> Dict[str, float]:
146
+ """
147
+ Calculate statistical moments (mean, variance, skewness, kurtosis) of the RND.
148
+
149
+ Parameters:
150
+ - moneyness_grid: Grid of log-moneyness values
151
+ - rnd_values: Array of RND values
152
+
153
+ Returns:
154
+ - Dictionary with statistical moments
155
+ """
156
+ # Calculate total probability (for normalization)
157
+ dx = moneyness_grid[1] - moneyness_grid[0]
158
+ total_prob = np.sum(rnd_values) * dx
159
+
160
+ # Normalize the RND if needed
161
+ normalized_rnd = rnd_values / total_prob if total_prob > 0 else rnd_values
162
+
163
+ # Calculate moments in percentage terms
164
+ # First, convert log-moneyness to percentage returns
165
+ returns_pct = (np.exp(moneyness_grid) - 1) * 100 # Convert to percentage returns
166
+
167
+ # Calculate mean (expected return in %)
168
+ mean_pct = np.sum(returns_pct * normalized_rnd) * dx
169
+
170
+ # Calculate variance (in % squared)
171
+ centered_returns = returns_pct - mean_pct
172
+ variance_pct = np.sum(centered_returns ** 2 * normalized_rnd) * dx
173
+ std_dev_pct = np.sqrt(variance_pct)
174
+
175
+ # Skewness and kurtosis are unitless
176
+ skewness = np.sum(centered_returns ** 3 * normalized_rnd) * dx / (std_dev_pct ** 3) if std_dev_pct > 0 else 0
177
+ kurtosis = np.sum(centered_returns ** 4 * normalized_rnd) * dx / (variance_pct ** 2) if variance_pct > 0 else 0
178
+
179
+ return {
180
+ "mean_pct": mean_pct, # Mean return in percentage
181
+ "variance_pct": variance_pct, # Variance in percentage squared
182
+ "std_dev_pct": std_dev_pct, # Standard deviation in percentage
183
+ "skewness": skewness, # Unitless
184
+ "kurtosis": kurtosis, # Unitless
185
+ "excess_kurtosis": kurtosis - 3 # Unitless
186
+ }
187
+
188
+
189
+ @catch_exception
190
+ def analyze_rnd_statistics(
191
+ moneyness_grid: np.ndarray,
192
+ rnd_surface: Dict[str, np.ndarray],
193
+ param_matrix: pd.DataFrame) -> pd.DataFrame:
194
+ """
195
+ Analyze RND statistics for all expiries.
196
+
197
+ Parameters:
198
+ - moneyness_grid: Grid of log-moneyness values
199
+ - rnd_surface: Dictionary mapping maturity names to RND arrays
200
+ - param_matrix: Matrix containing model parameters
201
+
202
+ Returns:
203
+ - DataFrame with RND statistics for each expiry
204
+ """
205
+ # Get maturity information
206
+ dte_values = param_matrix.attrs['dte_values']
207
+ yte_values = param_matrix.attrs['yte_values']
208
+
209
+ # Initialize data dictionary
210
+ data = {
211
+ "maturity_name": [],
212
+ "dte": [],
213
+ "yte": [],
214
+ "mean_pct": [],
215
+ "std_dev_pct": [],
216
+ "skewness": [],
217
+ "excess_kurtosis": []
218
+ }
219
+
220
+ # Calculate moments for each expiry
221
+ for maturity_name, rnd in rnd_surface.items():
222
+ moments = calculate_moments(moneyness_grid, rnd)
223
+
224
+ data["maturity_name"].append(maturity_name)
225
+ data["dte"].append(dte_values[maturity_name])
226
+ data["yte"].append(yte_values[maturity_name])
227
+ data["mean_pct"].append(moments["mean_pct"])
228
+ data["std_dev_pct"].append(moments["std_dev_pct"])
229
+ data["skewness"].append(moments["skewness"])
230
+ data["excess_kurtosis"].append(moments["excess_kurtosis"])
231
+
232
+ # Create DataFrame and sort by DTE
233
+ stats_df = pd.DataFrame(data)
234
+ stats_df = stats_df.sort_values(by="dte")
235
+
236
+ return stats_df
237
+
238
+
239
+ @catch_exception
240
+ def calculate_pdf(
241
+ moneyness_grid: np.ndarray,
242
+ rnd_values: np.ndarray,
243
+ spot_price: float = 1.0
244
+ ) -> Tuple[np.ndarray, np.ndarray]:
245
+ """
246
+ Calculate probability density function (PDF) from RND values.
247
+
248
+ Parameters:
249
+ - moneyness_grid: Grid of log-moneyness values
250
+ - rnd_values: Array of RND values
251
+ - spot_price: Spot price of the underlying
252
+
253
+ Returns:
254
+ - Tuple of (prices, pdf_values) for plotting
255
+ """
256
+ # Calculate step size for normalization
257
+ dx = moneyness_grid[1] - moneyness_grid[0]
258
+
259
+ # Normalize the RND
260
+ total_density = np.sum(rnd_values) * dx
261
+ pdf_values = rnd_values / total_density if total_density > 0 else rnd_values
262
+
263
+ # Convert log-moneyness to actual prices
264
+ prices = spot_price * np.exp(moneyness_grid)
265
+
266
+ return prices, pdf_values
267
+
268
+
269
+ @catch_exception
270
+ def calculate_cdf(
271
+ moneyness_grid: np.ndarray,
272
+ rnd_values: np.ndarray,
273
+ spot_price: float = 1.0
274
+ ) -> Tuple[np.ndarray, np.ndarray]:
275
+ """
276
+ Calculate cumulative distribution function (CDF) from RND values.
277
+
278
+ Parameters:
279
+ - moneyness_grid: Grid of log-moneyness values
280
+ - rnd_values: Array of RND values
281
+ - spot_price: Spot price of the underlying
282
+
283
+ Returns:
284
+ - Tuple of (prices, cdf_values) for plotting
285
+ """
286
+ # Calculate step size for normalization
287
+ dx = moneyness_grid[1] - moneyness_grid[0]
288
+
289
+ # Normalize the RND
290
+ total_density = np.sum(rnd_values) * dx
291
+ normalized_rnd = rnd_values / total_density if total_density > 0 else rnd_values
292
+
293
+ # Calculate CDF
294
+ cdf_values = np.cumsum(normalized_rnd) * dx
295
+
296
+ # Convert log-moneyness to actual prices
297
+ prices = spot_price * np.exp(moneyness_grid)
298
+
299
+ return prices, cdf_values
300
+
301
+
302
+ @catch_exception
303
+ def calculate_strike_probability(
304
+ target_price: float,
305
+ moneyness_grid: np.ndarray,
306
+ rnd_values: np.ndarray,
307
+ spot_price: float,
308
+ direction: str = 'above'
309
+ ) -> float:
310
+ """
311
+ Calculate probability of price being above or below a target price.
312
+
313
+ Parameters:
314
+ - target_price: Target price level
315
+ - moneyness_grid: Grid of log-moneyness values
316
+ - rnd_values: Array of RND values
317
+ - spot_price: Current spot price
318
+ - direction: 'above' or 'below'
319
+
320
+ Returns:
321
+ - Probability (0 to 1)
322
+ """
323
+ # Convert target price to log-moneyness
324
+ target_moneyness = np.log(target_price / spot_price)
325
+
326
+ # Calculate CDF
327
+ _, cdf_values = calculate_cdf(moneyness_grid, rnd_values, spot_price)
328
+
329
+ # Find the nearest index to target moneyness
330
+ target_idx = np.abs(moneyness_grid - target_moneyness).argmin()
331
+
332
+ # Get probability
333
+ if target_idx < len(cdf_values):
334
+ cdf_at_target = cdf_values[target_idx]
335
+ else:
336
+ cdf_at_target = 1.0
337
+
338
+ # Return probability based on direction
339
+ if direction.lower() == 'above':
340
+ return 1.0 - cdf_at_target
341
+ else: # below
342
+ return cdf_at_target
343
+
344
+
345
+ @catch_exception
346
+ def calculate_rnd(
347
+ fit_results: Dict[str, Any],
348
+ maturity: Optional[str] = None) -> Dict[str, Any]:
349
+ """
350
+ Calculate risk-neutral density from fit results.
351
+
352
+ Parameters:
353
+ - fit_results: Dictionary with fitting results from fit_model()
354
+ - maturity: Optional maturity name to calculate RND for a specific expiry
355
+
356
+ Returns:
357
+ - Dictionary with RND results
358
+ """
359
+ # Extract required data from fit results
360
+ raw_param_matrix = fit_results['raw_param_matrix']
361
+ moneyness_grid = fit_results['moneyness_grid']
362
+
363
+ # Calculate RND for all expiries or just the specified one
364
+ if maturity is not None:
365
+ # Validate maturity
366
+ if maturity not in raw_param_matrix.columns:
367
+ raise RNDError(f"Maturity '{maturity}' not found in fit results")
368
+
369
+ # Just calculate for the specified maturity
370
+ yte = raw_param_matrix.attrs['yte_values'][maturity]
371
+ params = raw_param_matrix[maturity].values
372
+ rnd_values = calculate_risk_neutral_density(moneyness_grid, params, yte)
373
+ rnd_surface = {maturity: rnd_values}
374
+ else:
375
+ # Calculate for all maturities
376
+ rnd_surface = calculate_rnd_for_all_expiries(moneyness_grid, raw_param_matrix)
377
+
378
+ # Calculate statistics
379
+ rnd_statistics = analyze_rnd_statistics(moneyness_grid, rnd_surface, raw_param_matrix)
380
+
381
+ # Calculate probabilities
382
+ rnd_probabilities = analyze_rnd_probabilities(moneyness_grid, rnd_surface, raw_param_matrix)
383
+
384
+ return {
385
+ 'moneyness_grid': moneyness_grid,
386
+ 'rnd_surface': rnd_surface,
387
+ 'rnd_statistics': rnd_statistics,
388
+ 'rnd_probabilities': rnd_probabilities
389
+ }
voly/exceptions.py ADDED
@@ -0,0 +1,3 @@
1
+ class VolyError(Exception):
2
+ """Base exception for all Voly-specific errors."""
3
+ pass