voly 0.0.58__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 +179 -258
- voly/core/fit.py +80 -215
- {voly-0.0.58.dist-info → voly-0.0.59.dist-info}/METADATA +1 -1
- {voly-0.0.58.dist-info → voly-0.0.59.dist-info}/RECORD +7 -7
- {voly-0.0.58.dist-info → voly-0.0.59.dist-info}/LICENSE +0 -0
- {voly-0.0.58.dist-info → voly-0.0.59.dist-info}/WHEEL +0 -0
- {voly-0.0.58.dist-info → voly-0.0.59.dist-info}/top_level.txt +0 -0
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[
|
|
29
|
-
|
|
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
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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[
|
|
74
|
+
iv_surface: Dict[str, np.ndarray],
|
|
98
75
|
market_data: Optional[pd.DataFrame] = None) -> List[go.Figure]:
|
|
99
|
-
"""
|
|
100
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
return figures
|
|
84
|
+
for maturity in iv_surface.keys()
|
|
85
|
+
]
|
|
126
86
|
|
|
127
87
|
|
|
128
88
|
@catch_exception
|
|
129
|
-
def plot_parameters(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
#
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
#
|
|
154
|
-
|
|
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
|
-
|
|
109
|
+
fig.add_trace(
|
|
162
110
|
go.Scatter(
|
|
163
|
-
x=list(range(len(maturity_names))),
|
|
164
|
-
y=
|
|
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,
|
|
117
|
+
text=tick_labels,
|
|
170
118
|
hovertemplate="%{text}<br>%{y:.4f}"
|
|
171
119
|
),
|
|
172
120
|
row=row, col=col
|
|
173
121
|
)
|
|
174
122
|
|
|
175
|
-
#
|
|
176
|
-
|
|
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
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
#
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
#
|
|
290
|
-
|
|
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
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
#
|
|
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=
|
|
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:
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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='
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
#
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
#
|
|
99
|
-
|
|
66
|
+
# ANSI color codes for terminal output
|
|
67
|
+
GREEN, RED, RESET = '\033[32m', '\033[31m', '\033[0m'
|
|
100
68
|
|
|
101
|
-
for ytm in
|
|
102
|
-
# Get
|
|
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
|
-
#
|
|
122
|
-
|
|
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, *
|
|
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).T
|
|
163
102
|
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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:
|
|
138
|
+
def get_iv_surface(fit_results: pd.DataFrame,
|
|
269
139
|
log_moneyness_params: Tuple[float, float, int] = (-2, 2, 500)
|
|
270
|
-
) -> Dict[str,
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
# Get YTM values from the parameter matrix attributes
|
|
290
|
-
ytm_values = fit_results['fit_performance'].loc['ytm'].values
|
|
291
|
-
maturity_values = fit_results['fit_performance'].loc['maturity_name'].values
|
|
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
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
@@ -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=
|
|
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=
|
|
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.
|
|
15
|
-
voly-0.0.
|
|
16
|
-
voly-0.0.
|
|
17
|
-
voly-0.0.
|
|
18
|
-
voly-0.0.
|
|
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
|
|
File without changes
|