voly 0.0.12__tar.gz → 0.0.13__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: voly
3
- Version: 0.0.12
3
+ Version: 0.0.13
4
4
  Summary: Options & volatility research package
5
5
  Author-email: Manu de Cara <manu.de.cara@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "voly"
7
- version = "0.0.12"
7
+ version = "0.0.13"
8
8
  description = "Options & volatility research package"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -60,7 +60,7 @@ line_length = 100
60
60
  multi_line_output = 3
61
61
 
62
62
  [tool.mypy]
63
- python_version = "0.0.12"
63
+ python_version = "0.0.13"
64
64
  warn_return_any = true
65
65
  warn_unused_configs = true
66
66
  disallow_untyped_defs = true
@@ -18,7 +18,7 @@ from voly.formulas import (
18
18
  bs, delta, gamma, vega, theta, rho, vanna, volga, charm, greeks, iv
19
19
  )
20
20
  from voly.core.data import fetch_option_chain, process_option_chain
21
- from voly.core.fit import fit_model
21
+ from voly.core.fit import fit_model, get_surface
22
22
  from voly.core.rnd import calculate_rnd, calculate_pdf, calculate_cdf, calculate_strike_probability
23
23
  from voly.core.interpolate import interpolate_model
24
24
  from voly.core.charts import (
@@ -295,18 +295,16 @@ class VolyClient:
295
295
  @staticmethod
296
296
  def fit_model(market_data: pd.DataFrame,
297
297
  model_name: str = 'svi',
298
- moneyness_range: Tuple[float, float] = (-2, 2),
299
- num_points: int = 500,
300
- plot: bool = False) -> Dict[str, Any]:
298
+ initial_params: Optional[List[float]] = None,
299
+ param_bounds: Optional[Tuple] = None) -> Dict[str, Any]:
301
300
  """
302
301
  Fit a volatility model to market data.
303
302
 
304
303
  Parameters:
305
304
  - market_data: DataFrame with market data
306
305
  - model_name: Name of model to fit (default: 'svi')
307
- - moneyness_range: (min, max) range for moneyness grid
308
- - num_points: Number of points for moneyness grid
309
- - plot: Whether to generate and return plots
306
+ - initial_params: Optional initial parameters for optimization
307
+ - param_bounds: Optional parameter bounds for optimization
310
308
 
311
309
  Returns:
312
310
  - Dictionary with fitting results and optional plots
@@ -317,18 +315,38 @@ class VolyClient:
317
315
  fit_results = fit_model(
318
316
  market_data=market_data,
319
317
  model_name=model_name,
320
- moneyness_range=moneyness_range,
321
- num_points=num_points
318
+ initial_params=initial_params,
319
+ param_bounds=param_bounds
322
320
  )
323
321
 
324
- # Generate plots if requested
325
- if plot:
326
- logger.info("Generating model fitting plots")
327
- plots = generate_all_plots(fit_results, market_data=market_data)
328
- fit_results['plots'] = plots
329
-
330
322
  return fit_results
331
323
 
324
+ @staticmethod
325
+ def get_surface(param_matrix: pd.DataFrame,
326
+ log_moneyness: Tuple[float, float, int] = (-2, 2, 500)) -> Dict[str, Any]:
327
+ """
328
+ Generate implied volatility surface using optimized SVI parameters.
329
+
330
+ Parameters:
331
+ - param_matrix: Matrix of optimized SVI parameters from fit_results['raw_param_matrix']
332
+ - log_moneyness: Tuple of (min, max, num_points) for the moneyness grid
333
+
334
+ Returns:
335
+ - Dictionary with surface generation results
336
+ """
337
+ logger.info("Generating implied volatility surface")
338
+
339
+ # Generate the surface
340
+ moneyness_grid, iv_surface = get_surface(
341
+ param_matrix=param_matrix,
342
+ log_moneyness=log_moneyness
343
+ )
344
+
345
+ return {
346
+ 'moneyness_grid': moneyness_grid,
347
+ 'iv_surface': iv_surface
348
+ }
349
+
332
350
  # -------------------------------------------------------------------------
333
351
  # Risk-Neutral Density (RND)
334
352
  # -------------------------------------------------------------------------
@@ -14,6 +14,7 @@ from voly.utils.logger import logger, catch_exception
14
14
  from voly.exceptions import VolyError
15
15
  from voly.models import SVIModel
16
16
  import warnings
17
+
17
18
  warnings.filterwarnings("ignore")
18
19
 
19
20
 
@@ -50,11 +51,11 @@ def calculate_residuals(params: List[float],
50
51
 
51
52
 
52
53
  @catch_exception
53
- def optimize_svi_parameters(market_data: pd.DataFrame,
54
- initial_params: Optional[List[float]] = None,
55
- param_bounds: Optional[Tuple] = None) -> Dict[str, Dict[str, Any]]:
54
+ def fit_svi_parameters(market_data: pd.DataFrame,
55
+ initial_params: Optional[List[float]] = None,
56
+ param_bounds: Optional[Tuple] = None) -> Tuple[pd.DataFrame, Dict[str, Dict]]:
56
57
  """
57
- Optimize SVI parameters for all unique expiries in the market data.
58
+ Fit SVI parameters for all unique expiries in the market data.
58
59
 
59
60
  Parameters:
60
61
  - market_data: DataFrame with market data
@@ -62,11 +63,8 @@ def optimize_svi_parameters(market_data: pd.DataFrame,
62
63
  - param_bounds: Bounds for parameters (default: from SVIModel)
63
64
 
64
65
  Returns:
65
- - Dictionary of optimization results by maturity name
66
+ - Tuple of (fit_performance DataFrame, params_dict)
66
67
  """
67
- results = {}
68
- unique_expiries = sorted(market_data['yte'].unique())
69
-
70
68
  # Use defaults if not provided
71
69
  if initial_params is None:
72
70
  initial_params = SVIModel.DEFAULT_INITIAL_PARAMS
@@ -74,37 +72,78 @@ def optimize_svi_parameters(market_data: pd.DataFrame,
74
72
  if param_bounds is None:
75
73
  param_bounds = SVIModel.DEFAULT_PARAM_BOUNDS
76
74
 
77
- for t_dte in unique_expiries:
75
+ # Initialize data for fit performance
76
+ fit_data = {
77
+ 'Maturity': [],
78
+ 'DTE': [],
79
+ 'YTE': [],
80
+ 'Success': [],
81
+ 'Cost': [],
82
+ 'Optimality': [],
83
+ 'RMSE': [],
84
+ 'MAE': [],
85
+ 'R²': [],
86
+ 'Max Error': [],
87
+ 'Number of Points': []
88
+ }
89
+
90
+ # Dictionary to store parameters
91
+ params_dict = {}
92
+
93
+ # Get unique expiries
94
+ unique_expiries = sorted(market_data['yte'].unique())
95
+
96
+ for yte in unique_expiries:
78
97
  # Get maturity name for reporting
79
- expiry_data = market_data[market_data['yte'] == t_dte]
98
+ expiry_data = market_data[market_data['yte'] == yte]
80
99
  maturity_name = expiry_data['maturity_name'].iloc[0]
81
100
  dte_value = expiry_data['dte'].iloc[0]
82
101
 
83
- logger.info(f"Optimizing for {maturity_name} (DTE: {dte_value:.1f}, YTE: {t_dte:.4f})...")
102
+ logger.info(f"Optimizing for {maturity_name} (DTE: {dte_value:.1f})...")
84
103
 
85
104
  # Optimize SVI parameters
86
105
  try:
87
106
  result = least_squares(
88
107
  calculate_residuals,
89
108
  initial_params,
90
- args=(t_dte, market_data, SVIModel),
109
+ args=(yte, market_data, SVIModel),
91
110
  bounds=param_bounds,
92
111
  max_nfev=1000
93
112
  )
94
113
  except Exception as e:
95
114
  raise VolyError(f"Optimization failed for {maturity_name}: {str(e)}")
96
115
 
97
- # Store results with maturity name as key
98
- results[maturity_name] = {
99
- 'params': result.x,
100
- 'success': result.success,
101
- 'cost': result.cost,
102
- 'optimality': result.optimality,
103
- 'message': result.message,
104
- 'yte': t_dte,
116
+ # Get parameters
117
+ params = result.x
118
+ params_dict[maturity_name] = {
119
+ 'params': params,
105
120
  'dte': dte_value
106
121
  }
107
122
 
123
+ # Calculate model predictions for statistics
124
+ w_svi = np.array([SVIModel.svi(x, *params) for x in expiry_data['log_moneyness']])
125
+ iv_model = np.sqrt(w_svi / yte)
126
+ iv_market = expiry_data['mark_iv'].values
127
+
128
+ # Calculate statistics
129
+ rmse = np.sqrt(mean_squared_error(iv_market, iv_model))
130
+ mae = mean_absolute_error(iv_market, iv_model)
131
+ r2 = r2_score(iv_market, iv_model)
132
+ max_error = np.max(np.abs(iv_market - iv_model))
133
+ num_points = len(expiry_data)
134
+
135
+ # Add to fit data
136
+ fit_data['Maturity'].append(maturity_name)
137
+ fit_data['DTE'].append(dte_value)
138
+ fit_data['Success'].append(result.success)
139
+ fit_data['Cost'].append(result.cost)
140
+ fit_data['Optimality'].append(result.optimality)
141
+ fit_data['RMSE'].append(rmse)
142
+ fit_data['MAE'].append(mae)
143
+ fit_data['R²'].append(r2)
144
+ fit_data['Max Error'].append(max_error)
145
+ fit_data['Number of Points'].append(num_points)
146
+
108
147
  if result.success:
109
148
  logger.info(f'Optimization for {maturity_name} (DTE: {dte_value:.1f}): SUCCESS')
110
149
  else:
@@ -112,17 +151,20 @@ def optimize_svi_parameters(market_data: pd.DataFrame,
112
151
 
113
152
  logger.info('------------------------------------------')
114
153
 
115
- return results
154
+ # Create DataFrame with all fit performance data
155
+ fit_performance = pd.DataFrame(fit_data)
156
+
157
+ return fit_performance, params_dict
116
158
 
117
159
 
118
160
  @catch_exception
119
- def create_parameters_matrix(optimization_results: Dict[str, Dict[str, Any]]) -> Tuple[pd.DataFrame, pd.DataFrame]:
161
+ def create_parameters_matrix(params_dict: Dict[str, Dict]) -> Tuple[pd.DataFrame, pd.DataFrame]:
120
162
  """
121
163
  Create matrices of optimized parameters for each expiry.
122
164
  Uses maturity names as column names.
123
165
 
124
166
  Parameters:
125
- - optimization_results: Dictionary of optimization results by maturity name
167
+ - params_dict: Dictionary of parameter results by maturity name
126
168
 
127
169
  Returns:
128
170
  - Tuple of DataFrames with optimized parameters:
@@ -130,8 +172,8 @@ def create_parameters_matrix(optimization_results: Dict[str, Dict[str, Any]]) ->
130
172
  2. Jump-Wing parameters (nu, psi, p, c, nu_tilde)
131
173
  """
132
174
  # Get maturity names in order by DTE
133
- maturity_names = sorted(optimization_results.keys(),
134
- key=lambda x: optimization_results[x]['dte'])
175
+ maturity_names = sorted(params_dict.keys(),
176
+ key=lambda x: params_dict[x]['dte'])
135
177
 
136
178
  # Create DataFrame for raw parameters with maturity names as columns
137
179
  raw_param_matrix = pd.DataFrame(
@@ -151,7 +193,7 @@ def create_parameters_matrix(optimization_results: Dict[str, Dict[str, Any]]) ->
151
193
 
152
194
  # Fill the matrices with optimized parameters
153
195
  for maturity_name in maturity_names:
154
- result = optimization_results[maturity_name]
196
+ result = params_dict[maturity_name]
155
197
 
156
198
  # Extract raw SVI parameters
157
199
  a, b, sigma, rho, m = result['params']
@@ -179,155 +221,71 @@ def create_parameters_matrix(optimization_results: Dict[str, Dict[str, Any]]) ->
179
221
 
180
222
 
181
223
  @catch_exception
182
- def generate_implied_volatility_surface(
183
- param_matrix: pd.DataFrame,
184
- moneyness_range: Tuple[float, float] = (-2, 2),
185
- num_points: int = 500
186
- ) -> Tuple[np.ndarray, Dict[float, np.ndarray]]:
187
- """
188
- Generate implied volatility surface using optimized SVI parameters.
189
-
190
- Parameters:
191
- - param_matrix: Matrix of optimized SVI parameters with maturity names as columns
192
- - moneyness_range: (min, max) range for moneyness grid
193
- - num_points: Number of points for moneyness grid
194
-
195
- Returns:
196
- - Moneyness grid and implied volatility surface
197
- """
198
- # Generate moneyness grid
199
- min_m, max_m = moneyness_range
200
- moneyness_values = np.linspace(min_m, max_m, num=num_points)
201
- implied_volatility_surface = {}
202
-
203
- # Get YTE values from the parameter matrix attributes
204
- yte_values = param_matrix.attrs['yte_values']
205
-
206
- # Generate implied volatility for each expiry
207
- for maturity_name, yte in yte_values.items():
208
- svi_params = param_matrix[maturity_name].values
209
- w_svi = [SVIModel.svi(x, *svi_params) for x in moneyness_values]
210
- implied_volatility_surface[yte] = np.sqrt(np.array(w_svi) / yte)
211
-
212
- return moneyness_values, implied_volatility_surface
213
-
214
-
215
- @catch_exception
216
- def calculate_fit_statistics(market_data: pd.DataFrame, param_matrix: pd.DataFrame) -> pd.DataFrame:
224
+ def fit_model(market_data: pd.DataFrame,
225
+ model_name: str = 'svi',
226
+ initial_params: Optional[List[float]] = None,
227
+ param_bounds: Optional[Tuple] = None) -> Dict[str, Any]:
217
228
  """
218
- Calculate fitting accuracy statistics for each expiry.
229
+ Fit a volatility model to market data.
219
230
 
220
231
  Parameters:
221
232
  - market_data: DataFrame with market data
222
- - param_matrix: Matrix of optimized SVI parameters with maturity names as columns
233
+ - model_name: Type of model to fit (default: 'svi')
234
+ - initial_params: Optional initial parameters for optimization (default: model's defaults)
235
+ - param_bounds: Optional parameter bounds for optimization (default: model's defaults)
223
236
 
224
237
  Returns:
225
- - DataFrame with fitting statistics
238
+ - Dictionary with fitting results
226
239
  """
227
- # Get YTE values from the parameter matrix attributes
228
- yte_values = param_matrix.attrs['yte_values']
229
- dte_values = param_matrix.attrs['dte_values']
230
-
231
- # Initialize lists for statistics
232
- maturity_name_list = []
233
- dte_list = []
234
- yte_list = []
235
- rmse_list = []
236
- mae_list = []
237
- r2_list = []
238
- max_error_list = []
239
- num_points_list = []
240
-
241
- # Calculate statistics for each expiry
242
- for maturity_name, yte in yte_values.items():
243
- # Filter market data for the specific expiry
244
- expiry_data = market_data[market_data['yte'] == yte]
245
- dte_value = dte_values[maturity_name]
246
-
247
- # Calculate SVI model predictions
248
- svi_params = param_matrix[maturity_name].values
249
- w_svi = np.array([SVIModel.svi(x, *svi_params) for x in expiry_data['log_moneyness']])
250
- iv_model = np.sqrt(w_svi / yte)
240
+ if model_name.lower() != 'svi':
241
+ raise VolyError(f"Model type '{model_name}' is not supported. Currently only 'svi' is available.")
251
242
 
252
- # Get actual market implied volatilities
253
- iv_market = expiry_data['mark_iv'].values
243
+ # Step 1: Fit model parameters and get performance metrics in one step
244
+ fit_performance, params_dict = fit_svi_parameters(
245
+ market_data,
246
+ initial_params=initial_params,
247
+ param_bounds=param_bounds
248
+ )
254
249
 
255
- # Calculate statistics
256
- rmse = np.sqrt(mean_squared_error(iv_market, iv_model))
257
- mae = mean_absolute_error(iv_market, iv_model)
258
- r2 = r2_score(iv_market, iv_model)
259
- max_error = np.max(np.abs(iv_market - iv_model))
260
- num_points = len(expiry_data)
250
+ # Step 2: Create parameter matrices
251
+ raw_param_matrix, jw_param_matrix = create_parameters_matrix(params_dict)
261
252
 
262
- # Append to lists
263
- maturity_name_list.append(maturity_name)
264
- dte_list.append(dte_value)
265
- yte_list.append(yte)
266
- rmse_list.append(rmse)
267
- mae_list.append(mae)
268
- r2_list.append(r2)
269
- max_error_list.append(max_error)
270
- num_points_list.append(num_points)
271
-
272
- # Create DataFrame with statistics
273
- stats_df = pd.DataFrame({
274
- 'Maturity': maturity_name_list,
275
- 'DTE': dte_list,
276
- 'YTE': yte_list,
277
- 'RMSE': rmse_list,
278
- 'MAE': mae_list,
279
- 'R²': r2_list,
280
- 'Max Error': max_error_list,
281
- 'Number of Points': num_points_list
282
- })
283
-
284
- return stats_df
253
+ return {
254
+ 'raw_param_matrix': raw_param_matrix,
255
+ 'jw_param_matrix': jw_param_matrix,
256
+ 'fit_performance': fit_performance,
257
+ }
285
258
 
286
259
 
287
260
  @catch_exception
288
- def fit_model(market_data: pd.DataFrame,
289
- model_name: str = 'svi',
290
- moneyness_range: Tuple[float, float] = (-2, 2),
291
- num_points: int = 500) -> Dict[str, Any]:
261
+ def get_surface(
262
+ param_matrix: pd.DataFrame,
263
+ log_moneyness: Tuple[float, float, int] = (-2, 2, 500)
264
+ ) -> Tuple[np.ndarray, Dict[float, np.ndarray], np.ndarray]:
292
265
  """
293
- Fit a volatility model to market data.
266
+ Generate implied volatility surface using optimized SVI parameters.
294
267
 
295
268
  Parameters:
296
- - market_data: DataFrame with market data
297
- - model_name: Type of model to fit (default: 'svi')
298
- - moneyness_range: (min, max) range for moneyness grid
299
- - num_points: Number of points for moneyness grid
269
+ - param_matrix: Matrix of optimized SVI parameters from fit_results
270
+ - log_moneyness: Tuple of (min, max, num_points) for the moneyness grid
300
271
 
301
272
  Returns:
302
- - Dictionary with fitting results
273
+ - Tuple of (moneyness_grid, iv_surface, unique_expiries)
303
274
  """
304
- if model_name.lower() != 'svi':
305
- raise VolyError(f"Model type '{model_name}' is not supported. Currently only 'svi' is available.")
306
-
307
- # Step 1: Optimize model parameters
308
- optimization_results = optimize_svi_parameters(market_data)
275
+ # Extract moneyness parameters
276
+ min_m, max_m, num_points = log_moneyness
309
277
 
310
- # Step 2: Create parameter matrices
311
- raw_param_matrix, jw_param_matrix = create_parameters_matrix(optimization_results)
312
-
313
- # Step 3: Generate implied volatility surface
314
- moneyness_grid, iv_surface = generate_implied_volatility_surface(
315
- raw_param_matrix, moneyness_range, num_points
316
- )
278
+ # Generate moneyness grid
279
+ moneyness_values = np.linspace(min_m, max_m, num=num_points)
280
+ implied_volatility_surface = {}
317
281
 
318
- # Step 4: Calculate fitting statistics
319
- stats_df = calculate_fit_statistics(market_data, raw_param_matrix)
282
+ # Get YTE values from the parameter matrix attributes
283
+ yte_values = param_matrix.attrs['yte_values']
320
284
 
321
- # Step 5: Get unique expiries in sorted order (in years)
322
- unique_expiries_years = np.array(sorted(market_data['yte'].unique()))
285
+ # Generate implied volatility for each expiry
286
+ for maturity_name, yte in yte_values.items():
287
+ svi_params = param_matrix[maturity_name].values
288
+ w_svi = [SVIModel.svi(x, *svi_params) for x in moneyness_values]
289
+ implied_volatility_surface[yte] = np.sqrt(np.array(w_svi) / yte)
323
290
 
324
- # Return all results in a dictionary
325
- return {
326
- 'optimization_results': optimization_results,
327
- 'raw_param_matrix': raw_param_matrix,
328
- 'jw_param_matrix': jw_param_matrix,
329
- 'moneyness_grid': moneyness_grid,
330
- 'iv_surface': iv_surface,
331
- 'stats_df': stats_df,
332
- 'unique_expiries': unique_expiries_years,
333
- }
291
+ return moneyness_values, implied_volatility_surface
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: voly
3
- Version: 0.0.12
3
+ Version: 0.0.13
4
4
  Summary: Options & volatility research package
5
5
  Author-email: Manu de Cara <manu.de.cara@gmail.com>
6
6
  License: MIT
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