voly 0.0.69__tar.gz → 0.0.70__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {voly-0.0.69/src/voly.egg-info → voly-0.0.70}/PKG-INFO +1 -1
- {voly-0.0.69 → voly-0.0.70}/pyproject.toml +2 -2
- {voly-0.0.69 → voly-0.0.70}/src/voly/client.py +28 -19
- voly-0.0.70/src/voly/core/charts.py +520 -0
- {voly-0.0.69 → voly-0.0.70/src/voly.egg-info}/PKG-INFO +1 -1
- voly-0.0.69/src/voly/core/charts.py +0 -948
- {voly-0.0.69 → voly-0.0.70}/LICENSE +0 -0
- {voly-0.0.69 → voly-0.0.70}/README.md +0 -0
- {voly-0.0.69 → voly-0.0.70}/setup.cfg +0 -0
- {voly-0.0.69 → voly-0.0.70}/setup.py +0 -0
- {voly-0.0.69 → voly-0.0.70}/src/voly/__init__.py +0 -0
- {voly-0.0.69 → voly-0.0.70}/src/voly/core/__init__.py +0 -0
- {voly-0.0.69 → voly-0.0.70}/src/voly/core/data.py +0 -0
- {voly-0.0.69 → voly-0.0.70}/src/voly/core/fit.py +0 -0
- {voly-0.0.69 → voly-0.0.70}/src/voly/core/interpolate.py +0 -0
- {voly-0.0.69 → voly-0.0.70}/src/voly/core/rnd.py +0 -0
- {voly-0.0.69 → voly-0.0.70}/src/voly/exceptions.py +0 -0
- {voly-0.0.69 → voly-0.0.70}/src/voly/formulas.py +0 -0
- {voly-0.0.69 → voly-0.0.70}/src/voly/models.py +0 -0
- {voly-0.0.69 → voly-0.0.70}/src/voly/utils/__init__.py +0 -0
- {voly-0.0.69 → voly-0.0.70}/src/voly/utils/logger.py +0 -0
- {voly-0.0.69 → voly-0.0.70}/src/voly.egg-info/SOURCES.txt +0 -0
- {voly-0.0.69 → voly-0.0.70}/src/voly.egg-info/dependency_links.txt +0 -0
- {voly-0.0.69 → voly-0.0.70}/src/voly.egg-info/requires.txt +0 -0
- {voly-0.0.69 → voly-0.0.70}/src/voly.egg-info/top_level.txt +0 -0
- {voly-0.0.69 → voly-0.0.70}/tests/test_client.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "voly"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.70"
|
|
8
8
|
description = "Options & volatility research package"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -60,7 +60,7 @@ line_length = 100
|
|
|
60
60
|
multi_line_output = 3
|
|
61
61
|
|
|
62
62
|
[tool.mypy]
|
|
63
|
-
python_version = "0.0.
|
|
63
|
+
python_version = "0.0.70"
|
|
64
64
|
warn_return_any = true
|
|
65
65
|
warn_unused_configs = true
|
|
66
66
|
disallow_untyped_defs = true
|
|
@@ -373,41 +373,50 @@ class VolyClient:
|
|
|
373
373
|
return {'iv_surface': iv_surface, 'x_surface': x_surface}
|
|
374
374
|
|
|
375
375
|
@staticmethod
|
|
376
|
-
def plot_model(fit_results:
|
|
376
|
+
def plot_model(fit_results: pd.DataFrame,
|
|
377
377
|
option_chain: pd.DataFrame = None,
|
|
378
|
-
|
|
379
|
-
|
|
378
|
+
log_moneyness_params: Tuple[float, float, int] = (-2, 2, 500),
|
|
379
|
+
return_domain: str = 'log_moneyness',
|
|
380
|
+
) -> Dict[str, Any]:
|
|
380
381
|
"""
|
|
381
|
-
Generate all plots for the fitted model
|
|
382
|
+
Generate all plots for the fitted model.
|
|
382
383
|
|
|
383
384
|
Parameters:
|
|
384
|
-
- fit_results:
|
|
385
|
+
- fit_results: DataFrame with fitting results from fit_model()
|
|
385
386
|
- option_chain: Optional market data for comparison
|
|
386
|
-
-
|
|
387
|
+
- log_moneyness_params: Grid of log-moneyness values
|
|
388
|
+
- return_domain: Domain for x-axis values ('log_moneyness', 'moneyness', 'strikes', 'delta')
|
|
387
389
|
|
|
388
390
|
Returns:
|
|
389
391
|
- Dictionary of plot figures
|
|
390
392
|
"""
|
|
391
393
|
plots = {}
|
|
392
394
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
# Extract data from fit results
|
|
396
|
-
raw_param_matrix = fit_results['raw_param_matrix']
|
|
397
|
-
jw_param_matrix = fit_results['jw_param_matrix']
|
|
398
|
-
fit_performance = fit_results['fit_performance']
|
|
395
|
+
# Generate IV surface and domain
|
|
396
|
+
iv_surface, x_surface = get_iv_surface(fit_results, log_moneyness_params, return_domain)
|
|
399
397
|
|
|
400
398
|
# Plot volatility smiles
|
|
401
|
-
plots['smiles'] = plot_all_smiles(
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
399
|
+
plots['smiles'] = plot_all_smiles(
|
|
400
|
+
x_surface=x_surface,
|
|
401
|
+
iv_surface=iv_surface,
|
|
402
|
+
option_chain=option_chain,
|
|
403
|
+
domain_type=return_domain
|
|
404
|
+
)
|
|
405
405
|
|
|
406
406
|
# Plot parameters
|
|
407
|
-
plots['raw_params']
|
|
407
|
+
plots['raw_params'] = plot_parameters(fit_results)
|
|
408
|
+
plots['jw_params'] = plot_jw_parameters(fit_results)
|
|
409
|
+
|
|
410
|
+
# Plot fit statistics
|
|
411
|
+
plots['fit_performance'] = plot_fit_performance(fit_results)
|
|
408
412
|
|
|
409
|
-
# Plot
|
|
410
|
-
plots['
|
|
413
|
+
# Plot 3D surface
|
|
414
|
+
plots['surface_3d'] = plot_3d_surface(
|
|
415
|
+
x_surface=x_surface,
|
|
416
|
+
iv_surface=iv_surface,
|
|
417
|
+
fit_results=fit_results,
|
|
418
|
+
domain_type=return_domain
|
|
419
|
+
)
|
|
411
420
|
|
|
412
421
|
return plots
|
|
413
422
|
|
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Visualization module for the Voly package.
|
|
3
|
+
|
|
4
|
+
This module provides visualization functions for volatility surfaces,
|
|
5
|
+
risk-neutral densities, and model fitting results.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from typing import Dict, List, Tuple, Optional, Union, Any
|
|
11
|
+
from voly.utils.logger import logger, catch_exception
|
|
12
|
+
from voly.models import SVIModel
|
|
13
|
+
import plotly.graph_objects as go
|
|
14
|
+
from plotly.subplots import make_subplots
|
|
15
|
+
import plotly.io as pio
|
|
16
|
+
|
|
17
|
+
# Set default renderer to browser for interactive plots
|
|
18
|
+
pio.renderers.default = "browser"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@catch_exception
|
|
22
|
+
def plot_volatility_smile(x_domain: np.ndarray,
|
|
23
|
+
iv_array: np.ndarray,
|
|
24
|
+
option_chain: pd.DataFrame = None,
|
|
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
|
+
"""
|
|
40
|
+
fig = go.Figure()
|
|
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
|
+
|
|
50
|
+
# Add model curve
|
|
51
|
+
fig.add_trace(
|
|
52
|
+
go.Scatter(
|
|
53
|
+
x=x_domain,
|
|
54
|
+
y=iv_array * 100, # Convert to percentage
|
|
55
|
+
mode='lines',
|
|
56
|
+
name='Model',
|
|
57
|
+
line=dict(color='#0080FF', width=2)
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Add market data if provided
|
|
62
|
+
if option_chain is not None and maturity is not None:
|
|
63
|
+
maturity_data = option_chain[option_chain['maturity_name'] == maturity]
|
|
64
|
+
|
|
65
|
+
if not maturity_data.empty:
|
|
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 - skip market data for this domain
|
|
77
|
+
market_x = None
|
|
78
|
+
|
|
79
|
+
# Add bid and ask IVs if the domain type allows
|
|
80
|
+
if domain_type != 'delta' and market_x is not None:
|
|
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
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
dte_value = maturity_data['dtm'].iloc[0]
|
|
94
|
+
|
|
95
|
+
# Update layout with DTE
|
|
96
|
+
title = f'Vol Smile for {maturity} (DTE: {dte_value:.1f})'
|
|
97
|
+
else:
|
|
98
|
+
title = f'Vol Smile for {maturity}'
|
|
99
|
+
else:
|
|
100
|
+
title = 'Volatility Smile'
|
|
101
|
+
|
|
102
|
+
# Update layout
|
|
103
|
+
fig.update_layout(
|
|
104
|
+
title=title,
|
|
105
|
+
xaxis_title=domain_labels.get(domain_type, 'X Domain'),
|
|
106
|
+
yaxis_title='Implied Volatility (%)',
|
|
107
|
+
template='plotly_dark',
|
|
108
|
+
legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return fig
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@catch_exception
|
|
115
|
+
def plot_all_smiles(x_surface: Dict[str, np.ndarray],
|
|
116
|
+
iv_surface: Dict[str, np.ndarray],
|
|
117
|
+
option_chain: Optional[pd.DataFrame] = None,
|
|
118
|
+
domain_type: str = 'log_moneyness') -> List[go.Figure]:
|
|
119
|
+
"""
|
|
120
|
+
Plot volatility smiles for all expiries.
|
|
121
|
+
|
|
122
|
+
Parameters:
|
|
123
|
+
- x_surface: Dictionary mapping maturity names to x-domain arrays
|
|
124
|
+
- iv_surface: Dictionary mapping maturity names to IV arrays
|
|
125
|
+
- option_chain: Optional market data for comparison
|
|
126
|
+
- domain_type: Type of x-domain ('log_moneyness', 'moneyness', 'strikes', 'delta')
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
- List of Plotly figures
|
|
130
|
+
"""
|
|
131
|
+
return [
|
|
132
|
+
plot_volatility_smile(
|
|
133
|
+
x_domain=x_surface[maturity],
|
|
134
|
+
iv_array=iv_surface[maturity],
|
|
135
|
+
option_chain=option_chain,
|
|
136
|
+
maturity=maturity,
|
|
137
|
+
domain_type=domain_type
|
|
138
|
+
)
|
|
139
|
+
for maturity in iv_surface.keys()
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@catch_exception
|
|
144
|
+
def plot_parameters(fit_results: pd.DataFrame) -> go.Figure:
|
|
145
|
+
"""
|
|
146
|
+
Plot raw SVI parameters across different expiries.
|
|
147
|
+
|
|
148
|
+
Parameters:
|
|
149
|
+
- fit_results: DataFrame from fit_model() with maturity names as index
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
- Plotly figure
|
|
153
|
+
"""
|
|
154
|
+
# Select parameters to plot
|
|
155
|
+
param_names = ['a', 'b', 'sigma', 'rho', 'm']
|
|
156
|
+
|
|
157
|
+
# Create subplots
|
|
158
|
+
fig = make_subplots(
|
|
159
|
+
rows=3, cols=2,
|
|
160
|
+
subplot_titles=[f"Parameter {p}: {SVIModel.PARAM_DESCRIPTIONS.get(p, '')}"
|
|
161
|
+
for p in param_names] + ['']
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Get maturity names from index
|
|
165
|
+
maturity_names = fit_results.index
|
|
166
|
+
|
|
167
|
+
# Create hover text with maturity info
|
|
168
|
+
tick_labels = [f"{m} (DTE: {fit_results.loc[m, 'dtm']:.1f}, YTE: {fit_results.loc[m, 'ytm']:.4f})"
|
|
169
|
+
for m in maturity_names]
|
|
170
|
+
|
|
171
|
+
# Plot each parameter
|
|
172
|
+
for i, param in enumerate(param_names):
|
|
173
|
+
row, col = (i // 2) + 1, (i % 2) + 1
|
|
174
|
+
|
|
175
|
+
fig.add_trace(
|
|
176
|
+
go.Scatter(
|
|
177
|
+
x=list(range(len(maturity_names))),
|
|
178
|
+
y=fit_results[param],
|
|
179
|
+
mode='lines+markers',
|
|
180
|
+
name=param,
|
|
181
|
+
line=dict(width=2),
|
|
182
|
+
marker=dict(size=8),
|
|
183
|
+
text=tick_labels,
|
|
184
|
+
hovertemplate="%{text}<br>%{y:.4f}"
|
|
185
|
+
),
|
|
186
|
+
row=row, col=col
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Set x-axis labels
|
|
190
|
+
fig.update_xaxes(
|
|
191
|
+
tickvals=list(range(len(maturity_names))),
|
|
192
|
+
ticktext=maturity_names,
|
|
193
|
+
tickangle=45,
|
|
194
|
+
row=row, col=col
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Update layout
|
|
198
|
+
fig.update_layout(
|
|
199
|
+
title='Raw SVI Parameters Across Expiries',
|
|
200
|
+
template='plotly_dark',
|
|
201
|
+
showlegend=False,
|
|
202
|
+
height=800
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return fig
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@catch_exception
|
|
209
|
+
def plot_jw_parameters(fit_results: pd.DataFrame) -> go.Figure:
|
|
210
|
+
"""
|
|
211
|
+
Plot Jump-Wing parameters across different expiries.
|
|
212
|
+
|
|
213
|
+
Parameters:
|
|
214
|
+
- fit_results: DataFrame from fit_model() with maturity names as index
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
- Plotly figure
|
|
218
|
+
"""
|
|
219
|
+
# Select parameters to plot
|
|
220
|
+
param_names = ['nu', 'psi', 'p', 'c', 'nu_tilde']
|
|
221
|
+
|
|
222
|
+
# Create subplots
|
|
223
|
+
fig = make_subplots(
|
|
224
|
+
rows=3, cols=2,
|
|
225
|
+
subplot_titles=[f"Parameter {p}: {SVIModel.PARAM_DESCRIPTIONS.get(p, '')}"
|
|
226
|
+
for p in param_names] + ['']
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Get maturity names from index
|
|
230
|
+
maturity_names = fit_results.index
|
|
231
|
+
|
|
232
|
+
# Create hover text with maturity info
|
|
233
|
+
tick_labels = [f"{m} (DTE: {fit_results.loc[m, 'dtm']:.1f}, YTE: {fit_results.loc[m, 'ytm']:.4f})"
|
|
234
|
+
for m in maturity_names]
|
|
235
|
+
|
|
236
|
+
# Plot each parameter
|
|
237
|
+
for i, param in enumerate(param_names):
|
|
238
|
+
row, col = (i // 2) + 1, (i % 2) + 1
|
|
239
|
+
|
|
240
|
+
fig.add_trace(
|
|
241
|
+
go.Scatter(
|
|
242
|
+
x=list(range(len(maturity_names))),
|
|
243
|
+
y=fit_results[param],
|
|
244
|
+
mode='lines+markers',
|
|
245
|
+
name=param,
|
|
246
|
+
line=dict(width=2, color='rgb(0, 180, 180)'),
|
|
247
|
+
marker=dict(size=8),
|
|
248
|
+
text=tick_labels,
|
|
249
|
+
hovertemplate="%{text}<br>%{y:.4f}"
|
|
250
|
+
),
|
|
251
|
+
row=row, col=col
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Set x-axis labels
|
|
255
|
+
fig.update_xaxes(
|
|
256
|
+
tickvals=list(range(len(maturity_names))),
|
|
257
|
+
ticktext=maturity_names,
|
|
258
|
+
tickangle=45,
|
|
259
|
+
row=row, col=col
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Update layout
|
|
263
|
+
fig.update_layout(
|
|
264
|
+
title='Jump-Wing Parameters Across Expiries',
|
|
265
|
+
template='plotly_dark',
|
|
266
|
+
showlegend=False,
|
|
267
|
+
height=800
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
return fig
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@catch_exception
|
|
274
|
+
def plot_fit_performance(fit_results: pd.DataFrame) -> go.Figure:
|
|
275
|
+
"""
|
|
276
|
+
Plot the fitting accuracy statistics.
|
|
277
|
+
|
|
278
|
+
Parameters:
|
|
279
|
+
- fit_results: DataFrame from fit_model() with maturity names as index
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
- Plotly figure
|
|
283
|
+
"""
|
|
284
|
+
# Define metrics to plot
|
|
285
|
+
metrics = {
|
|
286
|
+
'rmse': {'title': 'RMSE by Expiry', 'row': 1, 'col': 1, 'ylabel': 'RMSE (%)', 'scale': 100},
|
|
287
|
+
'mae': {'title': 'MAE by Expiry', 'row': 1, 'col': 2, 'ylabel': 'MAE (%)', 'scale': 100},
|
|
288
|
+
'r2': {'title': 'R² by Expiry', 'row': 2, 'col': 1, 'ylabel': 'R²', 'scale': 1},
|
|
289
|
+
'max_error': {'title': 'Max Error by Expiry', 'row': 2, 'col': 2, 'ylabel': 'Max Error (%)', 'scale': 100}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
# Create subplots
|
|
293
|
+
fig = make_subplots(
|
|
294
|
+
rows=2, cols=2,
|
|
295
|
+
subplot_titles=[metrics[m]['title'] for m in metrics]
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Get maturity names from index and create x-axis indices
|
|
299
|
+
maturity_names = fit_results.index
|
|
300
|
+
x_indices = list(range(len(maturity_names)))
|
|
301
|
+
|
|
302
|
+
# Create hover labels
|
|
303
|
+
hover_labels = [f"{m} (DTE: {fit_results.loc[m, 'dtm']:.1f})" for m in maturity_names]
|
|
304
|
+
|
|
305
|
+
# Plot each metric
|
|
306
|
+
for metric, config in metrics.items():
|
|
307
|
+
fig.add_trace(
|
|
308
|
+
go.Scatter(
|
|
309
|
+
x=x_indices,
|
|
310
|
+
y=fit_results[metric] * config['scale'],
|
|
311
|
+
mode='lines+markers',
|
|
312
|
+
name=metric.upper(),
|
|
313
|
+
line=dict(width=2),
|
|
314
|
+
marker=dict(size=8),
|
|
315
|
+
text=hover_labels,
|
|
316
|
+
hovertemplate="%{text}<br>%{y:.4f}"
|
|
317
|
+
),
|
|
318
|
+
row=config['row'], col=config['col']
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Update axes
|
|
322
|
+
fig.update_yaxes(title_text=config['ylabel'], row=config['row'], col=config['col'])
|
|
323
|
+
|
|
324
|
+
# Set x-axis labels for all subplots
|
|
325
|
+
for row in range(1, 3):
|
|
326
|
+
for col in range(1, 3):
|
|
327
|
+
fig.update_xaxes(
|
|
328
|
+
tickvals=x_indices,
|
|
329
|
+
ticktext=maturity_names,
|
|
330
|
+
tickangle=45,
|
|
331
|
+
row=row, col=col
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# Update layout
|
|
335
|
+
fig.update_layout(
|
|
336
|
+
title='Model Fitting Accuracy Statistics',
|
|
337
|
+
template='plotly_dark',
|
|
338
|
+
showlegend=False,
|
|
339
|
+
height=700
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
return fig
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@catch_exception
|
|
346
|
+
def plot_3d_surface(x_surface: Dict[str, np.ndarray],
|
|
347
|
+
iv_surface: Dict[str, np.ndarray],
|
|
348
|
+
fit_results: pd.DataFrame = None,
|
|
349
|
+
domain_type: str = 'log_moneyness') -> go.Figure:
|
|
350
|
+
"""
|
|
351
|
+
Plot 3D implied volatility surface.
|
|
352
|
+
|
|
353
|
+
Parameters:
|
|
354
|
+
- x_surface: Dictionary mapping maturity names to x-domain arrays
|
|
355
|
+
- iv_surface: Dictionary mapping maturity names to IV arrays
|
|
356
|
+
- fit_results: Optional DataFrame with maturity information
|
|
357
|
+
- domain_type: Type of x-domain ('log_moneyness', 'moneyness', 'strikes', 'delta')
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
- Plotly figure
|
|
361
|
+
"""
|
|
362
|
+
# Map domain types to axis labels
|
|
363
|
+
domain_labels = {
|
|
364
|
+
'log_moneyness': 'Log Moneyness',
|
|
365
|
+
'moneyness': 'Moneyness (S/K)',
|
|
366
|
+
'strikes': 'Strike Price',
|
|
367
|
+
'delta': 'Call Delta'
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
# Define custom colorscale
|
|
371
|
+
custom_blue_scale = [[0, '#60AEFC'], [1, '#002040']]
|
|
372
|
+
|
|
373
|
+
# Get maturity names
|
|
374
|
+
maturity_names = list(iv_surface.keys())
|
|
375
|
+
|
|
376
|
+
# Get z-axis values (days to expiry)
|
|
377
|
+
if fit_results is not None:
|
|
378
|
+
# Use DTM values from fit_results
|
|
379
|
+
maturity_values = [fit_results.loc[name, 'dtm'] for name in maturity_names]
|
|
380
|
+
else:
|
|
381
|
+
# Default to sequential values
|
|
382
|
+
maturity_values = list(range(len(maturity_names)))
|
|
383
|
+
|
|
384
|
+
# For domains with uniform x values across maturities (like log_moneyness, moneyness)
|
|
385
|
+
# we can use standard surface plot
|
|
386
|
+
if domain_type in ['log_moneyness', 'moneyness']:
|
|
387
|
+
# Create mesh grid
|
|
388
|
+
X, Y = np.meshgrid(list(x_surface.values())[0], maturity_values)
|
|
389
|
+
Z = np.array([iv_surface[m] * 100 for m in maturity_names]) # Convert to percentage
|
|
390
|
+
|
|
391
|
+
# Create figure
|
|
392
|
+
fig = go.Figure(data=[
|
|
393
|
+
go.Surface(
|
|
394
|
+
z=Z,
|
|
395
|
+
x=X,
|
|
396
|
+
y=Y,
|
|
397
|
+
colorscale=custom_blue_scale,
|
|
398
|
+
contours_z=dict(
|
|
399
|
+
show=True,
|
|
400
|
+
usecolormap=True,
|
|
401
|
+
highlightcolor="#0080FF",
|
|
402
|
+
project_z=True
|
|
403
|
+
)
|
|
404
|
+
)
|
|
405
|
+
])
|
|
406
|
+
|
|
407
|
+
# For domains that might have different x values per maturity (like strikes, delta)
|
|
408
|
+
# we need to use a different approach
|
|
409
|
+
else:
|
|
410
|
+
# Create a 3D scatter plot with lines
|
|
411
|
+
fig = go.Figure()
|
|
412
|
+
|
|
413
|
+
# For each maturity, create a curve
|
|
414
|
+
for i, maturity in enumerate(maturity_names):
|
|
415
|
+
x_values = x_surface[maturity]
|
|
416
|
+
z_values = iv_surface[maturity] * 100 # Convert to percentage
|
|
417
|
+
y_values = np.full_like(x_values, maturity_values[i])
|
|
418
|
+
|
|
419
|
+
# Add a line for this maturity
|
|
420
|
+
fig.add_trace(go.Scatter3d(
|
|
421
|
+
x=x_values,
|
|
422
|
+
y=y_values,
|
|
423
|
+
z=z_values,
|
|
424
|
+
mode='lines',
|
|
425
|
+
line=dict(
|
|
426
|
+
color=f'rgb({30 + 225 * (i / len(maturity_names))}, {30 + 150 * (i / len(maturity_names))}, {200 - 170 * (i / len(maturity_names))})',
|
|
427
|
+
width=5
|
|
428
|
+
),
|
|
429
|
+
name=maturity
|
|
430
|
+
))
|
|
431
|
+
|
|
432
|
+
# Update layout
|
|
433
|
+
fig.update_layout(
|
|
434
|
+
title='Implied Volatility 3D Surface',
|
|
435
|
+
template='plotly_dark',
|
|
436
|
+
scene=dict(
|
|
437
|
+
xaxis_title=domain_labels.get(domain_type, 'X Domain'),
|
|
438
|
+
yaxis_title='Days to Expiry',
|
|
439
|
+
zaxis_title='Implied Volatility (%)',
|
|
440
|
+
aspectmode='manual',
|
|
441
|
+
aspectratio=dict(x=1.5, y=1, z=1),
|
|
442
|
+
camera=dict(
|
|
443
|
+
eye=dict(x=1.5, y=-1.5, z=1)
|
|
444
|
+
)
|
|
445
|
+
),
|
|
446
|
+
margin=dict(l=65, r=50, b=65, t=90)
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
return fig
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
@catch_exception
|
|
453
|
+
def plot_rnd(x_domain: np.ndarray,
|
|
454
|
+
rnd_values: np.ndarray,
|
|
455
|
+
spot_price: float = 1.0,
|
|
456
|
+
title: str = 'Risk-Neutral Density') -> go.Figure:
|
|
457
|
+
"""
|
|
458
|
+
Plot risk-neutral density (RND).
|
|
459
|
+
|
|
460
|
+
Parameters:
|
|
461
|
+
- x_domain: Grid of domain values
|
|
462
|
+
- rnd_values: RND values
|
|
463
|
+
- spot_price: Spot price for reference
|
|
464
|
+
- title: Plot title
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
- Plotly figure
|
|
468
|
+
"""
|
|
469
|
+
# Create figure
|
|
470
|
+
fig = go.Figure()
|
|
471
|
+
|
|
472
|
+
# Convert x_domain to prices (assuming it's in log_moneyness)
|
|
473
|
+
# This may need adjustment if the domain is not log_moneyness
|
|
474
|
+
prices = spot_price * np.exp(x_domain)
|
|
475
|
+
|
|
476
|
+
# Normalize the RND to integrate to 1
|
|
477
|
+
dx = x_domain[1] - x_domain[0]
|
|
478
|
+
total_density = np.sum(rnd_values) * dx
|
|
479
|
+
normalized_rnd = rnd_values / total_density if total_density > 0 else rnd_values
|
|
480
|
+
|
|
481
|
+
# Add trace
|
|
482
|
+
fig.add_trace(
|
|
483
|
+
go.Scatter(
|
|
484
|
+
x=prices,
|
|
485
|
+
y=normalized_rnd,
|
|
486
|
+
mode='lines',
|
|
487
|
+
name='RND',
|
|
488
|
+
line=dict(color='#00FFC1', width=2),
|
|
489
|
+
fill='tozeroy',
|
|
490
|
+
fillcolor='rgba(0, 255, 193, 0.2)'
|
|
491
|
+
)
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
# Add vertical line at spot price
|
|
495
|
+
fig.add_shape(
|
|
496
|
+
type='line',
|
|
497
|
+
x0=spot_price, y0=0,
|
|
498
|
+
x1=spot_price, y1=max(normalized_rnd) * 1.1,
|
|
499
|
+
line=dict(color='red', width=2, dash='dash')
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
# Add annotation for spot price
|
|
503
|
+
fig.add_annotation(
|
|
504
|
+
x=spot_price,
|
|
505
|
+
y=max(normalized_rnd) * 1.15,
|
|
506
|
+
text=f"Spot: {spot_price}",
|
|
507
|
+
showarrow=False,
|
|
508
|
+
font=dict(color='red')
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
# Update layout
|
|
512
|
+
fig.update_layout(
|
|
513
|
+
title=title,
|
|
514
|
+
xaxis_title='Price',
|
|
515
|
+
yaxis_title='Density',
|
|
516
|
+
template='plotly_dark',
|
|
517
|
+
showlegend=False
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
return fig
|