voly 0.0.57__py3-none-any.whl → 0.0.59__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/core/charts.py CHANGED
@@ -13,9 +13,6 @@ from voly.models import SVIModel
13
13
  import plotly.graph_objects as go
14
14
  from plotly.subplots import make_subplots
15
15
  import plotly.io as pio
16
- import plotly.express as px
17
- from plotly.colors import hex_to_rgb, make_colorscale
18
-
19
16
 
20
17
  # Set default renderer to browser for interactive plots
21
18
  pio.renderers.default = "browser"
@@ -25,20 +22,8 @@ pio.renderers.default = "browser"
25
22
  def plot_volatility_smile(moneyness_array: np.ndarray,
26
23
  iv_array: np.ndarray,
27
24
  market_data: pd.DataFrame = None,
28
- maturity: Optional[float] = None,
29
- ) -> go.Figure:
30
- """
31
- Plot volatility smile for a single expiry.
32
-
33
- Parameters:
34
- - moneyness_array: Moneyness grid
35
- - iv_array: Implied volatility values
36
- - market_data: Optional market data for comparison
37
- - maturity: Maturity name for filtering market data
38
-
39
- Returns:
40
- - Plotly figure
41
- """
25
+ maturity: Optional[str] = None) -> go.Figure:
26
+ """Plot volatility smile for a single expiry."""
42
27
  fig = go.Figure()
43
28
 
44
29
  # Add model curve
@@ -52,289 +37,218 @@ def plot_volatility_smile(moneyness_array: np.ndarray,
52
37
  )
53
38
  )
54
39
 
55
- # Filter market data for the specific expiry
56
- maturity_data = market_data[market_data['maturity_name'] == maturity]
57
-
58
- if not maturity_data.empty:
59
- # Add bid IV
60
- fig.add_trace(
61
- go.Scatter(
62
- x=maturity_data['log_moneyness'],
63
- y=maturity_data['bid_iv'] * 100, # Convert to percentage
64
- mode='markers',
65
- name='Bid IV',
66
- marker=dict(size=8, symbol='circle', opacity=0.7)
40
+ # Add market data if provided
41
+ if market_data is not None and maturity is not None:
42
+ maturity_data = market_data[market_data['maturity_name'] == maturity]
43
+
44
+ if not maturity_data.empty:
45
+ # Add bid and ask IVs
46
+ for iv_type in ['bid_iv', 'ask_iv']:
47
+ if iv_type in maturity_data.columns:
48
+ fig.add_trace(
49
+ go.Scatter(
50
+ x=maturity_data['log_moneyness'],
51
+ y=maturity_data[iv_type] * 100, # Convert to percentage
52
+ mode='markers',
53
+ name=iv_type.replace('_', ' ').upper(),
54
+ marker=dict(size=8, symbol='circle', opacity=0.7)
55
+ )
56
+ )
57
+
58
+ dte_value = maturity_data['dtm'].iloc[0]
59
+
60
+ # Update layout
61
+ fig.update_layout(
62
+ title=f'Vol Smile for {maturity} (DTE: {dte_value:.1f})',
63
+ xaxis_title='Log Moneyness',
64
+ yaxis_title='Implied Volatility (%)',
65
+ template='plotly_dark',
66
+ legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)
67
67
  )
68
- )
69
-
70
- # Add ask IV
71
- fig.add_trace(
72
- go.Scatter(
73
- x=maturity_data['log_moneyness'],
74
- y=maturity_data['ask_iv'] * 100, # Convert to percentage
75
- mode='markers',
76
- name='Ask IV',
77
- marker=dict(size=8, symbol='circle', opacity=0.7)
78
- )
79
- )
80
-
81
- dte_value = maturity_data['dte'].iloc[0]
82
-
83
- # Update layout
84
- fig.update_layout(
85
- title=f'Vol Smile for {maturity} (DTE: {dte_value:.1f})',
86
- xaxis_title='Log Moneyness',
87
- yaxis_title='Implied Volatility (%)',
88
- template='plotly_dark',
89
- legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)
90
- )
91
68
 
92
69
  return fig
93
70
 
94
71
 
95
72
  @catch_exception
96
73
  def plot_all_smiles(moneyness_array: np.ndarray,
97
- iv_surface: Dict[float, np.ndarray],
74
+ iv_surface: Dict[str, np.ndarray],
98
75
  market_data: Optional[pd.DataFrame] = None) -> List[go.Figure]:
99
- """
100
- Plot volatility smiles for all expiries.
101
-
102
- Parameters:
103
- - moneyness: Moneyness grid
104
- - iv_surface: Dictionary mapping expiry times to IV arrays
105
- - market_data: Optional market data for comparison
106
-
107
- Returns:
108
- - List of Plotly figures
109
- """
110
- figures = []
111
-
112
- # Get maturities
113
- maturities = list(iv_surface.keys())
114
-
115
- # Create a figure for each expiry
116
- for maturity in maturities:
117
- fig = plot_volatility_smile(
76
+ """Plot volatility smiles for all expiries."""
77
+ return [
78
+ plot_volatility_smile(
118
79
  moneyness_array=moneyness_array,
119
80
  iv_array=iv_surface[maturity],
120
81
  market_data=market_data,
121
82
  maturity=maturity
122
83
  )
123
- figures.append(fig)
124
-
125
- return figures
84
+ for maturity in iv_surface.keys()
85
+ ]
126
86
 
127
87
 
128
88
  @catch_exception
129
- def plot_parameters(raw_param_matrix: pd.DataFrame,
130
- jw_param_matrix: Optional[pd.DataFrame] = None) -> Tuple[go.Figure, Optional[go.Figure]]:
131
- """
132
- Plot model parameters across different expiries.
133
-
134
- Parameters:
135
- - raw_param_matrix: Matrix of raw SVI parameters with maturity names as columns
136
- - jw_param_matrix: Optional matrix of Jump-Wing parameters
137
-
138
- Returns:
139
- - Tuple of Plotly figures (raw_params_fig, jw_params_fig)
140
- """
141
- # Plot raw SVI parameters
142
- param_names = raw_param_matrix.index
143
- raw_fig = make_subplots(rows=3, cols=2, subplot_titles=[f"Parameter {p}: {SVIModel.PARAM_DESCRIPTIONS.get(p, '')}"
144
- for p in param_names] + [''])
145
-
146
- # Get maturity names (columns) in order
147
- maturity_names = raw_param_matrix.columns
89
+ def plot_parameters(fit_results: pd.DataFrame) -> go.Figure:
90
+ """Plot raw SVI parameters across different expiries."""
91
+ # Select parameters to plot
92
+ param_names = ['a', 'b', 'sigma', 'rho', 'm']
148
93
 
149
- # Get YTE and DTE values from attributes
150
- yte_values = raw_param_matrix.attrs['yte_values']
151
- dte_values = raw_param_matrix.attrs['dte_values']
94
+ # Create subplots
95
+ fig = make_subplots(
96
+ rows=3, cols=2,
97
+ subplot_titles=[f"Parameter {p}: {SVIModel.PARAM_DESCRIPTIONS.get(p, '')}"
98
+ for p in param_names] + ['']
99
+ )
152
100
 
153
- # Create custom x-axis tick labels
154
- tick_labels = [f"{m} (DTE: {dte_values[m]:.1f}, YTE: {yte_values[m]:.4f})" for m in maturity_names]
101
+ # Get maturity names and prepare tick labels
102
+ maturity_names = fit_results.columns
103
+ tick_labels = [f"{m} (DTE: {fit_results.loc['dtm', m]:.1f})" for m in maturity_names]
155
104
 
156
105
  # Plot each parameter
157
106
  for i, param in enumerate(param_names):
158
- row = i // 2 + 1
159
- col = i % 2 + 1
107
+ row, col = (i // 2) + 1, (i % 2) + 1
160
108
 
161
- raw_fig.add_trace(
109
+ fig.add_trace(
162
110
  go.Scatter(
163
- x=list(range(len(maturity_names))), # Use indices for x-axis positioning
164
- y=raw_param_matrix.loc[param],
111
+ x=list(range(len(maturity_names))),
112
+ y=fit_results.loc[param],
165
113
  mode='lines+markers',
166
114
  name=param,
167
115
  line=dict(width=2),
168
116
  marker=dict(size=8),
169
- text=tick_labels, # Add hover text
117
+ text=tick_labels,
170
118
  hovertemplate="%{text}<br>%{y:.4f}"
171
119
  ),
172
120
  row=row, col=col
173
121
  )
174
122
 
175
- # Update x-axis for this subplot with custom tick labels
176
- raw_fig.update_xaxes(
123
+ # Set x-axis labels
124
+ fig.update_xaxes(
177
125
  tickvals=list(range(len(maturity_names))),
178
126
  ticktext=maturity_names,
179
127
  tickangle=45,
180
128
  row=row, col=col
181
129
  )
182
130
 
183
- # Update layout for raw parameters
184
- raw_fig.update_layout(
131
+ # Update layout
132
+ fig.update_layout(
185
133
  title='Raw SVI Parameters Across Expiries',
186
134
  template='plotly_dark',
187
135
  showlegend=False,
188
- height=800
189
136
  )
190
137
 
191
- # Plot Jump-Wing parameters if provided
192
- jw_fig = None
193
- if jw_param_matrix is not None:
194
- jw_param_names = jw_param_matrix.index
195
- jw_fig = make_subplots(rows=3, cols=2,
196
- subplot_titles=[f"Parameter {p}: {SVIModel.PARAM_DESCRIPTIONS.get(p, '')}"
197
- for p in jw_param_names] + [''])
198
-
199
- # Plot each JW parameter
200
- for i, param in enumerate(jw_param_names):
201
- row = i // 2 + 1
202
- col = i % 2 + 1
203
-
204
- jw_fig.add_trace(
205
- go.Scatter(
206
- x=list(range(len(maturity_names))), # Use indices for x-axis positioning
207
- y=jw_param_matrix.loc[param],
208
- mode='lines+markers',
209
- name=param,
210
- line=dict(width=2, color='rgb(0, 180, 180)'),
211
- marker=dict(size=8),
212
- text=tick_labels, # Add hover text
213
- hovertemplate="%{text}<br>%{y:.4f}"
214
- ),
215
- row=row, col=col
216
- )
138
+ return fig
217
139
 
218
- # Update x-axis for this subplot with custom tick labels
219
- jw_fig.update_xaxes(
220
- tickvals=list(range(len(maturity_names))),
221
- ticktext=maturity_names,
222
- tickangle=45,
223
- row=row, col=col
224
- )
225
140
 
226
- # Update layout for JW parameters
227
- jw_fig.update_layout(
228
- title='Jump-Wing Parameters Across Expiries',
229
- template='plotly_dark',
230
- showlegend=False,
231
- height=800
232
- )
141
+ @catch_exception
142
+ def plot_jw_parameters(fit_results: pd.DataFrame) -> go.Figure:
143
+ """Plot Jump-Wing parameters across different expiries."""
144
+ # Select parameters to plot
145
+ param_names = ['nu', 'psi', 'p', 'c', 'nu_tilde']
233
146
 
234
- return raw_fig, jw_fig
147
+ # Create subplots
148
+ fig = make_subplots(
149
+ rows=3, cols=2,
150
+ subplot_titles=[f"Parameter {p}: {SVIModel.PARAM_DESCRIPTIONS.get(p, '')}"
151
+ for p in param_names] + ['']
152
+ )
235
153
 
154
+ # Get maturity names and prepare tick labels
155
+ maturity_names = fit_results.columns
156
+ tick_labels = [f"{m} (DTE: {fit_results.loc['dtm', m]:.1f})" for m in maturity_names]
236
157
 
237
- @catch_exception
238
- def plot_fit_performance(fit_performance: pd.DataFrame) -> go.Figure:
239
- """
240
- Plot the fitting accuracy statistics.
158
+ # Plot each parameter
159
+ for i, param in enumerate(param_names):
160
+ row, col = (i // 2) + 1, (i % 2) + 1
241
161
 
242
- Parameters:
243
- - stats_df: DataFrame with fitting statistics
162
+ fig.add_trace(
163
+ go.Scatter(
164
+ x=list(range(len(maturity_names))),
165
+ y=fit_results.loc[param],
166
+ mode='lines+markers',
167
+ name=param,
168
+ line=dict(width=2, color='rgb(0, 180, 180)'),
169
+ marker=dict(size=8),
170
+ text=tick_labels,
171
+ hovertemplate="%{text}<br>%{y:.4f}"
172
+ ),
173
+ row=row, col=col
174
+ )
244
175
 
245
- Returns:
246
- - Plotly figure
247
- """
248
- fig = make_subplots(
249
- rows=2, cols=2,
250
- subplot_titles=['RMSE by Expiry', 'MAE by Expiry',
251
- 'R² by Expiry', 'Max Error by Expiry']
176
+ # Set x-axis labels
177
+ fig.update_xaxes(
178
+ tickvals=list(range(len(maturity_names))),
179
+ ticktext=maturity_names,
180
+ tickangle=45,
181
+ row=row, col=col
182
+ )
183
+
184
+ # Update layout
185
+ fig.update_layout(
186
+ title='Jump-Wing Parameters Across Expiries',
187
+ template='plotly_dark',
188
+ showlegend=False,
252
189
  )
253
190
 
254
- # Create custom tick labels with maturity name, DTE, and YTE
255
- tick_labels = [f"{m} (DTE: {d:.1f})" for m, d in
256
- zip(fit_performance['Maturity'], fit_performance['DTE'])]
191
+ return fig
257
192
 
258
- # Get x-axis values for plotting (use indices for positioning)
259
- x_indices = list(range(len(fit_performance)))
260
193
 
261
- # Plot RMSE
262
- fig.add_trace(
263
- go.Scatter(
264
- x=x_indices,
265
- y=fit_performance['RMSE'] * 100, # Convert to percentage
266
- mode='lines+markers',
267
- name='RMSE',
268
- line=dict(width=2),
269
- marker=dict(size=8),
270
- text=tick_labels # Add hover text
271
- ),
272
- row=1, col=1
194
+ @catch_exception
195
+ def plot_fit_performance(fit_performance: pd.DataFrame) -> go.Figure:
196
+ """Plot the fitting accuracy statistics."""
197
+ # Define metrics to plot
198
+ metrics = {
199
+ 'rmse': {'title': 'RMSE by Expiry', 'row': 1, 'col': 1, 'ylabel': 'RMSE (%)', 'scale': 100},
200
+ 'mae': {'title': 'MAE by Expiry', 'row': 1, 'col': 2, 'ylabel': 'MAE (%)', 'scale': 100},
201
+ 'r2': {'title': 'R² by Expiry', 'row': 2, 'col': 1, 'ylabel': 'R²', 'scale': 1},
202
+ 'max_error': {'title': 'Max Error by Expiry', 'row': 2, 'col': 2, 'ylabel': 'Max Error (%)', 'scale': 100}
203
+ }
204
+
205
+ # Create subplots
206
+ fig = make_subplots(
207
+ rows=2, cols=2,
208
+ subplot_titles=[metrics[m]['title'] for m in metrics]
273
209
  )
274
210
 
275
- # Plot MAE
276
- fig.add_trace(
277
- go.Scatter(
278
- x=x_indices,
279
- y=fit_performance['MAE'] * 100, # Convert to percentage
280
- mode='lines+markers',
281
- name='MAE',
282
- line=dict(width=2),
283
- marker=dict(size=8),
284
- text=tick_labels # Add hover text
285
- ),
286
- row=1, col=2
287
- )
211
+ # Get maturity names and create x-axis indices
212
+ maturity_names = fit_performance.columns
213
+ x_indices = list(range(len(maturity_names)))
288
214
 
289
- # Plot
290
- fig.add_trace(
291
- go.Scatter(
292
- x=x_indices,
293
- y=fit_performance['R²'],
294
- mode='lines+markers',
295
- name='R²',
296
- line=dict(width=2),
297
- marker=dict(size=8),
298
- text=tick_labels # Add hover text
299
- ),
300
- row=2, col=1
301
- )
215
+ # Create hover labels
216
+ hover_labels = [f"{m}" for m in maturity_names]
302
217
 
303
- # Plot Max Error
304
- fig.add_trace(
305
- go.Scatter(
306
- x=x_indices,
307
- y=fit_performance['Max Error'] * 100, # Convert to percentage
308
- mode='lines+markers',
309
- name='Max Error',
310
- line=dict(width=2),
311
- marker=dict(size=8),
312
- text=tick_labels # Add hover text
313
- ),
314
- row=2, col=2
315
- )
218
+ # Plot each metric
219
+ for metric, config in metrics.items():
220
+ fig.add_trace(
221
+ go.Scatter(
222
+ x=x_indices,
223
+ y=fit_performance.loc[metric] * config['scale'],
224
+ mode='lines+markers',
225
+ name=metric.upper(),
226
+ line=dict(width=2),
227
+ marker=dict(size=8),
228
+ text=hover_labels,
229
+ hovertemplate="%{text}<br>%{y:.4f}"
230
+ ),
231
+ row=config['row'], col=config['col']
232
+ )
233
+
234
+ # Update axes
235
+ fig.update_yaxes(title_text=config['ylabel'], row=config['row'], col=config['col'])
316
236
 
317
- # Update x-axis for all subplots with maturity names
237
+ # Set x-axis labels for all subplots
318
238
  for row in range(1, 3):
319
239
  for col in range(1, 3):
320
240
  fig.update_xaxes(
321
241
  tickvals=x_indices,
322
- ticktext=fit_performance['Maturity'],
242
+ ticktext=maturity_names,
323
243
  tickangle=45,
324
244
  row=row, col=col
325
245
  )
326
246
 
327
- # Update y-axis titles
328
- fig.update_yaxes(title_text='RMSE (%)', row=1, col=1)
329
- fig.update_yaxes(title_text='MAE (%)', row=1, col=2)
330
- fig.update_yaxes(title_text='R²', row=2, col=1)
331
- fig.update_yaxes(title_text='Max Error (%)', row=2, col=2)
332
-
333
247
  # Update layout
334
248
  fig.update_layout(
335
249
  title='Model Fitting Accuracy Statistics',
336
250
  template='plotly_dark',
337
- showlegend=False
251
+ showlegend=False,
338
252
  )
339
253
 
340
254
  return fig
@@ -342,35 +256,42 @@ def plot_fit_performance(fit_performance: pd.DataFrame) -> go.Figure:
342
256
 
343
257
  @catch_exception
344
258
  def plot_3d_surface(moneyness_array: np.ndarray,
345
- iv_surface: dict[float, np.ndarray]) -> go.Figure:
346
- """
347
- Plot 3D implied volatility surface.
259
+ iv_surface: Dict[str, np.ndarray],
260
+ fit_results: pd.DataFrame = None) -> go.Figure:
261
+ """Plot 3D implied volatility surface."""
262
+ # Define custom colorscale
263
+ custom_blue_scale = [[0, '#60AEFC'], [1, '#002040']]
264
+
265
+ # Get maturity names
266
+ maturity_names = list(iv_surface.keys())
267
+
268
+ # Get z-axis values (days to expiry)
269
+ if fit_results is not None:
270
+ # Use DTM values from fit_results
271
+ maturity_values = [fit_results.loc['dtm', name] for name in maturity_names]
272
+ else:
273
+ # Default to sequential values
274
+ maturity_values = list(range(len(maturity_names)))
275
+
276
+ # Create 3D surface data
277
+ z_array = np.array([iv_surface[m] * 100 for m in maturity_names]) # Convert to percentage
278
+ X, Y = np.meshgrid(moneyness_array, maturity_values)
348
279
 
349
- Parameters:
350
- - moneyness_array: grid of moneyness values
351
- - iv_surface: Dictionary mapping expiry times to IV arrays
352
-
353
- Returns:
354
- - Plotly figure
355
- """
356
- start_color = '#60AEFC'
357
- end_color = '#002040' # Darker blue
358
- custom_blue_scale = [[0, start_color], [1, end_color]]
359
- maturities = list(iv_surface.keys())
360
-
361
- # Convert implied volatility surface to array
362
- z_array = np.array([iv_surface[m] for m in maturities])
363
-
364
- # Create mesh grid
365
- X, Y = np.meshgrid(moneyness_array, maturities)
366
- Z = z_array * 100 # Convert to percentage
367
-
368
- # Create 3D surface plot with custom blue colorscale
369
- fig = go.Figure(data=[go.Surface(z=Z, x=X, y=Y, colorscale=custom_blue_scale)])
370
-
371
- # Add colorbar
372
- fig.update_traces(contours_z=dict(show=True, usecolormap=True,
373
- highlightcolor="#0080FF", project_z=True))
280
+ # Create figure
281
+ fig = go.Figure(data=[
282
+ go.Surface(
283
+ z=z_array,
284
+ x=X,
285
+ y=Y,
286
+ colorscale=custom_blue_scale,
287
+ contours_z=dict(
288
+ show=True,
289
+ usecolormap=True,
290
+ highlightcolor="#0080FF",
291
+ project_z=True
292
+ )
293
+ )
294
+ ])
374
295
 
375
296
  # Update layout
376
297
  fig.update_layout(
@@ -378,7 +299,7 @@ def plot_3d_surface(moneyness_array: np.ndarray,
378
299
  template='plotly_dark',
379
300
  scene=dict(
380
301
  xaxis_title='Log Moneyness',
381
- yaxis_title='Maturities',
302
+ yaxis_title='Days to Expiry',
382
303
  zaxis_title='Implied Volatility (%)'
383
304
  ),
384
305
  margin=dict(l=65, r=50, b=65, t=90)
voly/core/fit.py CHANGED
@@ -19,87 +19,55 @@ warnings.filterwarnings("ignore")
19
19
 
20
20
 
21
21
  @catch_exception
22
- def calculate_residuals(params: List[float],
23
- ytm: float,
24
- market_data: pd.DataFrame,
22
+ def calculate_residuals(params: List[float], ytm: float, market_data: pd.DataFrame,
25
23
  model: Any = SVIModel) -> np.ndarray:
26
- """
27
- Calculate the residuals between market and model implied volatilities.
28
-
29
- Parameters:
30
- - params: Model parameters (e.g., SVI parameters [a, b, sigma, rho, m])
31
- - ytm: The time to maturity in years
32
- - market_data: DataFrame with market data
33
- - model: Model class to use (default: SVIModel)
34
-
35
- Returns:
36
- - Array of residuals
37
- """
38
- # Filter market data for the specific time to maturity
24
+ """Calculate residuals between market and model implied volatilities."""
39
25
  maturity_data = market_data[market_data['ytm'] == ytm]
40
-
41
- # Calculate the total implied variance (w) using the model for filtered data
42
26
  w = np.array([model.svi(x, *params) for x in maturity_data['log_moneyness']])
43
-
44
- # Extract the actual market implied volatilities
45
27
  iv_actual = maturity_data['mark_iv'].values
46
-
47
- # Calculate residuals between market implied volatilities and model predictions
48
- residuals = iv_actual - np.sqrt(w / ytm)
49
-
50
- return residuals
28
+ return iv_actual - np.sqrt(w / ytm)
51
29
 
52
30
 
53
31
  @catch_exception
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]]:
32
+ def fit_model(market_data: pd.DataFrame,
33
+ model_name: str = 'svi',
34
+ initial_params: Optional[List[float]] = None,
35
+ param_bounds: Optional[Tuple] = None) -> Tuple[pd.DataFrame, pd.DataFrame]:
57
36
  """
58
- Fit SVI parameters for all unique expiries in the market data.
59
-
60
- Parameters:
61
- - market_data: DataFrame with market data
62
- - initial_params: Initial guess for SVI parameters (default: from SVIModel)
63
- - param_bounds: Bounds for parameters (default: from SVIModel)
37
+ Fit a volatility model to market data.
64
38
 
65
39
  Returns:
66
- - Tuple of (fit_performance DataFrame, params_dict)
40
+ - Tuple of (fit_results_df, fit_performance_df)
67
41
  """
42
+ if model_name.lower() != 'svi':
43
+ raise VolyError(f"Model type '{model_name}' is not supported. Currently only 'svi' is available.")
44
+
68
45
  # Use defaults if not provided
69
- if initial_params is None:
70
- initial_params = SVIModel.DEFAULT_INITIAL_PARAMS
71
-
72
- if param_bounds is None:
73
- param_bounds = SVIModel.DEFAULT_PARAM_BOUNDS
74
-
75
- # Initialize data for fit performance
76
- fit_data = {
77
- 'maturity_name': [],
78
- 'dtm': [],
79
- 'ytm': [],
80
- 'fit_success': [],
81
- 'cost': [],
82
- 'optimality': [],
83
- 'rmse': [],
84
- 'mae': [],
85
- 'r2': [],
86
- 'max_error': [],
87
- 'n_points': []
88
- }
89
-
90
- # Dictionary to store parameters
91
- params_dict = {}
46
+ initial_params = initial_params or SVIModel.DEFAULT_INITIAL_PARAMS
47
+ param_bounds = param_bounds or SVIModel.DEFAULT_PARAM_BOUNDS
92
48
 
93
- # ANSI color codes for terminal output
94
- GREEN = '\033[32m'
95
- RED = '\033[31m'
96
- RESET = '\033[0m'
49
+ # Define indices for result DataFrames
50
+ results_index = ['s', 'u', 'maturity_date', 'dtm', 'ytm',
51
+ 'a', 'b', 'sigma', 'rho', 'm',
52
+ 'nu', 'psi', 'p', 'c', 'nu_tilde',
53
+ 'oi', 'volume', 'r']
54
+
55
+ performance_index = ['fit_success', 'cost', 'optimality',
56
+ 'rmse', 'mae', 'r2', 'max_error', 'n_points']
57
+
58
+ # Get unique maturities and sort them
59
+ unique_ytms = sorted(market_data['ytm'].unique())
60
+ maturity_names = [market_data[market_data['ytm'] == ytm]['maturity_name'].iloc[0] for ytm in unique_ytms]
61
+
62
+ # Create empty DataFrames
63
+ fit_results_df = pd.DataFrame(index=results_index, columns=maturity_names)
64
+ fit_performance_df = pd.DataFrame(index=performance_index, columns=maturity_names)
97
65
 
98
- # Get unique expiries
99
- unique_maturities = sorted(market_data['ytm'].unique())
66
+ # ANSI color codes for terminal output
67
+ GREEN, RED, RESET = '\033[32m', '\033[31m', '\033[0m'
100
68
 
101
- for ytm in unique_maturities:
102
- # Get maturity name for reporting
69
+ for ytm in unique_ytms:
70
+ # Get data for this maturity
103
71
  maturity_data = market_data[market_data['ytm'] == ytm]
104
72
  maturity_name = maturity_data['maturity_name'].iloc[0]
105
73
  dtm = maturity_data['dtm'].iloc[0]
@@ -118,16 +86,11 @@ def fit_svi_parameters(market_data: pd.DataFrame,
118
86
  except Exception as e:
119
87
  raise VolyError(f"Optimization failed for {maturity_name}: {str(e)}")
120
88
 
121
- # Get parameters
122
- params = result.x
123
- params_dict[maturity_name] = {
124
- 'params': params,
125
- 'dtm': dtm,
126
- 'ytm': ytm
127
- }
89
+ # Extract raw parameters
90
+ a, b, sigma, rho, m = result.x
128
91
 
129
92
  # Calculate model predictions for statistics
130
- w = np.array([SVIModel.svi(x, *params) for x in maturity_data['log_moneyness']])
93
+ w = np.array([SVIModel.svi(x, *result.x) for x in maturity_data['log_moneyness']])
131
94
  iv_model = np.sqrt(w / ytm)
132
95
  iv_market = maturity_data['mark_iv'].values
133
96
 
@@ -136,165 +99,67 @@ def fit_svi_parameters(market_data: pd.DataFrame,
136
99
  mae = mean_absolute_error(iv_market, iv_model)
137
100
  r2 = r2_score(iv_market, iv_model)
138
101
  max_error = np.max(np.abs(iv_market - iv_model))
139
- num_points = len(maturity_data)
140
-
141
- # Add to fit data
142
- fit_data['maturity_name'].append(maturity_name)
143
- fit_data['dtm'].append(dtm)
144
- fit_data['ytm'].append(ytm)
145
- fit_data['fit_success'].append(result.success)
146
- fit_data['cost'].append(result.cost)
147
- fit_data['optimality'].append(result.optimality)
148
- fit_data['rmse'].append(rmse)
149
- fit_data['mae'].append(mae)
150
- fit_data['r2'].append(r2)
151
- fit_data['max_error'].append(max_error)
152
- fit_data['n_points'].append(num_points)
153
-
154
- if result.success:
155
- logger.info(f'Optimization for {maturity_name}: {GREEN}SUCCESS{RESET}')
156
- else:
157
- logger.warning(f'Optimization for {maturity_name}: {RED}FAILED{RESET}')
158
-
159
- logger.info('-------------------------------------')
160
-
161
- # Create DataFrame with all fit performance data
162
- fit_performance = pd.DataFrame(fit_data)
163
102
 
164
- return fit_performance, params_dict
103
+ # Get or calculate additional required data
104
+ s = maturity_data['index_price'].iloc[0]
105
+ u = maturity_data['underlying_price'].iloc[0]
165
106
 
107
+ # Aggregate open interest and volume
108
+ oi = maturity_data['open_interest'].sum() if 'open_interest' in maturity_data.columns else 0
109
+ volume = maturity_data['volume'].sum() if 'volume' in maturity_data.columns else 0
110
+ r = 0.0
166
111
 
167
- @catch_exception
168
- def create_parameters_matrix(params_dict: Dict[str, Dict]) -> Tuple[pd.DataFrame, pd.DataFrame]:
169
- """
170
- Create matrices of optimized parameters for each maturity.
171
- Uses maturity names as column names.
172
-
173
- Parameters:
174
- - params_dict: Dictionary of raw parameter results by maturity name
175
-
176
- Returns:
177
- - Tuple of DataFrames with optimized parameters:
178
- 1. Raw SVI parameters (a, b, sigma, rho, m)
179
- 2. Jump-Wing parameters (nu, psi, p, c, nu_tilde)
180
- """
181
- # Get maturity names in order by dtm
182
- maturity_names = sorted(params_dict.keys(),
183
- key=lambda x: params_dict[x]['dtm'])
184
-
185
- # Create DataFrame for raw parameters
186
- raw_params_matrix = pd.DataFrame(
187
- columns=maturity_names,
188
- index=SVIModel.PARAM_NAMES
189
- )
190
-
191
- # Create DataFrame for JW parameters
192
- jw_params_matrix = pd.DataFrame(
193
- columns=maturity_names,
194
- index=SVIModel.JW_PARAM_NAMES
195
- )
196
-
197
- # Store YTM and DTM values for reference
198
- ytm_values = {}
199
- dtm_values = {}
200
-
201
- # Fill the matrices with optimized parameters
202
- for maturity_name in maturity_names:
203
- result = params_dict[maturity_name]
204
-
205
- # Extract raw SVI parameters
206
- a, b, sigma, rho, m = result['params']
207
- raw_params_matrix[maturity_name] = [a, b, sigma, rho, m]
208
-
209
- # Get time to maturity
210
- ytm = result['ytm']
211
- ytm_values[maturity_name] = ytm
212
- dtm_values[maturity_name] = result['dtm']
213
-
214
- # Calculate JW parameters
112
+ # Calculate Jump-Wing parameters
215
113
  nu, psi, p, c, nu_tilde = SVIModel.raw_to_jw_params(a, b, sigma, rho, m, ytm)
216
- jw_params_matrix[maturity_name] = [nu, psi, p, c, nu_tilde]
217
-
218
- # Store YTE and DTE as attributes in all DataFrames for reference
219
- attrs = {
220
- 'ytm_values': ytm_values,
221
- 'dtm_values': dtm_values
222
- }
223
114
 
224
- raw_params_matrix.attrs.update(attrs)
225
- jw_params_matrix.attrs.update(attrs)
115
+ # Store results
116
+ result_values = [s, u, maturity_data['maturity_date'].iloc[0], dtm, ytm,
117
+ a, b, rho, m, sigma, nu, psi, p, c, nu_tilde, oi, volume, r]
226
118
 
227
- return raw_params_matrix, jw_params_matrix
119
+ for idx, val in zip(results_index, result_values):
120
+ fit_results_df.loc[idx, maturity_name] = val
228
121
 
122
+ # Store performance metrics
123
+ perf_values = [result.success, result.cost, result.optimality,
124
+ rmse, mae, r2, max_error, len(maturity_data)]
229
125
 
230
- @catch_exception
231
- def fit_model(market_data: pd.DataFrame,
232
- model_name: str = 'svi',
233
- initial_params: Optional[List[float]] = None,
234
- param_bounds: Optional[Tuple] = None) -> Dict[str, Any]:
235
- """
236
- Fit a volatility model to market data.
237
-
238
- Parameters:
239
- - market_data: DataFrame with market data
240
- - model_name: Type of model to fit (default: 'svi')
241
- - initial_params: Optional initial parameters for optimization (default: model's defaults)
242
- - param_bounds: Optional parameter bounds for optimization (default: model's defaults)
126
+ for idx, val in zip(performance_index, perf_values):
127
+ fit_performance_df.loc[idx, maturity_name] = val
243
128
 
244
- Returns:
245
- - Dictionary with fitting results
246
- """
247
- if model_name.lower() != 'svi':
248
- raise VolyError(f"Model type '{model_name}' is not supported. Currently only 'svi' is available.")
249
-
250
- # Step 1: Fit model parameters and get performance metrics in one step
251
- fit_performance, params_dict = fit_svi_parameters(
252
- market_data,
253
- initial_params=initial_params,
254
- param_bounds=param_bounds
255
- )
256
-
257
- # Step 2: Create parameter matrices
258
- raw_params_matrix, jw_params_matrix = create_parameters_matrix(params_dict)
129
+ # Log result
130
+ status = f'{GREEN}SUCCESS{RESET}' if result.success else f'{RED}FAILED{RESET}'
131
+ logger.info(f'Optimization for {maturity_name}: {status}')
132
+ logger.info('-------------------------------------')
259
133
 
260
- return {
261
- 'raw_params_matrix': raw_params_matrix,
262
- 'jw_params_matrix': jw_params_matrix,
263
- 'fit_performance': fit_performance,
264
- }
134
+ return fit_results_df, fit_performance_df
265
135
 
266
136
 
267
137
  @catch_exception
268
- def get_iv_surface(fit_results: Dict[str, Any],
138
+ def get_iv_surface(fit_results: pd.DataFrame,
269
139
  log_moneyness_params: Tuple[float, float, int] = (-2, 2, 500)
270
- ) -> Dict[str, Any]:
271
- """
272
- Generate implied volatility surface using optimized SVI parameters.
273
-
274
- Parameters:
275
- - fit_results: results from fit_model()
276
- - log_moneyness_params: Tuple of (min, max, num_points) for the moneyness grid
277
-
278
- Returns:
279
- - x_domain, iv_surface
280
- """
281
- iv_surface = {}
282
-
140
+ ) -> Tuple[np.ndarray, Dict[str, np.ndarray]]:
141
+ """Generate implied volatility surface using optimized SVI parameters."""
283
142
  # Extract moneyness parameters
284
143
  min_m, max_m, num_points = log_moneyness_params
285
144
 
286
- # Generate moneyness array
287
- log_moneyness_array = np.linspace(min_m, max_m, num=num_points)
288
-
289
- # Get YTM values from the parameter matrix attributes
290
- ytm_values = fit_results['fit_performance']['ytm']
291
- maturity_values = fit_results['fit_performance']['maturity_name']
292
- raw_params_matrix = fit_results['raw_params_matrix']
145
+ # Generate moneyness array and initialize surface dictionary
146
+ moneyness_array = np.linspace(min_m, max_m, num=num_points)
147
+ iv_surface = {}
293
148
 
294
149
  # Generate implied volatility for each maturity
295
- for maturity, ytm in zip(maturity_values, ytm_values):
296
- svi_params = raw_params_matrix[maturity].values
297
- w_svi = [SVIModel.svi(x, *svi_params) for x in log_moneyness_array]
298
- iv_surface[maturity] = np.sqrt(np.array(w_svi) / ytm)
150
+ for maturity in fit_results.columns:
151
+ # Extract parameters and YTM
152
+ params = [
153
+ fit_results.loc['a', maturity],
154
+ fit_results.loc['b', maturity],
155
+ fit_results.loc['sigma', maturity],
156
+ fit_results.loc['rho', maturity],
157
+ fit_results.loc['m', maturity]
158
+ ]
159
+ ytm = fit_results.loc['ytm', maturity]
160
+
161
+ # Calculate SVI total implied variance and convert to IV
162
+ w_svi = np.array([SVIModel.svi(x, *params) for x in moneyness_array])
163
+ iv_surface[maturity] = np.sqrt(w_svi / ytm)
299
164
 
300
165
  return moneyness_array, iv_surface
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: voly
3
- Version: 0.0.57
3
+ Version: 0.0.59
4
4
  Summary: Options & volatility research package
5
5
  Author-email: Manu de Cara <manu.de.cara@gmail.com>
6
6
  License: MIT
@@ -4,15 +4,15 @@ voly/exceptions.py,sha256=PBsbn1vNMvKcCJwwJ4lBO6glD85jo1h2qiEmD7ArAjs,92
4
4
  voly/formulas.py,sha256=Xgaq4lx1fNzRfu9W84fMNeH6GRJ0FNFNUUUYn5ffjjE,8843
5
5
  voly/models.py,sha256=LXXIlpXZQEfXTuCngxC8Hd3bWtw6wdXDCSGxTLmHM-c,3659
6
6
  voly/core/__init__.py,sha256=bu6fS2I1Pj9fPPnl-zY3L7NqrZSY5Zy6NY2uMUvdhKs,183
7
- voly/core/charts.py,sha256=T8cogkiHj8NcFxfARumNvAjLJUAxsMlHUOVgshwjye8,26474
7
+ voly/core/charts.py,sha256=kWjd3sfvTalfuElt7vSuRPtYuWpMoJ9-L2Z9Mj-mOMQ,24374
8
8
  voly/core/data.py,sha256=e8qBArubNqPkrfuIYB_q2WhRf7TKzA4Z3FhMC-xyLEE,8862
9
- voly/core/fit.py,sha256=aCF8ZWmGKCJN720hqz6JbfYq1NNFzEOfi3IP-fUCXX4,9894
9
+ voly/core/fit.py,sha256=YClTiODlH9CBAqv5VNBlE0k6qD22Hwic91PnTWstyj4,6490
10
10
  voly/core/interpolate.py,sha256=ztVIePJZOh-CIbn69wkh1JW2rKywNe2FEewRN0zcSAo,8185
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.57.dist-info/LICENSE,sha256=wcHIVbE12jfcBOai_wqBKY6xvNQU5E909xL1zZNq_2Q,1065
15
- voly-0.0.57.dist-info/METADATA,sha256=G-_VF5HjhYn9Wu8cWP_YVMwZ_IyldPq6CygB0DqPGsA,4092
16
- voly-0.0.57.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
17
- voly-0.0.57.dist-info/top_level.txt,sha256=ZfLw2sSxF-LrKAkgGjOmeTcw6_gD-30zvtdEY5W4B7c,5
18
- voly-0.0.57.dist-info/RECORD,,
14
+ voly-0.0.59.dist-info/LICENSE,sha256=wcHIVbE12jfcBOai_wqBKY6xvNQU5E909xL1zZNq_2Q,1065
15
+ voly-0.0.59.dist-info/METADATA,sha256=z6kwXaTKNBU4imtpUyRVsWGcNdZyajgwMjXxfUe2DmY,4092
16
+ voly-0.0.59.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
17
+ voly-0.0.59.dist-info/top_level.txt,sha256=ZfLw2sSxF-LrKAkgGjOmeTcw6_gD-30zvtdEY5W4B7c,5
18
+ voly-0.0.59.dist-info/RECORD,,
File without changes
File without changes