voly 0.0.65__py3-none-any.whl → 0.0.67__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- voly/client.py +2 -5
- voly/core/charts.py +194 -63
- voly/core/fit.py +47 -43
- voly/formulas.py +1 -3
- {voly-0.0.65.dist-info → voly-0.0.67.dist-info}/METADATA +1 -1
- {voly-0.0.65.dist-info → voly-0.0.67.dist-info}/RECORD +9 -9
- {voly-0.0.65.dist-info → voly-0.0.67.dist-info}/LICENSE +0 -0
- {voly-0.0.65.dist-info → voly-0.0.67.dist-info}/WHEEL +0 -0
- {voly-0.0.65.dist-info → voly-0.0.67.dist-info}/top_level.txt +0 -0
voly/client.py
CHANGED
|
@@ -338,17 +338,14 @@ class VolyClient:
|
|
|
338
338
|
logger.info(f"Fitting {model_name.upper()} model to market data")
|
|
339
339
|
|
|
340
340
|
# Fit the model
|
|
341
|
-
fit_results
|
|
341
|
+
fit_results = fit_model(
|
|
342
342
|
option_chain=option_chain,
|
|
343
343
|
model_name=model_name,
|
|
344
344
|
initial_params=initial_params,
|
|
345
345
|
param_bounds=param_bounds
|
|
346
346
|
)
|
|
347
347
|
|
|
348
|
-
return
|
|
349
|
-
'fit_results': fit_results,
|
|
350
|
-
'fit_performance': fit_performance
|
|
351
|
-
}
|
|
348
|
+
return fit_results
|
|
352
349
|
|
|
353
350
|
@staticmethod
|
|
354
351
|
def get_iv_surface(fit_results: Dict[str, Any],
|
voly/core/charts.py
CHANGED
|
@@ -19,17 +19,38 @@ pio.renderers.default = "browser"
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
@catch_exception
|
|
22
|
-
def plot_volatility_smile(
|
|
22
|
+
def plot_volatility_smile(x_domain: np.ndarray,
|
|
23
23
|
iv_array: np.ndarray,
|
|
24
24
|
option_chain: pd.DataFrame = None,
|
|
25
|
-
maturity: Optional[str] = None
|
|
26
|
-
|
|
25
|
+
maturity: Optional[str] = None,
|
|
26
|
+
domain_type: str = 'log_moneyness') -> go.Figure:
|
|
27
|
+
"""
|
|
28
|
+
Plot volatility smile for a single expiry.
|
|
29
|
+
|
|
30
|
+
Parameters:
|
|
31
|
+
- x_domain: Array of x-axis values (log_moneyness, moneyness, strikes, delta)
|
|
32
|
+
- iv_array: Implied volatility values
|
|
33
|
+
- option_chain: Optional market data for comparison
|
|
34
|
+
- maturity: Maturity name for filtering market data
|
|
35
|
+
- domain_type: Type of x-domain ('log_moneyness', 'moneyness', 'strikes', 'delta')
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
- Plotly figure
|
|
39
|
+
"""
|
|
27
40
|
fig = go.Figure()
|
|
28
41
|
|
|
42
|
+
# Map domain types to axis labels
|
|
43
|
+
domain_labels = {
|
|
44
|
+
'log_moneyness': 'Log Moneyness',
|
|
45
|
+
'moneyness': 'Moneyness (S/K)',
|
|
46
|
+
'strikes': 'Strike Price',
|
|
47
|
+
'delta': 'Call Delta'
|
|
48
|
+
}
|
|
49
|
+
|
|
29
50
|
# Add model curve
|
|
30
51
|
fig.add_trace(
|
|
31
52
|
go.Scatter(
|
|
32
|
-
x=
|
|
53
|
+
x=x_domain,
|
|
33
54
|
y=iv_array * 100, # Convert to percentage
|
|
34
55
|
mode='lines',
|
|
35
56
|
name='Model',
|
|
@@ -42,25 +63,39 @@ def plot_volatility_smile(moneyness_array: np.ndarray,
|
|
|
42
63
|
maturity_data = option_chain[option_chain['maturity_name'] == maturity]
|
|
43
64
|
|
|
44
65
|
if not maturity_data.empty:
|
|
45
|
-
#
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
# For market data, use log_moneyness by default as x-axis
|
|
67
|
+
market_x = maturity_data['log_moneyness']
|
|
68
|
+
|
|
69
|
+
# If domain is not log_moneyness, convert market data to match the domain
|
|
70
|
+
if domain_type == 'moneyness':
|
|
71
|
+
market_x = np.exp(market_x)
|
|
72
|
+
elif domain_type == 'strikes':
|
|
73
|
+
s = maturity_data['underlying_price'].iloc[0]
|
|
74
|
+
market_x = s / np.exp(market_x)
|
|
75
|
+
elif domain_type == 'delta':
|
|
76
|
+
# For delta, we'd need more complex conversion - only show model curve
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
# Add bid and ask IVs if the domain type allows
|
|
80
|
+
if domain_type != 'delta':
|
|
81
|
+
for iv_type in ['bid_iv', 'ask_iv']:
|
|
82
|
+
if iv_type in maturity_data.columns:
|
|
83
|
+
fig.add_trace(
|
|
84
|
+
go.Scatter(
|
|
85
|
+
x=market_x,
|
|
86
|
+
y=maturity_data[iv_type] * 100, # Convert to percentage
|
|
87
|
+
mode='markers',
|
|
88
|
+
name=iv_type.replace('_', ' ').upper(),
|
|
89
|
+
marker=dict(size=8, symbol='circle', opacity=0.7)
|
|
90
|
+
)
|
|
55
91
|
)
|
|
56
|
-
)
|
|
57
92
|
|
|
58
93
|
dte_value = maturity_data['dtm'].iloc[0]
|
|
59
94
|
|
|
60
95
|
# Update layout
|
|
61
96
|
fig.update_layout(
|
|
62
97
|
title=f'Vol Smile for {maturity} (DTE: {dte_value:.1f})',
|
|
63
|
-
xaxis_title='
|
|
98
|
+
xaxis_title=domain_labels.get(domain_type, 'X Domain'),
|
|
64
99
|
yaxis_title='Implied Volatility (%)',
|
|
65
100
|
template='plotly_dark',
|
|
66
101
|
legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)
|
|
@@ -70,16 +105,29 @@ def plot_volatility_smile(moneyness_array: np.ndarray,
|
|
|
70
105
|
|
|
71
106
|
|
|
72
107
|
@catch_exception
|
|
73
|
-
def plot_all_smiles(
|
|
108
|
+
def plot_all_smiles(x_surface: Dict[str, np.ndarray],
|
|
74
109
|
iv_surface: Dict[str, np.ndarray],
|
|
75
|
-
option_chain: Optional[pd.DataFrame] = None
|
|
76
|
-
|
|
110
|
+
option_chain: Optional[pd.DataFrame] = None,
|
|
111
|
+
domain_type: str = 'log_moneyness') -> List[go.Figure]:
|
|
112
|
+
"""
|
|
113
|
+
Plot volatility smiles for all expiries.
|
|
114
|
+
|
|
115
|
+
Parameters:
|
|
116
|
+
- x_surface: Dictionary mapping maturity names to x-domain arrays
|
|
117
|
+
- iv_surface: Dictionary mapping maturity names to IV arrays
|
|
118
|
+
- option_chain: Optional market data for comparison
|
|
119
|
+
- domain_type: Type of x-domain ('log_moneyness', 'moneyness', 'strikes', 'delta')
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
- List of Plotly figures
|
|
123
|
+
"""
|
|
77
124
|
return [
|
|
78
125
|
plot_volatility_smile(
|
|
79
|
-
|
|
126
|
+
x_domain=x_surface[maturity],
|
|
80
127
|
iv_array=iv_surface[maturity],
|
|
81
128
|
option_chain=option_chain,
|
|
82
|
-
maturity=maturity
|
|
129
|
+
maturity=maturity,
|
|
130
|
+
domain_type=domain_type
|
|
83
131
|
)
|
|
84
132
|
for maturity in iv_surface.keys()
|
|
85
133
|
]
|
|
@@ -87,7 +135,15 @@ def plot_all_smiles(moneyness_array: np.ndarray,
|
|
|
87
135
|
|
|
88
136
|
@catch_exception
|
|
89
137
|
def plot_parameters(fit_results: pd.DataFrame) -> go.Figure:
|
|
90
|
-
"""
|
|
138
|
+
"""
|
|
139
|
+
Plot raw SVI parameters across different expiries.
|
|
140
|
+
|
|
141
|
+
Parameters:
|
|
142
|
+
- fit_results: DataFrame from fit_model() with maturity names as index
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
- Plotly figure
|
|
146
|
+
"""
|
|
91
147
|
# Select parameters to plot
|
|
92
148
|
param_names = ['a', 'b', 'sigma', 'rho', 'm']
|
|
93
149
|
|
|
@@ -98,9 +154,12 @@ def plot_parameters(fit_results: pd.DataFrame) -> go.Figure:
|
|
|
98
154
|
for p in param_names] + ['']
|
|
99
155
|
)
|
|
100
156
|
|
|
101
|
-
# Get maturity names
|
|
102
|
-
maturity_names = fit_results.
|
|
103
|
-
|
|
157
|
+
# Get maturity names from index
|
|
158
|
+
maturity_names = fit_results.index
|
|
159
|
+
|
|
160
|
+
# Create hover text with maturity info
|
|
161
|
+
tick_labels = [f"{m} (DTE: {fit_results.loc[m, 'dtm']:.1f}, YTE: {fit_results.loc[m, 'ytm']:.4f})"
|
|
162
|
+
for m in maturity_names]
|
|
104
163
|
|
|
105
164
|
# Plot each parameter
|
|
106
165
|
for i, param in enumerate(param_names):
|
|
@@ -109,7 +168,7 @@ def plot_parameters(fit_results: pd.DataFrame) -> go.Figure:
|
|
|
109
168
|
fig.add_trace(
|
|
110
169
|
go.Scatter(
|
|
111
170
|
x=list(range(len(maturity_names))),
|
|
112
|
-
y=fit_results
|
|
171
|
+
y=fit_results[param],
|
|
113
172
|
mode='lines+markers',
|
|
114
173
|
name=param,
|
|
115
174
|
line=dict(width=2),
|
|
@@ -132,7 +191,7 @@ def plot_parameters(fit_results: pd.DataFrame) -> go.Figure:
|
|
|
132
191
|
fig.update_layout(
|
|
133
192
|
title='Raw SVI Parameters Across Expiries',
|
|
134
193
|
template='plotly_dark',
|
|
135
|
-
showlegend=False
|
|
194
|
+
showlegend=False
|
|
136
195
|
)
|
|
137
196
|
|
|
138
197
|
return fig
|
|
@@ -140,7 +199,15 @@ def plot_parameters(fit_results: pd.DataFrame) -> go.Figure:
|
|
|
140
199
|
|
|
141
200
|
@catch_exception
|
|
142
201
|
def plot_jw_parameters(fit_results: pd.DataFrame) -> go.Figure:
|
|
143
|
-
"""
|
|
202
|
+
"""
|
|
203
|
+
Plot Jump-Wing parameters across different expiries.
|
|
204
|
+
|
|
205
|
+
Parameters:
|
|
206
|
+
- fit_results: DataFrame from fit_model() with maturity names as index
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
- Plotly figure
|
|
210
|
+
"""
|
|
144
211
|
# Select parameters to plot
|
|
145
212
|
param_names = ['nu', 'psi', 'p', 'c', 'nu_tilde']
|
|
146
213
|
|
|
@@ -151,9 +218,12 @@ def plot_jw_parameters(fit_results: pd.DataFrame) -> go.Figure:
|
|
|
151
218
|
for p in param_names] + ['']
|
|
152
219
|
)
|
|
153
220
|
|
|
154
|
-
# Get maturity names
|
|
155
|
-
maturity_names = fit_results.
|
|
156
|
-
|
|
221
|
+
# Get maturity names from index
|
|
222
|
+
maturity_names = fit_results.index
|
|
223
|
+
|
|
224
|
+
# Create hover text with maturity info
|
|
225
|
+
tick_labels = [f"{m} (DTE: {fit_results.loc[m, 'dtm']:.1f})"
|
|
226
|
+
for m in maturity_names]
|
|
157
227
|
|
|
158
228
|
# Plot each parameter
|
|
159
229
|
for i, param in enumerate(param_names):
|
|
@@ -162,7 +232,7 @@ def plot_jw_parameters(fit_results: pd.DataFrame) -> go.Figure:
|
|
|
162
232
|
fig.add_trace(
|
|
163
233
|
go.Scatter(
|
|
164
234
|
x=list(range(len(maturity_names))),
|
|
165
|
-
y=fit_results
|
|
235
|
+
y=fit_results[param],
|
|
166
236
|
mode='lines+markers',
|
|
167
237
|
name=param,
|
|
168
238
|
line=dict(width=2, color='rgb(0, 180, 180)'),
|
|
@@ -185,15 +255,23 @@ def plot_jw_parameters(fit_results: pd.DataFrame) -> go.Figure:
|
|
|
185
255
|
fig.update_layout(
|
|
186
256
|
title='Jump-Wing Parameters Across Expiries',
|
|
187
257
|
template='plotly_dark',
|
|
188
|
-
showlegend=False
|
|
258
|
+
showlegend=False
|
|
189
259
|
)
|
|
190
260
|
|
|
191
261
|
return fig
|
|
192
262
|
|
|
193
263
|
|
|
194
264
|
@catch_exception
|
|
195
|
-
def plot_fit_performance(
|
|
196
|
-
"""
|
|
265
|
+
def plot_fit_performance(fit_results: pd.DataFrame) -> go.Figure:
|
|
266
|
+
"""
|
|
267
|
+
Plot the fitting accuracy statistics.
|
|
268
|
+
|
|
269
|
+
Parameters:
|
|
270
|
+
- fit_results: DataFrame from fit_model() with maturity names as index
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
- Plotly figure
|
|
274
|
+
"""
|
|
197
275
|
# Define metrics to plot
|
|
198
276
|
metrics = {
|
|
199
277
|
'rmse': {'title': 'RMSE by Expiry', 'row': 1, 'col': 1, 'ylabel': 'RMSE (%)', 'scale': 100},
|
|
@@ -208,19 +286,19 @@ def plot_fit_performance(fit_performance: pd.DataFrame) -> go.Figure:
|
|
|
208
286
|
subplot_titles=[metrics[m]['title'] for m in metrics]
|
|
209
287
|
)
|
|
210
288
|
|
|
211
|
-
# Get maturity names and create x-axis indices
|
|
212
|
-
maturity_names =
|
|
289
|
+
# Get maturity names from index and create x-axis indices
|
|
290
|
+
maturity_names = fit_results.index
|
|
213
291
|
x_indices = list(range(len(maturity_names)))
|
|
214
292
|
|
|
215
293
|
# Create hover labels
|
|
216
|
-
hover_labels = [f"{m}" for m in maturity_names]
|
|
294
|
+
hover_labels = [f"{m} (DTE: {fit_results.loc[m, 'dtm']:.1f})" for m in maturity_names]
|
|
217
295
|
|
|
218
296
|
# Plot each metric
|
|
219
297
|
for metric, config in metrics.items():
|
|
220
298
|
fig.add_trace(
|
|
221
299
|
go.Scatter(
|
|
222
300
|
x=x_indices,
|
|
223
|
-
y=
|
|
301
|
+
y=fit_results[metric] * config['scale'],
|
|
224
302
|
mode='lines+markers',
|
|
225
303
|
name=metric.upper(),
|
|
226
304
|
line=dict(width=2),
|
|
@@ -248,17 +326,37 @@ def plot_fit_performance(fit_performance: pd.DataFrame) -> go.Figure:
|
|
|
248
326
|
fig.update_layout(
|
|
249
327
|
title='Model Fitting Accuracy Statistics',
|
|
250
328
|
template='plotly_dark',
|
|
251
|
-
showlegend=False
|
|
329
|
+
showlegend=False
|
|
252
330
|
)
|
|
253
331
|
|
|
254
332
|
return fig
|
|
255
333
|
|
|
256
334
|
|
|
257
335
|
@catch_exception
|
|
258
|
-
def plot_3d_surface(
|
|
336
|
+
def plot_3d_surface(x_surface: Dict[str, np.ndarray],
|
|
259
337
|
iv_surface: Dict[str, np.ndarray],
|
|
260
|
-
fit_results: pd.DataFrame = None
|
|
261
|
-
|
|
338
|
+
fit_results: pd.DataFrame = None,
|
|
339
|
+
domain_type: str = 'log_moneyness') -> go.Figure:
|
|
340
|
+
"""
|
|
341
|
+
Plot 3D implied volatility surface.
|
|
342
|
+
|
|
343
|
+
Parameters:
|
|
344
|
+
- x_surface: Dictionary mapping maturity names to x-domain arrays
|
|
345
|
+
- iv_surface: Dictionary mapping maturity names to IV arrays
|
|
346
|
+
- fit_results: Optional DataFrame with maturity information
|
|
347
|
+
- domain_type: Type of x-domain ('log_moneyness', 'moneyness', 'strikes', 'delta')
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
- Plotly figure
|
|
351
|
+
"""
|
|
352
|
+
# Map domain types to axis labels
|
|
353
|
+
domain_labels = {
|
|
354
|
+
'log_moneyness': 'Log Moneyness',
|
|
355
|
+
'moneyness': 'Moneyness (S/K)',
|
|
356
|
+
'strikes': 'Strike Price',
|
|
357
|
+
'delta': 'Delta'
|
|
358
|
+
}
|
|
359
|
+
|
|
262
360
|
# Define custom colorscale
|
|
263
361
|
custom_blue_scale = [[0, '#60AEFC'], [1, '#002040']]
|
|
264
362
|
|
|
@@ -268,39 +366,72 @@ def plot_3d_surface(moneyness_array: np.ndarray,
|
|
|
268
366
|
# Get z-axis values (days to expiry)
|
|
269
367
|
if fit_results is not None:
|
|
270
368
|
# Use DTM values from fit_results
|
|
271
|
-
maturity_values = [fit_results.loc['dtm'
|
|
369
|
+
maturity_values = [fit_results.loc[name, 'dtm'] for name in maturity_names]
|
|
272
370
|
else:
|
|
273
371
|
# Default to sequential values
|
|
274
372
|
maturity_values = list(range(len(maturity_names)))
|
|
275
373
|
|
|
276
|
-
# Create
|
|
277
|
-
|
|
278
|
-
|
|
374
|
+
# Create a mesh grid for each maturity to handle different x domains
|
|
375
|
+
X = []
|
|
376
|
+
Y = []
|
|
377
|
+
Z = []
|
|
279
378
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
379
|
+
for i, m in enumerate(maturity_names):
|
|
380
|
+
x_values = x_surface[m]
|
|
381
|
+
z_values = iv_surface[m] * 100 # Convert to percentage
|
|
382
|
+
y_value = maturity_values[i]
|
|
383
|
+
|
|
384
|
+
# Add to point lists
|
|
385
|
+
for j in range(len(x_values)):
|
|
386
|
+
X.append(x_values[j])
|
|
387
|
+
Y.append(y_value)
|
|
388
|
+
Z.append(z_values[j])
|
|
389
|
+
|
|
390
|
+
# Create 3D scatter plot with lines connecting points within each maturity
|
|
391
|
+
fig = go.Figure()
|
|
392
|
+
|
|
393
|
+
# Add data as a 3D scatter plot
|
|
394
|
+
fig.add_trace(go.Scatter3d(
|
|
395
|
+
x=X, y=Y, z=Z,
|
|
396
|
+
mode='markers',
|
|
397
|
+
marker=dict(
|
|
398
|
+
size=3,
|
|
399
|
+
color=Z,
|
|
286
400
|
colorscale=custom_blue_scale,
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
401
|
+
opacity=0.8
|
|
402
|
+
),
|
|
403
|
+
hovertemplate="<b>%{y:.1f} days</b><br>X: %{x:.4f}<br>IV: %{z:.2f}%"
|
|
404
|
+
))
|
|
405
|
+
|
|
406
|
+
# Add lines connecting points for each maturity
|
|
407
|
+
cumulative_index = 0
|
|
408
|
+
for i, m in enumerate(maturity_names):
|
|
409
|
+
points_count = len(x_surface[m])
|
|
410
|
+
indices = list(range(cumulative_index, cumulative_index + points_count))
|
|
411
|
+
|
|
412
|
+
if len(indices) > 1:
|
|
413
|
+
fig.add_trace(go.Scatter3d(
|
|
414
|
+
x=[X[j] for j in indices],
|
|
415
|
+
y=[Y[j] for j in indices],
|
|
416
|
+
z=[Z[j] for j in indices],
|
|
417
|
+
mode='lines',
|
|
418
|
+
line=dict(color='blue', width=3),
|
|
419
|
+
showlegend=False,
|
|
420
|
+
hoverinfo='none'
|
|
421
|
+
))
|
|
422
|
+
|
|
423
|
+
cumulative_index += points_count
|
|
295
424
|
|
|
296
425
|
# Update layout
|
|
297
426
|
fig.update_layout(
|
|
298
427
|
title='Implied Volatility 3D Surface',
|
|
299
428
|
template='plotly_dark',
|
|
300
429
|
scene=dict(
|
|
301
|
-
xaxis_title='
|
|
430
|
+
xaxis_title=domain_labels.get(domain_type, 'X Domain'),
|
|
302
431
|
yaxis_title='Days to Expiry',
|
|
303
|
-
zaxis_title='Implied Volatility (%)'
|
|
432
|
+
zaxis_title='Implied Volatility (%)',
|
|
433
|
+
aspectmode='manual',
|
|
434
|
+
aspectratio=dict(x=1.5, y=1, z=1)
|
|
304
435
|
),
|
|
305
436
|
margin=dict(l=65, r=50, b=65, t=90)
|
|
306
437
|
)
|
voly/core/fit.py
CHANGED
|
@@ -33,12 +33,18 @@ def calculate_residuals(params: List[float], ytm: float, option_chain: pd.DataFr
|
|
|
33
33
|
def fit_model(option_chain: pd.DataFrame,
|
|
34
34
|
model_name: str = 'svi',
|
|
35
35
|
initial_params: Optional[List[float]] = None,
|
|
36
|
-
param_bounds: Optional[Tuple] = None) ->
|
|
36
|
+
param_bounds: Optional[Tuple] = None) -> pd.DataFrame:
|
|
37
37
|
"""
|
|
38
38
|
Fit a volatility model to market data.
|
|
39
39
|
|
|
40
|
+
Parameters:
|
|
41
|
+
- option_chain: DataFrame with market data
|
|
42
|
+
- model_name: Type of model to fit (default: 'svi')
|
|
43
|
+
- initial_params: Optional initial parameters for optimization (default: model's defaults)
|
|
44
|
+
- param_bounds: Optional parameter bounds for optimization (default: model's defaults)
|
|
45
|
+
|
|
40
46
|
Returns:
|
|
41
|
-
-
|
|
47
|
+
- DataFrame with all fit results and performance metrics as columns, maturity_names as index
|
|
42
48
|
"""
|
|
43
49
|
if model_name.lower() != 'svi':
|
|
44
50
|
raise VolyError(f"Model type '{model_name}' is not supported. Currently only 'svi' is available.")
|
|
@@ -47,22 +53,22 @@ def fit_model(option_chain: pd.DataFrame,
|
|
|
47
53
|
initial_params = initial_params or SVIModel.DEFAULT_INITIAL_PARAMS
|
|
48
54
|
param_bounds = param_bounds or SVIModel.DEFAULT_PARAM_BOUNDS
|
|
49
55
|
|
|
50
|
-
# Define
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
# Define column names for the result DataFrame
|
|
57
|
+
result_columns = [
|
|
58
|
+
's', 'u', 'maturity_date', 'dtm', 'ytm',
|
|
59
|
+
'a', 'b', 'rho', 'm', 'sigma',
|
|
60
|
+
'nu', 'psi', 'p', 'c', 'nu_tilde',
|
|
61
|
+
'oi', 'volume', 'r',
|
|
62
|
+
'fit_success', 'cost', 'optimality',
|
|
63
|
+
'rmse', 'mae', 'r2', 'max_error', 'n_points'
|
|
64
|
+
]
|
|
58
65
|
|
|
59
66
|
# Get unique maturities and sort them
|
|
60
67
|
unique_ytms = sorted(option_chain['ytm'].unique())
|
|
61
68
|
maturity_names = [option_chain[option_chain['ytm'] == ytm]['maturity_name'].iloc[0] for ytm in unique_ytms]
|
|
62
69
|
|
|
63
|
-
# Create empty
|
|
64
|
-
|
|
65
|
-
fit_performance_df = pd.DataFrame(index=performance_index, columns=maturity_names)
|
|
70
|
+
# Create empty DataFrame with maturity_names as index
|
|
71
|
+
results_df = pd.DataFrame(index=maturity_names, columns=result_columns)
|
|
66
72
|
|
|
67
73
|
# ANSI color codes for terminal output
|
|
68
74
|
GREEN, RED, RESET = '\033[32m', '\033[31m', '\033[0m'
|
|
@@ -108,42 +114,38 @@ def fit_model(option_chain: pd.DataFrame,
|
|
|
108
114
|
# Aggregate open interest and volume
|
|
109
115
|
oi = maturity_data['open_interest'].sum() if 'open_interest' in maturity_data.columns else 0
|
|
110
116
|
volume = maturity_data['volume'].sum() if 'volume' in maturity_data.columns else 0
|
|
111
|
-
r = 0.0
|
|
117
|
+
r = maturity_data['interest_rate'].iloc[0] if 'interest_rate' in maturity_data.columns else 0
|
|
112
118
|
|
|
113
119
|
# Calculate Jump-Wing parameters
|
|
114
120
|
nu, psi, p, c, nu_tilde = SVIModel.raw_to_jw_params(a, b, sigma, rho, m, ytm)
|
|
115
121
|
|
|
116
|
-
# Store results
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
rmse, mae, r2, max_error, len(maturity_data)]
|
|
126
|
-
|
|
127
|
-
for idx, val in zip(performance_index, perf_values):
|
|
128
|
-
fit_performance_df.loc[idx, maturity_name] = val
|
|
122
|
+
# Store all results in the DataFrame
|
|
123
|
+
results_df.loc[maturity_name] = [
|
|
124
|
+
s, u, maturity_data['maturity_date'].iloc[0], dtm, ytm,
|
|
125
|
+
a, b, rho, m, sigma,
|
|
126
|
+
nu, psi, p, c, nu_tilde,
|
|
127
|
+
oi, volume, r,
|
|
128
|
+
result.success, result.cost, result.optimality,
|
|
129
|
+
rmse, mae, r2, max_error, len(maturity_data)
|
|
130
|
+
]
|
|
129
131
|
|
|
130
132
|
# Log result
|
|
131
133
|
status = f'{GREEN}SUCCESS{RESET}' if result.success else f'{RED}FAILED{RESET}'
|
|
132
134
|
logger.info(f'Optimization for {maturity_name}: {status}')
|
|
133
135
|
logger.info('-------------------------------------')
|
|
134
136
|
|
|
135
|
-
return
|
|
137
|
+
return results_df
|
|
136
138
|
|
|
137
139
|
|
|
138
140
|
@catch_exception
|
|
139
141
|
def get_iv_surface(fit_results: pd.DataFrame,
|
|
140
142
|
log_moneyness_params: Tuple[float, float, int] = (-1.5, 1.5, 1000),
|
|
141
|
-
return_domain: str = 'log_moneyness') -> Tuple[Dict[str, np.ndarray], np.ndarray]:
|
|
143
|
+
return_domain: str = 'log_moneyness') -> Tuple[Dict[str, np.ndarray], Dict[str, np.ndarray]]:
|
|
142
144
|
"""
|
|
143
145
|
Generate implied volatility surface using optimized SVI parameters.
|
|
144
146
|
|
|
145
147
|
Parameters:
|
|
146
|
-
- fit_results: DataFrame from fit_model()
|
|
148
|
+
- fit_results: DataFrame from fit_model() with maturity names as index
|
|
147
149
|
- log_moneyness_params: Tuple of (min, max, num_points) for the moneyness grid
|
|
148
150
|
- return_domain: Domain for x-axis values ('log_moneyness', 'moneyness', 'strikes', 'delta')
|
|
149
151
|
|
|
@@ -159,30 +161,32 @@ def get_iv_surface(fit_results: pd.DataFrame,
|
|
|
159
161
|
|
|
160
162
|
iv_surface = {}
|
|
161
163
|
x_surface = {}
|
|
162
|
-
|
|
164
|
+
|
|
165
|
+
# Process each maturity
|
|
166
|
+
for maturity in fit_results.index:
|
|
163
167
|
# Calculate SVI total implied variance and convert to IV
|
|
164
168
|
params = [
|
|
165
|
-
fit_results.loc['a'
|
|
166
|
-
fit_results.loc['b'
|
|
167
|
-
fit_results.loc['sigma'
|
|
168
|
-
fit_results.loc['rho'
|
|
169
|
-
fit_results.loc['m'
|
|
169
|
+
fit_results.loc[maturity, 'a'],
|
|
170
|
+
fit_results.loc[maturity, 'b'],
|
|
171
|
+
fit_results.loc[maturity, 'sigma'],
|
|
172
|
+
fit_results.loc[maturity, 'rho'],
|
|
173
|
+
fit_results.loc[maturity, 'm']
|
|
170
174
|
]
|
|
171
|
-
ytm = fit_results.loc['ytm'
|
|
175
|
+
ytm = fit_results.loc[maturity, 'ytm']
|
|
172
176
|
|
|
177
|
+
# Calculate implied volatility
|
|
173
178
|
w_svi = np.array([SVIModel.svi(x, *params) for x in log_moneyness_array])
|
|
174
179
|
iv_array = np.sqrt(w_svi / ytm)
|
|
175
180
|
iv_surface[maturity] = iv_array
|
|
176
181
|
|
|
177
|
-
|
|
182
|
+
# Calculate x domain for this maturity
|
|
183
|
+
x_surface[maturity] = get_x_domain(
|
|
178
184
|
log_moneyness_params=log_moneyness_params,
|
|
179
185
|
return_domain=return_domain,
|
|
180
|
-
s=fit_results.loc['s'
|
|
181
|
-
r=fit_results.loc['r'
|
|
186
|
+
s=fit_results.loc[maturity, 's'],
|
|
187
|
+
r=fit_results.loc[maturity, 'r'],
|
|
182
188
|
iv_array=iv_array,
|
|
183
|
-
ytm=
|
|
189
|
+
ytm=ytm
|
|
184
190
|
)
|
|
185
191
|
|
|
186
|
-
x_surface[maturity] = x_domain
|
|
187
|
-
|
|
188
192
|
return iv_surface, x_surface
|
voly/formulas.py
CHANGED
|
@@ -316,10 +316,8 @@ def get_x_domain(log_moneyness_params: Tuple[float, float, int] = (-1.5, 1.5, 10
|
|
|
316
316
|
If required parameters are missing for the specified return_domain.
|
|
317
317
|
"""
|
|
318
318
|
|
|
319
|
-
# Extract log-moneyness parameters
|
|
319
|
+
# Extract log-moneyness parameters and generate array
|
|
320
320
|
min_m, max_m, num_points = log_moneyness_params
|
|
321
|
-
|
|
322
|
-
# Generate the base log-moneyness array
|
|
323
321
|
log_moneyness = np.linspace(min_m, max_m, num_points)
|
|
324
322
|
|
|
325
323
|
# Handle different return domains
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
voly/__init__.py,sha256=8xyDk7rFCn_MOD5hxuv5cxxKZvBVRiSIM7TgaMPpwpw,211
|
|
2
|
-
voly/client.py,sha256=
|
|
2
|
+
voly/client.py,sha256=fXcS856ZYyJrhHjxUhGnth4N4BQbR-ExMYd25CTpR64,20539
|
|
3
3
|
voly/exceptions.py,sha256=PBsbn1vNMvKcCJwwJ4lBO6glD85jo1h2qiEmD7ArAjs,92
|
|
4
|
-
voly/formulas.py,sha256=
|
|
4
|
+
voly/formulas.py,sha256=wSbGAH6GuQThT9QyQY4Ud2eUf9fo1YFHglUmP6fNris,11871
|
|
5
5
|
voly/models.py,sha256=LXXIlpXZQEfXTuCngxC8Hd3bWtw6wdXDCSGxTLmHM-c,3659
|
|
6
6
|
voly/core/__init__.py,sha256=bu6fS2I1Pj9fPPnl-zY3L7NqrZSY5Zy6NY2uMUvdhKs,183
|
|
7
|
-
voly/core/charts.py,sha256=
|
|
7
|
+
voly/core/charts.py,sha256=rsAkVddQVwAwCle9NQ-_zDO-29U7gPF_Zx8n4OjK-X8,28467
|
|
8
8
|
voly/core/data.py,sha256=e8qBArubNqPkrfuIYB_q2WhRf7TKzA4Z3FhMC-xyLEE,8862
|
|
9
|
-
voly/core/fit.py,sha256=
|
|
9
|
+
voly/core/fit.py,sha256=ATTycxY7im4QYAuj7djXgx0TBSBNy-M5EBMw9rwaQTI,7455
|
|
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.67.dist-info/LICENSE,sha256=wcHIVbE12jfcBOai_wqBKY6xvNQU5E909xL1zZNq_2Q,1065
|
|
15
|
+
voly-0.0.67.dist-info/METADATA,sha256=MDh6ieKzi95-nmuNfgbqUwIOJORAJvdsRm68lrut9aU,4092
|
|
16
|
+
voly-0.0.67.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
|
17
|
+
voly-0.0.67.dist-info/top_level.txt,sha256=ZfLw2sSxF-LrKAkgGjOmeTcw6_gD-30zvtdEY5W4B7c,5
|
|
18
|
+
voly-0.0.67.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|