voly 0.0.1__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/__init__.py +10 -0
- voly/client.py +540 -0
- voly/core/__init__.py +11 -0
- voly/core/charts.py +984 -0
- voly/core/data.py +312 -0
- voly/core/fit.py +331 -0
- voly/core/interpolate.py +221 -0
- voly/core/rnd.py +389 -0
- voly/exceptions.py +3 -0
- voly/formulas.py +243 -0
- voly/models.py +86 -0
- voly/utils/__init__.py +8 -0
- voly/utils/logger.py +72 -0
- voly-0.0.1.dist-info/LICENSE +21 -0
- voly-0.0.1.dist-info/METADATA +132 -0
- voly-0.0.1.dist-info/RECORD +18 -0
- voly-0.0.1.dist-info/WHEEL +5 -0
- voly-0.0.1.dist-info/top_level.txt +1 -0
voly/core/charts.py
ADDED
|
@@ -0,0 +1,984 @@
|
|
|
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
|
+
import plotly.graph_objects as go
|
|
12
|
+
from plotly.subplots import make_subplots
|
|
13
|
+
import plotly.io as pio
|
|
14
|
+
from voly.utils.logger import logger, catch_exception
|
|
15
|
+
from voly.models import SVIModel
|
|
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(moneyness: np.ndarray,
|
|
23
|
+
iv: np.ndarray,
|
|
24
|
+
market_data: Optional[pd.DataFrame] = None,
|
|
25
|
+
expiry: Optional[float] = None,
|
|
26
|
+
title: Optional[str] = None) -> go.Figure:
|
|
27
|
+
"""
|
|
28
|
+
Plot volatility smile for a single expiry.
|
|
29
|
+
|
|
30
|
+
Parameters:
|
|
31
|
+
- moneyness: Moneyness grid
|
|
32
|
+
- iv: Implied volatility values
|
|
33
|
+
- market_data: Optional market data for comparison
|
|
34
|
+
- expiry: Optional expiry time (in years) for filtering market data
|
|
35
|
+
- title: Optional plot title
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
- Plotly figure
|
|
39
|
+
"""
|
|
40
|
+
fig = go.Figure()
|
|
41
|
+
|
|
42
|
+
# Add model curve
|
|
43
|
+
fig.add_trace(
|
|
44
|
+
go.Scatter(
|
|
45
|
+
x=moneyness,
|
|
46
|
+
y=iv * 100, # Convert to percentage
|
|
47
|
+
mode='lines',
|
|
48
|
+
name='Model',
|
|
49
|
+
line=dict(color='#00FFC1', width=2)
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Add market data if provided
|
|
54
|
+
if market_data is not None and expiry is not None:
|
|
55
|
+
# Filter market data for the specific expiry
|
|
56
|
+
expiry_data = market_data[market_data['yte'] == expiry]
|
|
57
|
+
|
|
58
|
+
if not expiry_data.empty:
|
|
59
|
+
# Add bid IV
|
|
60
|
+
fig.add_trace(
|
|
61
|
+
go.Scatter(
|
|
62
|
+
x=expiry_data['log_moneyness'],
|
|
63
|
+
y=expiry_data['bid_iv'] * 100, # Convert to percentage
|
|
64
|
+
mode='markers',
|
|
65
|
+
name='Bid IV',
|
|
66
|
+
marker=dict(size=8, symbol='circle', opacity=0.7)
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Add ask IV
|
|
71
|
+
fig.add_trace(
|
|
72
|
+
go.Scatter(
|
|
73
|
+
x=expiry_data['log_moneyness'],
|
|
74
|
+
y=expiry_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
|
+
# Get maturity name and DTE for title if not provided
|
|
82
|
+
if title is None:
|
|
83
|
+
maturity_name = expiry_data['maturity_name'].iloc[0]
|
|
84
|
+
dte_value = expiry_data['dte'].iloc[0]
|
|
85
|
+
title = f'Volatility Smile for {maturity_name} (DTE: {dte_value:.1f}, YTE: {expiry:.4f})'
|
|
86
|
+
|
|
87
|
+
# Use default title if not provided
|
|
88
|
+
if title is None:
|
|
89
|
+
title = 'Volatility Smile'
|
|
90
|
+
|
|
91
|
+
# Update layout
|
|
92
|
+
fig.update_layout(
|
|
93
|
+
title=title,
|
|
94
|
+
xaxis_title='Log Moneyness',
|
|
95
|
+
yaxis_title='Implied Volatility (%)',
|
|
96
|
+
template='plotly_dark',
|
|
97
|
+
legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return fig
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@catch_exception
|
|
104
|
+
def plot_all_smiles(moneyness: np.ndarray,
|
|
105
|
+
iv_surface: Dict[float, np.ndarray],
|
|
106
|
+
market_data: Optional[pd.DataFrame] = None) -> List[go.Figure]:
|
|
107
|
+
"""
|
|
108
|
+
Plot volatility smiles for all expiries.
|
|
109
|
+
|
|
110
|
+
Parameters:
|
|
111
|
+
- moneyness: Moneyness grid
|
|
112
|
+
- iv_surface: Dictionary mapping expiry times to IV arrays
|
|
113
|
+
- market_data: Optional market data for comparison
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
- List of Plotly figures
|
|
117
|
+
"""
|
|
118
|
+
figures = []
|
|
119
|
+
|
|
120
|
+
# Sort expiries in ascending order
|
|
121
|
+
sorted_expiries = sorted(iv_surface.keys())
|
|
122
|
+
|
|
123
|
+
# Create a figure for each expiry
|
|
124
|
+
for expiry in sorted_expiries:
|
|
125
|
+
fig = plot_volatility_smile(
|
|
126
|
+
moneyness=moneyness,
|
|
127
|
+
iv=iv_surface[expiry],
|
|
128
|
+
market_data=market_data,
|
|
129
|
+
expiry=expiry
|
|
130
|
+
)
|
|
131
|
+
figures.append(fig)
|
|
132
|
+
|
|
133
|
+
return figures
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@catch_exception
|
|
137
|
+
def plot_parameters(raw_param_matrix: pd.DataFrame,
|
|
138
|
+
jw_param_matrix: Optional[pd.DataFrame] = None) -> Tuple[go.Figure, Optional[go.Figure]]:
|
|
139
|
+
"""
|
|
140
|
+
Plot model parameters across different expiries.
|
|
141
|
+
|
|
142
|
+
Parameters:
|
|
143
|
+
- raw_param_matrix: Matrix of raw SVI parameters with maturity names as columns
|
|
144
|
+
- jw_param_matrix: Optional matrix of Jump-Wing parameters
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
- Tuple of Plotly figures (raw_params_fig, jw_params_fig)
|
|
148
|
+
"""
|
|
149
|
+
# Plot raw SVI parameters
|
|
150
|
+
param_names = raw_param_matrix.index
|
|
151
|
+
raw_fig = make_subplots(rows=3, cols=2, subplot_titles=[f"Parameter {p}: {SVIModel.PARAM_DESCRIPTIONS.get(p, '')}"
|
|
152
|
+
for p in param_names] + [''])
|
|
153
|
+
|
|
154
|
+
# Get maturity names (columns) in order
|
|
155
|
+
maturity_names = raw_param_matrix.columns
|
|
156
|
+
|
|
157
|
+
# Get YTE and DTE values from attributes
|
|
158
|
+
yte_values = raw_param_matrix.attrs['yte_values']
|
|
159
|
+
dte_values = raw_param_matrix.attrs['dte_values']
|
|
160
|
+
|
|
161
|
+
# Create custom x-axis tick labels
|
|
162
|
+
tick_labels = [f"{m} (DTE: {dte_values[m]:.1f}, YTE: {yte_values[m]:.4f})" for m in maturity_names]
|
|
163
|
+
|
|
164
|
+
# Plot each parameter
|
|
165
|
+
for i, param in enumerate(param_names):
|
|
166
|
+
row = i // 2 + 1
|
|
167
|
+
col = i % 2 + 1
|
|
168
|
+
|
|
169
|
+
raw_fig.add_trace(
|
|
170
|
+
go.Scatter(
|
|
171
|
+
x=list(range(len(maturity_names))), # Use indices for x-axis positioning
|
|
172
|
+
y=raw_param_matrix.loc[param],
|
|
173
|
+
mode='lines+markers',
|
|
174
|
+
name=param,
|
|
175
|
+
line=dict(width=2),
|
|
176
|
+
marker=dict(size=8),
|
|
177
|
+
text=tick_labels, # Add hover text
|
|
178
|
+
hovertemplate="%{text}<br>%{y:.4f}"
|
|
179
|
+
),
|
|
180
|
+
row=row, col=col
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Update x-axis for this subplot with custom tick labels
|
|
184
|
+
raw_fig.update_xaxes(
|
|
185
|
+
tickvals=list(range(len(maturity_names))),
|
|
186
|
+
ticktext=maturity_names,
|
|
187
|
+
tickangle=45,
|
|
188
|
+
row=row, col=col
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Update layout for raw parameters
|
|
192
|
+
raw_fig.update_layout(
|
|
193
|
+
title='Raw SVI Parameters Across Expiries',
|
|
194
|
+
template='plotly_dark',
|
|
195
|
+
showlegend=False,
|
|
196
|
+
height=800
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Plot Jump-Wing parameters if provided
|
|
200
|
+
jw_fig = None
|
|
201
|
+
if jw_param_matrix is not None:
|
|
202
|
+
jw_param_names = jw_param_matrix.index
|
|
203
|
+
jw_fig = make_subplots(rows=3, cols=2,
|
|
204
|
+
subplot_titles=[f"Parameter {p}: {SVIModel.PARAM_DESCRIPTIONS.get(p, '')}"
|
|
205
|
+
for p in jw_param_names] + [''])
|
|
206
|
+
|
|
207
|
+
# Plot each JW parameter
|
|
208
|
+
for i, param in enumerate(jw_param_names):
|
|
209
|
+
row = i // 2 + 1
|
|
210
|
+
col = i % 2 + 1
|
|
211
|
+
|
|
212
|
+
jw_fig.add_trace(
|
|
213
|
+
go.Scatter(
|
|
214
|
+
x=list(range(len(maturity_names))), # Use indices for x-axis positioning
|
|
215
|
+
y=jw_param_matrix.loc[param],
|
|
216
|
+
mode='lines+markers',
|
|
217
|
+
name=param,
|
|
218
|
+
line=dict(width=2, color='rgb(0, 180, 180)'),
|
|
219
|
+
marker=dict(size=8),
|
|
220
|
+
text=tick_labels, # Add hover text
|
|
221
|
+
hovertemplate="%{text}<br>%{y:.4f}"
|
|
222
|
+
),
|
|
223
|
+
row=row, col=col
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Update x-axis for this subplot with custom tick labels
|
|
227
|
+
jw_fig.update_xaxes(
|
|
228
|
+
tickvals=list(range(len(maturity_names))),
|
|
229
|
+
ticktext=maturity_names,
|
|
230
|
+
tickangle=45,
|
|
231
|
+
row=row, col=col
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Update layout for JW parameters
|
|
235
|
+
jw_fig.update_layout(
|
|
236
|
+
title='Jump-Wing Parameters Across Expiries',
|
|
237
|
+
template='plotly_dark',
|
|
238
|
+
showlegend=False,
|
|
239
|
+
height=800
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
return raw_fig, jw_fig
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@catch_exception
|
|
246
|
+
def plot_fit_statistics(stats_df: pd.DataFrame) -> go.Figure:
|
|
247
|
+
"""
|
|
248
|
+
Plot the fitting accuracy statistics.
|
|
249
|
+
|
|
250
|
+
Parameters:
|
|
251
|
+
- stats_df: DataFrame with fitting statistics
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
- Plotly figure
|
|
255
|
+
"""
|
|
256
|
+
fig = make_subplots(
|
|
257
|
+
rows=2, cols=2,
|
|
258
|
+
subplot_titles=['RMSE by Expiry', 'MAE by Expiry',
|
|
259
|
+
'R² by Expiry', 'Max Error by Expiry']
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Create custom tick labels with maturity name, DTE, and YTE
|
|
263
|
+
tick_labels = [f"{m} (DTE: {d:.1f}, YTE: {y:.4f})" for m, d, y in
|
|
264
|
+
zip(stats_df['Maturity'], stats_df['DTE'], stats_df['YTE'])]
|
|
265
|
+
|
|
266
|
+
# Get x-axis values for plotting (use indices for positioning)
|
|
267
|
+
x_indices = list(range(len(stats_df)))
|
|
268
|
+
|
|
269
|
+
# Plot RMSE
|
|
270
|
+
fig.add_trace(
|
|
271
|
+
go.Scatter(
|
|
272
|
+
x=x_indices,
|
|
273
|
+
y=stats_df['RMSE'] * 100, # Convert to percentage
|
|
274
|
+
mode='lines+markers',
|
|
275
|
+
name='RMSE',
|
|
276
|
+
line=dict(width=2),
|
|
277
|
+
marker=dict(size=8),
|
|
278
|
+
text=tick_labels # Add hover text
|
|
279
|
+
),
|
|
280
|
+
row=1, col=1
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Plot MAE
|
|
284
|
+
fig.add_trace(
|
|
285
|
+
go.Scatter(
|
|
286
|
+
x=x_indices,
|
|
287
|
+
y=stats_df['MAE'] * 100, # Convert to percentage
|
|
288
|
+
mode='lines+markers',
|
|
289
|
+
name='MAE',
|
|
290
|
+
line=dict(width=2),
|
|
291
|
+
marker=dict(size=8),
|
|
292
|
+
text=tick_labels # Add hover text
|
|
293
|
+
),
|
|
294
|
+
row=1, col=2
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Plot R²
|
|
298
|
+
fig.add_trace(
|
|
299
|
+
go.Scatter(
|
|
300
|
+
x=x_indices,
|
|
301
|
+
y=stats_df['R²'],
|
|
302
|
+
mode='lines+markers',
|
|
303
|
+
name='R²',
|
|
304
|
+
line=dict(width=2),
|
|
305
|
+
marker=dict(size=8),
|
|
306
|
+
text=tick_labels # Add hover text
|
|
307
|
+
),
|
|
308
|
+
row=2, col=1
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Plot Max Error
|
|
312
|
+
fig.add_trace(
|
|
313
|
+
go.Scatter(
|
|
314
|
+
x=x_indices,
|
|
315
|
+
y=stats_df['Max Error'] * 100, # Convert to percentage
|
|
316
|
+
mode='lines+markers',
|
|
317
|
+
name='Max Error',
|
|
318
|
+
line=dict(width=2),
|
|
319
|
+
marker=dict(size=8),
|
|
320
|
+
text=tick_labels # Add hover text
|
|
321
|
+
),
|
|
322
|
+
row=2, col=2
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Update x-axis for all subplots with maturity names
|
|
326
|
+
for row in range(1, 3):
|
|
327
|
+
for col in range(1, 3):
|
|
328
|
+
fig.update_xaxes(
|
|
329
|
+
tickvals=x_indices,
|
|
330
|
+
ticktext=stats_df['Maturity'],
|
|
331
|
+
tickangle=45,
|
|
332
|
+
row=row, col=col
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Update y-axis titles
|
|
336
|
+
fig.update_yaxes(title_text='RMSE (%)', row=1, col=1)
|
|
337
|
+
fig.update_yaxes(title_text='MAE (%)', row=1, col=2)
|
|
338
|
+
fig.update_yaxes(title_text='R²', row=2, col=1)
|
|
339
|
+
fig.update_yaxes(title_text='Max Error (%)', row=2, col=2)
|
|
340
|
+
|
|
341
|
+
# Update layout
|
|
342
|
+
fig.update_layout(
|
|
343
|
+
title='Model Fitting Accuracy Statistics',
|
|
344
|
+
template='plotly_dark',
|
|
345
|
+
showlegend=False
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
return fig
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@catch_exception
|
|
352
|
+
def plot_3d_surface(moneyness: np.ndarray,
|
|
353
|
+
expiries: np.ndarray,
|
|
354
|
+
iv_surface: Dict[float, np.ndarray],
|
|
355
|
+
interpolate: bool = True,
|
|
356
|
+
title: str = 'Implied Volatility Surface') -> go.Figure:
|
|
357
|
+
"""
|
|
358
|
+
Plot 3D implied volatility surface.
|
|
359
|
+
|
|
360
|
+
Parameters:
|
|
361
|
+
- moneyness: Moneyness grid
|
|
362
|
+
- expiries: Expiry times in years
|
|
363
|
+
- iv_surface: Dictionary mapping expiry times to IV arrays
|
|
364
|
+
- interpolate: Whether to interpolate the surface
|
|
365
|
+
- title: Plot title
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
- Plotly figure
|
|
369
|
+
"""
|
|
370
|
+
# Convert implied volatility surface to array
|
|
371
|
+
z_array = np.array([iv_surface[t] for t in expiries])
|
|
372
|
+
|
|
373
|
+
# Create mesh grid
|
|
374
|
+
X, Y = np.meshgrid(moneyness, expiries)
|
|
375
|
+
Z = z_array * 100 # Convert to percentage
|
|
376
|
+
|
|
377
|
+
# Create 3D surface plot
|
|
378
|
+
fig = go.Figure(data=[go.Surface(z=Z, x=X, y=Y, colorscale='tealgrn')])
|
|
379
|
+
|
|
380
|
+
# Add colorbar
|
|
381
|
+
fig.update_traces(contours_z=dict(show=True, usecolormap=True,
|
|
382
|
+
highlightcolor="limegreen", project_z=True))
|
|
383
|
+
|
|
384
|
+
# Update layout
|
|
385
|
+
fig.update_layout(
|
|
386
|
+
title=title,
|
|
387
|
+
template='plotly_dark',
|
|
388
|
+
scene=dict(
|
|
389
|
+
xaxis_title='Log Moneyness',
|
|
390
|
+
yaxis_title='Time to Expiry (years)',
|
|
391
|
+
zaxis_title='Implied Volatility (%)'
|
|
392
|
+
),
|
|
393
|
+
margin=dict(l=65, r=50, b=65, t=90)
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
return fig
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@catch_exception
|
|
400
|
+
def plot_rnd(moneyness_grid: np.ndarray,
|
|
401
|
+
rnd_values: np.ndarray,
|
|
402
|
+
spot_price: float = 1.0,
|
|
403
|
+
title: str = 'Risk-Neutral Density') -> go.Figure:
|
|
404
|
+
"""
|
|
405
|
+
Plot risk-neutral density (RND).
|
|
406
|
+
|
|
407
|
+
Parameters:
|
|
408
|
+
- moneyness_grid: Grid of log-moneyness values
|
|
409
|
+
- rnd_values: RND values
|
|
410
|
+
- spot_price: Spot price for converting to absolute prices
|
|
411
|
+
- title: Plot title
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
- Plotly figure
|
|
415
|
+
"""
|
|
416
|
+
# Create figure
|
|
417
|
+
fig = go.Figure()
|
|
418
|
+
|
|
419
|
+
# Convert to prices and normalize RND
|
|
420
|
+
prices = spot_price * np.exp(moneyness_grid)
|
|
421
|
+
|
|
422
|
+
# Normalize the RND to integrate to 1
|
|
423
|
+
dx = moneyness_grid[1] - moneyness_grid[0]
|
|
424
|
+
total_density = np.sum(rnd_values) * dx
|
|
425
|
+
normalized_rnd = rnd_values / total_density if total_density > 0 else rnd_values
|
|
426
|
+
|
|
427
|
+
# Add trace
|
|
428
|
+
fig.add_trace(
|
|
429
|
+
go.Scatter(
|
|
430
|
+
x=prices,
|
|
431
|
+
y=normalized_rnd,
|
|
432
|
+
mode='lines',
|
|
433
|
+
name='RND',
|
|
434
|
+
line=dict(color='#00FFC1', width=2),
|
|
435
|
+
fill='tozeroy',
|
|
436
|
+
fillcolor='rgba(0, 255, 193, 0.2)'
|
|
437
|
+
)
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
# Add vertical line at spot price
|
|
441
|
+
fig.add_shape(
|
|
442
|
+
type='line',
|
|
443
|
+
x0=spot_price, y0=0,
|
|
444
|
+
x1=spot_price, y1=max(normalized_rnd) * 1.1,
|
|
445
|
+
line=dict(color='red', width=2, dash='dash')
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Add annotation for spot price
|
|
449
|
+
fig.add_annotation(
|
|
450
|
+
x=spot_price,
|
|
451
|
+
y=max(normalized_rnd) * 1.15,
|
|
452
|
+
text=f"Spot: {spot_price}",
|
|
453
|
+
showarrow=False,
|
|
454
|
+
font=dict(color='red')
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
# Update layout
|
|
458
|
+
fig.update_layout(
|
|
459
|
+
title=title,
|
|
460
|
+
xaxis_title='Price',
|
|
461
|
+
yaxis_title='Density',
|
|
462
|
+
template='plotly_dark',
|
|
463
|
+
showlegend=False
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
return fig
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@catch_exception
|
|
470
|
+
def plot_rnd_all_expiries(moneyness_grid: np.ndarray,
|
|
471
|
+
rnd_surface: Dict[str, np.ndarray],
|
|
472
|
+
param_matrix: pd.DataFrame,
|
|
473
|
+
spot_price: float = 1.0) -> go.Figure:
|
|
474
|
+
"""
|
|
475
|
+
Plot risk-neutral densities for all expiries.
|
|
476
|
+
|
|
477
|
+
Parameters:
|
|
478
|
+
- moneyness_grid: Grid of log-moneyness values
|
|
479
|
+
- rnd_surface: Dictionary mapping maturity names to RND arrays
|
|
480
|
+
- param_matrix: Matrix containing model parameters with maturity info
|
|
481
|
+
- spot_price: Spot price for converting to absolute prices
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
- Plotly figure
|
|
485
|
+
"""
|
|
486
|
+
# Get maturity information
|
|
487
|
+
dte_values = param_matrix.attrs['dte_values']
|
|
488
|
+
|
|
489
|
+
# Create figure
|
|
490
|
+
fig = go.Figure()
|
|
491
|
+
|
|
492
|
+
# Get maturity names in order by DTE
|
|
493
|
+
maturity_names = sorted(rnd_surface.keys(), key=lambda x: dte_values[x])
|
|
494
|
+
|
|
495
|
+
# Create color scale from purple to green
|
|
496
|
+
n_maturities = len(maturity_names)
|
|
497
|
+
colors = [f'rgb({int(255 - i * 255 / n_maturities)}, {int(i * 255 / n_maturities)}, 255)'
|
|
498
|
+
for i in range(n_maturities)]
|
|
499
|
+
|
|
500
|
+
# Convert to prices
|
|
501
|
+
prices = spot_price * np.exp(moneyness_grid)
|
|
502
|
+
|
|
503
|
+
# Add traces for each expiry
|
|
504
|
+
for i, maturity_name in enumerate(maturity_names):
|
|
505
|
+
rnd = rnd_surface[maturity_name]
|
|
506
|
+
dte = dte_values[maturity_name]
|
|
507
|
+
|
|
508
|
+
# Normalize the RND
|
|
509
|
+
dx = moneyness_grid[1] - moneyness_grid[0]
|
|
510
|
+
total_density = np.sum(rnd) * dx
|
|
511
|
+
normalized_rnd = rnd / total_density if total_density > 0 else rnd
|
|
512
|
+
|
|
513
|
+
# Add trace
|
|
514
|
+
fig.add_trace(
|
|
515
|
+
go.Scatter(
|
|
516
|
+
x=prices,
|
|
517
|
+
y=normalized_rnd,
|
|
518
|
+
mode='lines',
|
|
519
|
+
name=f"{maturity_name} (DTE: {dte:.1f})",
|
|
520
|
+
line=dict(color=colors[i], width=2),
|
|
521
|
+
)
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# Add vertical line at spot price
|
|
525
|
+
fig.add_shape(
|
|
526
|
+
type='line',
|
|
527
|
+
x0=spot_price, y0=0,
|
|
528
|
+
x1=spot_price, y1=1, # Will be scaled automatically
|
|
529
|
+
line=dict(color='red', width=2, dash='dash')
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# Update layout
|
|
533
|
+
fig.update_layout(
|
|
534
|
+
title="Risk-Neutral Densities Across Expiries",
|
|
535
|
+
xaxis_title='Price',
|
|
536
|
+
yaxis_title='Density',
|
|
537
|
+
template='plotly_dark',
|
|
538
|
+
legend=dict(
|
|
539
|
+
yanchor="top",
|
|
540
|
+
y=0.99,
|
|
541
|
+
xanchor="left",
|
|
542
|
+
x=0.01
|
|
543
|
+
)
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
return fig
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
@catch_exception
|
|
550
|
+
def plot_rnd_3d(moneyness_grid: np.ndarray,
|
|
551
|
+
rnd_surface: Dict[str, np.ndarray],
|
|
552
|
+
param_matrix: pd.DataFrame,
|
|
553
|
+
spot_price: float = 1.0) -> go.Figure:
|
|
554
|
+
"""
|
|
555
|
+
Plot 3D surface of risk-neutral densities.
|
|
556
|
+
|
|
557
|
+
Parameters:
|
|
558
|
+
- moneyness_grid: Grid of log-moneyness values
|
|
559
|
+
- rnd_surface: Dictionary mapping maturity names to RND arrays
|
|
560
|
+
- param_matrix: Matrix containing model parameters with maturity info
|
|
561
|
+
- spot_price: Spot price for converting to absolute prices
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
- Plotly figure
|
|
565
|
+
"""
|
|
566
|
+
# Get maturity information
|
|
567
|
+
dte_values = param_matrix.attrs['dte_values']
|
|
568
|
+
|
|
569
|
+
# Get maturity names in order by DTE
|
|
570
|
+
maturity_names = sorted(rnd_surface.keys(), key=lambda x: dte_values[x])
|
|
571
|
+
|
|
572
|
+
# Extract DTE values for z-axis
|
|
573
|
+
dte_list = [dte_values[name] for name in maturity_names]
|
|
574
|
+
|
|
575
|
+
# Convert to prices
|
|
576
|
+
prices = spot_price * np.exp(moneyness_grid)
|
|
577
|
+
|
|
578
|
+
# Create z-data matrix and normalize RNDs
|
|
579
|
+
z_data = np.zeros((len(maturity_names), len(prices)))
|
|
580
|
+
|
|
581
|
+
for i, name in enumerate(maturity_names):
|
|
582
|
+
rnd = rnd_surface[name]
|
|
583
|
+
|
|
584
|
+
# Normalize the RND
|
|
585
|
+
dx = moneyness_grid[1] - moneyness_grid[0]
|
|
586
|
+
total_density = np.sum(rnd) * dx
|
|
587
|
+
normalized_rnd = rnd / total_density if total_density > 0 else rnd
|
|
588
|
+
|
|
589
|
+
z_data[i] = normalized_rnd
|
|
590
|
+
|
|
591
|
+
# Create mesh grid
|
|
592
|
+
X, Y = np.meshgrid(prices, dte_list)
|
|
593
|
+
|
|
594
|
+
# Create 3D surface
|
|
595
|
+
fig = go.Figure(data=[
|
|
596
|
+
go.Surface(
|
|
597
|
+
z=z_data,
|
|
598
|
+
x=X,
|
|
599
|
+
y=Y,
|
|
600
|
+
colorscale='Viridis',
|
|
601
|
+
showscale=True
|
|
602
|
+
)
|
|
603
|
+
])
|
|
604
|
+
|
|
605
|
+
# Update layout
|
|
606
|
+
fig.update_layout(
|
|
607
|
+
title="3D Risk-Neutral Density Surface",
|
|
608
|
+
scene=dict(
|
|
609
|
+
xaxis_title="Price",
|
|
610
|
+
yaxis_title="Days to Expiry",
|
|
611
|
+
zaxis_title="Density"
|
|
612
|
+
),
|
|
613
|
+
margin=dict(l=65, r=50, b=65, t=90),
|
|
614
|
+
template="plotly_dark"
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
return fig
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
@catch_exception
|
|
621
|
+
def plot_rnd_statistics(rnd_statistics: pd.DataFrame,
|
|
622
|
+
rnd_probabilities: pd.DataFrame) -> Tuple[go.Figure, go.Figure]:
|
|
623
|
+
"""
|
|
624
|
+
Plot RND statistics and probabilities.
|
|
625
|
+
|
|
626
|
+
Parameters:
|
|
627
|
+
- rnd_statistics: DataFrame with RND statistics
|
|
628
|
+
- rnd_probabilities: DataFrame with RND probabilities
|
|
629
|
+
|
|
630
|
+
Returns:
|
|
631
|
+
- Tuple of (statistics_fig, probabilities_fig)
|
|
632
|
+
"""
|
|
633
|
+
# Create subplot figure for key statistics
|
|
634
|
+
stats_fig = make_subplots(
|
|
635
|
+
rows=1, cols=3,
|
|
636
|
+
subplot_titles=("Standard Deviation (%) vs. DTE",
|
|
637
|
+
"Skewness vs. DTE",
|
|
638
|
+
"Excess Kurtosis vs. DTE")
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
# Add traces for each statistic
|
|
642
|
+
stats_fig.add_trace(
|
|
643
|
+
go.Scatter(
|
|
644
|
+
x=rnd_statistics["dte"],
|
|
645
|
+
y=rnd_statistics["std_dev_pct"],
|
|
646
|
+
mode="lines+markers",
|
|
647
|
+
name="Standard Deviation (%)",
|
|
648
|
+
hovertemplate="DTE: %{x:.1f}<br>Std Dev: %{y:.2f}%"
|
|
649
|
+
),
|
|
650
|
+
row=1, col=1
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
stats_fig.add_trace(
|
|
654
|
+
go.Scatter(
|
|
655
|
+
x=rnd_statistics["dte"],
|
|
656
|
+
y=rnd_statistics["skewness"],
|
|
657
|
+
mode="lines+markers",
|
|
658
|
+
name="Skewness",
|
|
659
|
+
hovertemplate="DTE: %{x:.1f}<br>Skewness: %{y:.4f}"
|
|
660
|
+
),
|
|
661
|
+
row=1, col=2
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
stats_fig.add_trace(
|
|
665
|
+
go.Scatter(
|
|
666
|
+
x=rnd_statistics["dte"],
|
|
667
|
+
y=rnd_statistics["excess_kurtosis"],
|
|
668
|
+
mode="lines+markers",
|
|
669
|
+
name="Excess Kurtosis",
|
|
670
|
+
hovertemplate="DTE: %{x:.1f}<br>Excess Kurtosis: %{y:.4f}"
|
|
671
|
+
),
|
|
672
|
+
row=1, col=3
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
# Update layout
|
|
676
|
+
stats_fig.update_layout(
|
|
677
|
+
title="Risk-Neutral Density Statistics Across Expiries",
|
|
678
|
+
template="plotly_dark",
|
|
679
|
+
height=500,
|
|
680
|
+
showlegend=False
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
# Update axes
|
|
684
|
+
stats_fig.update_xaxes(title_text="Days to Expiry", row=1, col=1)
|
|
685
|
+
stats_fig.update_xaxes(title_text="Days to Expiry", row=1, col=2)
|
|
686
|
+
stats_fig.update_xaxes(title_text="Days to Expiry", row=1, col=3)
|
|
687
|
+
|
|
688
|
+
stats_fig.update_yaxes(title_text="Standard Deviation (%)", row=1, col=1)
|
|
689
|
+
stats_fig.update_yaxes(title_text="Skewness", row=1, col=2)
|
|
690
|
+
stats_fig.update_yaxes(title_text="Excess Kurtosis", row=1, col=3)
|
|
691
|
+
|
|
692
|
+
# Create a second figure for probability thresholds
|
|
693
|
+
prob_fig = go.Figure()
|
|
694
|
+
|
|
695
|
+
# Get probability columns (those starting with "p_")
|
|
696
|
+
prob_cols = [col for col in rnd_probabilities.columns if col.startswith("p_")]
|
|
697
|
+
|
|
698
|
+
# Sort the columns to ensure they're in order by threshold value
|
|
699
|
+
prob_cols_above = sorted([col for col in prob_cols if "above" in col],
|
|
700
|
+
key=lambda x: float(x.split("_")[2]))
|
|
701
|
+
prob_cols_below = sorted([col for col in prob_cols if "below" in col],
|
|
702
|
+
key=lambda x: float(x.split("_")[2]))
|
|
703
|
+
|
|
704
|
+
# Color gradients
|
|
705
|
+
green_colors = [
|
|
706
|
+
'rgba(144, 238, 144, 1)', # Light green
|
|
707
|
+
'rgba(50, 205, 50, 1)', # Lime green
|
|
708
|
+
'rgba(34, 139, 34, 1)', # Forest green
|
|
709
|
+
'rgba(0, 100, 0, 1)' # Dark green
|
|
710
|
+
]
|
|
711
|
+
|
|
712
|
+
red_colors = [
|
|
713
|
+
'rgba(139, 0, 0, 1)', # Dark red
|
|
714
|
+
'rgba(220, 20, 60, 1)', # Crimson
|
|
715
|
+
'rgba(240, 128, 128, 1)', # Light coral
|
|
716
|
+
'rgba(255, 182, 193, 1)' # Light pink/red
|
|
717
|
+
]
|
|
718
|
+
|
|
719
|
+
# Add lines for upside probabilities (green)
|
|
720
|
+
for i, col in enumerate(prob_cols_above):
|
|
721
|
+
threshold = float(col.split("_")[2])
|
|
722
|
+
label = f"P(X > {threshold})"
|
|
723
|
+
|
|
724
|
+
# Select color based on how far OTM
|
|
725
|
+
color_idx = min(i, len(green_colors) - 1)
|
|
726
|
+
|
|
727
|
+
prob_fig.add_trace(
|
|
728
|
+
go.Scatter(
|
|
729
|
+
x=rnd_probabilities["dte"],
|
|
730
|
+
y=rnd_probabilities[col] * 100, # Convert to percentage
|
|
731
|
+
mode="lines+markers",
|
|
732
|
+
name=label,
|
|
733
|
+
line=dict(color=green_colors[color_idx], width=3),
|
|
734
|
+
marker=dict(size=8, color=green_colors[color_idx]),
|
|
735
|
+
hovertemplate="DTE: %{x:.1f}<br>" + label + ": %{y:.2f}%"
|
|
736
|
+
)
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
# Add lines for downside probabilities (red)
|
|
740
|
+
for i, col in enumerate(prob_cols_below):
|
|
741
|
+
threshold = float(col.split("_")[2])
|
|
742
|
+
label = f"P(X < {threshold})"
|
|
743
|
+
|
|
744
|
+
# Select color based on how far OTM
|
|
745
|
+
color_idx = min(i, len(red_colors) - 1)
|
|
746
|
+
|
|
747
|
+
prob_fig.add_trace(
|
|
748
|
+
go.Scatter(
|
|
749
|
+
x=rnd_probabilities["dte"],
|
|
750
|
+
y=rnd_probabilities[col] * 100, # Convert to percentage
|
|
751
|
+
mode="lines+markers",
|
|
752
|
+
name=label,
|
|
753
|
+
line=dict(color=red_colors[color_idx], width=3),
|
|
754
|
+
marker=dict(size=8, color=red_colors[color_idx]),
|
|
755
|
+
hovertemplate="DTE: %{x:.1f}<br>" + label + ": %{y:.2f}%"
|
|
756
|
+
)
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
# Update layout
|
|
760
|
+
prob_fig.update_layout(
|
|
761
|
+
title="Probability Thresholds Across Expiries",
|
|
762
|
+
xaxis_title="Days to Expiry",
|
|
763
|
+
yaxis_title="Probability (%)",
|
|
764
|
+
template="plotly_dark",
|
|
765
|
+
legend=dict(
|
|
766
|
+
yanchor="top",
|
|
767
|
+
y=0.99,
|
|
768
|
+
xanchor="right",
|
|
769
|
+
x=0.99
|
|
770
|
+
)
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
return stats_fig, prob_fig
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
@catch_exception
|
|
777
|
+
def plot_cdf(moneyness_grid: np.ndarray,
|
|
778
|
+
rnd_values: np.ndarray,
|
|
779
|
+
spot_price: float = 1.0,
|
|
780
|
+
title: str = 'Cumulative Distribution Function') -> go.Figure:
|
|
781
|
+
"""
|
|
782
|
+
Plot the cumulative distribution function (CDF) from RND values.
|
|
783
|
+
|
|
784
|
+
Parameters:
|
|
785
|
+
- moneyness_grid: Grid of log-moneyness values
|
|
786
|
+
- rnd_values: RND values
|
|
787
|
+
- spot_price: Spot price for converting to absolute prices
|
|
788
|
+
- title: Plot title
|
|
789
|
+
|
|
790
|
+
Returns:
|
|
791
|
+
- Plotly figure
|
|
792
|
+
"""
|
|
793
|
+
# Convert to prices and normalize RND
|
|
794
|
+
prices = spot_price * np.exp(moneyness_grid)
|
|
795
|
+
|
|
796
|
+
# Normalize the RND
|
|
797
|
+
dx = moneyness_grid[1] - moneyness_grid[0]
|
|
798
|
+
total_density = np.sum(rnd_values) * dx
|
|
799
|
+
normalized_rnd = rnd_values / total_density if total_density > 0 else rnd_values
|
|
800
|
+
|
|
801
|
+
# Calculate CDF
|
|
802
|
+
cdf = np.cumsum(normalized_rnd) * dx
|
|
803
|
+
|
|
804
|
+
# Create figure
|
|
805
|
+
fig = go.Figure()
|
|
806
|
+
|
|
807
|
+
# Add CDF trace
|
|
808
|
+
fig.add_trace(
|
|
809
|
+
go.Scatter(
|
|
810
|
+
x=prices,
|
|
811
|
+
y=cdf,
|
|
812
|
+
mode='lines',
|
|
813
|
+
name='CDF',
|
|
814
|
+
line=dict(color='#00FFC1', width=2)
|
|
815
|
+
)
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
# Add vertical line at spot price
|
|
819
|
+
fig.add_shape(
|
|
820
|
+
type='line',
|
|
821
|
+
x0=spot_price, y0=0,
|
|
822
|
+
x1=spot_price, y1=1,
|
|
823
|
+
line=dict(color='red', width=2, dash='dash')
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
# Add horizontal line at CDF=0.5 (median)
|
|
827
|
+
fig.add_shape(
|
|
828
|
+
type='line',
|
|
829
|
+
x0=prices[0], y0=0.5,
|
|
830
|
+
x1=prices[-1], y1=0.5,
|
|
831
|
+
line=dict(color='orange', width=2, dash='dash')
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
# Add annotation for spot price
|
|
835
|
+
fig.add_annotation(
|
|
836
|
+
x=spot_price,
|
|
837
|
+
y=1.05,
|
|
838
|
+
text=f"Spot: {spot_price}",
|
|
839
|
+
showarrow=False,
|
|
840
|
+
font=dict(color='red')
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
# Update layout
|
|
844
|
+
fig.update_layout(
|
|
845
|
+
title=title,
|
|
846
|
+
xaxis_title='Price',
|
|
847
|
+
yaxis_title='Cumulative Probability',
|
|
848
|
+
template='plotly_dark',
|
|
849
|
+
yaxis=dict(range=[0, 1.1]),
|
|
850
|
+
showlegend=False
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
return fig
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
@catch_exception
|
|
857
|
+
def plot_pdf(moneyness_grid: np.ndarray,
|
|
858
|
+
rnd_values: np.ndarray,
|
|
859
|
+
spot_price: float = 1.0,
|
|
860
|
+
title: str = 'Probability Density Function') -> go.Figure:
|
|
861
|
+
"""
|
|
862
|
+
Plot the probability density function (PDF) from RND values.
|
|
863
|
+
|
|
864
|
+
Parameters:
|
|
865
|
+
- moneyness_grid: Grid of log-moneyness values
|
|
866
|
+
- rnd_values: RND values
|
|
867
|
+
- spot_price: Spot price for converting to absolute prices
|
|
868
|
+
- title: Plot title
|
|
869
|
+
|
|
870
|
+
Returns:
|
|
871
|
+
- Plotly figure
|
|
872
|
+
"""
|
|
873
|
+
# This is essentially the same as plot_rnd but with a different title
|
|
874
|
+
return plot_rnd(moneyness_grid, rnd_values, spot_price, title)
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
@catch_exception
|
|
878
|
+
def plot_interpolated_surface(
|
|
879
|
+
interp_results: Dict[str, Any],
|
|
880
|
+
title: str = 'Interpolated Implied Volatility Surface'
|
|
881
|
+
) -> go.Figure:
|
|
882
|
+
"""
|
|
883
|
+
Plot interpolated implied volatility surface.
|
|
884
|
+
|
|
885
|
+
Parameters:
|
|
886
|
+
- interp_results: Dictionary with interpolation results
|
|
887
|
+
- title: Plot title
|
|
888
|
+
|
|
889
|
+
Returns:
|
|
890
|
+
- Plotly figure
|
|
891
|
+
"""
|
|
892
|
+
# Extract data from interpolation results
|
|
893
|
+
moneyness_grid = interp_results['moneyness_grid']
|
|
894
|
+
target_expiries_years = interp_results['target_expiries_years']
|
|
895
|
+
iv_surface = interp_results['iv_surface']
|
|
896
|
+
|
|
897
|
+
# Create a 3D surface plot
|
|
898
|
+
fig = plot_3d_surface(
|
|
899
|
+
moneyness=moneyness_grid,
|
|
900
|
+
expiries=target_expiries_years,
|
|
901
|
+
iv_surface=iv_surface,
|
|
902
|
+
title=title
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
return fig
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
@catch_exception
|
|
909
|
+
def generate_all_plots(fit_results: Dict[str, Any],
|
|
910
|
+
rnd_results: Optional[Dict[str, Any]] = None,
|
|
911
|
+
market_data: Optional[pd.DataFrame] = None) -> Dict[str, go.Figure]:
|
|
912
|
+
"""
|
|
913
|
+
Generate all plots for the fitted model and RND results.
|
|
914
|
+
|
|
915
|
+
Parameters:
|
|
916
|
+
- fit_results: Dictionary with fitting results from fit_model()
|
|
917
|
+
- rnd_results: Optional dictionary with RND results from calculate_rnd()
|
|
918
|
+
- market_data: Optional market data for comparison
|
|
919
|
+
|
|
920
|
+
Returns:
|
|
921
|
+
- Dictionary of plot figures
|
|
922
|
+
"""
|
|
923
|
+
plots = {}
|
|
924
|
+
|
|
925
|
+
# Extract data from fit results
|
|
926
|
+
moneyness_grid = fit_results['moneyness_grid']
|
|
927
|
+
iv_surface = fit_results['iv_surface']
|
|
928
|
+
raw_param_matrix = fit_results['raw_param_matrix']
|
|
929
|
+
jw_param_matrix = fit_results.get('jw_param_matrix')
|
|
930
|
+
stats_df = fit_results.get('stats_df')
|
|
931
|
+
unique_expiries = fit_results['unique_expiries']
|
|
932
|
+
|
|
933
|
+
# Plot volatility smiles
|
|
934
|
+
logger.info("Generating volatility smile plots...")
|
|
935
|
+
plots['smiles'] = plot_all_smiles(moneyness_grid, iv_surface, market_data)
|
|
936
|
+
|
|
937
|
+
# Plot 3D surface
|
|
938
|
+
logger.info("Generating 3D volatility surface plot...")
|
|
939
|
+
plots['surface_3d'] = plot_3d_surface(moneyness_grid, unique_expiries, iv_surface)
|
|
940
|
+
|
|
941
|
+
# Plot parameters
|
|
942
|
+
logger.info("Generating parameter plots...")
|
|
943
|
+
plots['raw_params'], plots['jw_params'] = plot_parameters(raw_param_matrix, jw_param_matrix)
|
|
944
|
+
|
|
945
|
+
# Plot fit statistics if available
|
|
946
|
+
if stats_df is not None:
|
|
947
|
+
logger.info("Generating fit statistics plot...")
|
|
948
|
+
plots['fit_stats'] = plot_fit_statistics(stats_df)
|
|
949
|
+
|
|
950
|
+
# Plot RND results if available
|
|
951
|
+
if rnd_results is not None:
|
|
952
|
+
logger.info("Generating RND plots...")
|
|
953
|
+
|
|
954
|
+
# Extract RND data
|
|
955
|
+
rnd_surface = rnd_results['rnd_surface']
|
|
956
|
+
rnd_statistics = rnd_results['rnd_statistics']
|
|
957
|
+
rnd_probabilities = rnd_results['rnd_probabilities']
|
|
958
|
+
spot_price = rnd_results['spot_price']
|
|
959
|
+
|
|
960
|
+
# Plot RND for each expiry
|
|
961
|
+
plots['rnd'] = {}
|
|
962
|
+
for maturity_name, rnd_values in rnd_surface.items():
|
|
963
|
+
plots['rnd'][maturity_name] = plot_rnd(
|
|
964
|
+
moneyness_grid, rnd_values, spot_price,
|
|
965
|
+
title=f"Risk-Neutral Density - {maturity_name}"
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
# Plot all RNDs in one figure
|
|
969
|
+
plots['rnd_all'] = plot_rnd_all_expiries(moneyness_grid, rnd_surface, raw_param_matrix, spot_price)
|
|
970
|
+
|
|
971
|
+
# Plot 3D RND surface
|
|
972
|
+
plots['rnd_3d'] = plot_rnd_3d(moneyness_grid, rnd_surface, raw_param_matrix, spot_price)
|
|
973
|
+
|
|
974
|
+
# Plot RND statistics
|
|
975
|
+
plots['rnd_stats'], plots['rnd_probs'] = plot_rnd_statistics(rnd_statistics, rnd_probabilities)
|
|
976
|
+
|
|
977
|
+
# Plot PDF and CDF for the first expiry
|
|
978
|
+
first_maturity = list(rnd_surface.keys())[0]
|
|
979
|
+
first_rnd = rnd_surface[first_maturity]
|
|
980
|
+
|
|
981
|
+
plots['pdf'] = plot_pdf(moneyness_grid, first_rnd, spot_price)
|
|
982
|
+
plots['cdf'] = plot_cdf(moneyness_grid, first_rnd, spot_price)
|
|
983
|
+
|
|
984
|
+
return plots
|