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.
Files changed (26) hide show
  1. {voly-0.0.69/src/voly.egg-info → voly-0.0.70}/PKG-INFO +1 -1
  2. {voly-0.0.69 → voly-0.0.70}/pyproject.toml +2 -2
  3. {voly-0.0.69 → voly-0.0.70}/src/voly/client.py +28 -19
  4. voly-0.0.70/src/voly/core/charts.py +520 -0
  5. {voly-0.0.69 → voly-0.0.70/src/voly.egg-info}/PKG-INFO +1 -1
  6. voly-0.0.69/src/voly/core/charts.py +0 -948
  7. {voly-0.0.69 → voly-0.0.70}/LICENSE +0 -0
  8. {voly-0.0.69 → voly-0.0.70}/README.md +0 -0
  9. {voly-0.0.69 → voly-0.0.70}/setup.cfg +0 -0
  10. {voly-0.0.69 → voly-0.0.70}/setup.py +0 -0
  11. {voly-0.0.69 → voly-0.0.70}/src/voly/__init__.py +0 -0
  12. {voly-0.0.69 → voly-0.0.70}/src/voly/core/__init__.py +0 -0
  13. {voly-0.0.69 → voly-0.0.70}/src/voly/core/data.py +0 -0
  14. {voly-0.0.69 → voly-0.0.70}/src/voly/core/fit.py +0 -0
  15. {voly-0.0.69 → voly-0.0.70}/src/voly/core/interpolate.py +0 -0
  16. {voly-0.0.69 → voly-0.0.70}/src/voly/core/rnd.py +0 -0
  17. {voly-0.0.69 → voly-0.0.70}/src/voly/exceptions.py +0 -0
  18. {voly-0.0.69 → voly-0.0.70}/src/voly/formulas.py +0 -0
  19. {voly-0.0.69 → voly-0.0.70}/src/voly/models.py +0 -0
  20. {voly-0.0.69 → voly-0.0.70}/src/voly/utils/__init__.py +0 -0
  21. {voly-0.0.69 → voly-0.0.70}/src/voly/utils/logger.py +0 -0
  22. {voly-0.0.69 → voly-0.0.70}/src/voly.egg-info/SOURCES.txt +0 -0
  23. {voly-0.0.69 → voly-0.0.70}/src/voly.egg-info/dependency_links.txt +0 -0
  24. {voly-0.0.69 → voly-0.0.70}/src/voly.egg-info/requires.txt +0 -0
  25. {voly-0.0.69 → voly-0.0.70}/src/voly.egg-info/top_level.txt +0 -0
  26. {voly-0.0.69 → voly-0.0.70}/tests/test_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: voly
3
- Version: 0.0.69
3
+ Version: 0.0.70
4
4
  Summary: Options & volatility research package
5
5
  Author-email: Manu de Cara <manu.de.cara@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "voly"
7
- version = "0.0.69"
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.69"
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: Dict[str, Any],
376
+ def plot_model(fit_results: pd.DataFrame,
377
377
  option_chain: pd.DataFrame = None,
378
- moneyness_params: Tuple[float, float, int] = (-2, 2, 500)
379
- ) -> Dict[str, go.Figure]:
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 and RND results.
382
+ Generate all plots for the fitted model.
382
383
 
383
384
  Parameters:
384
- - fit_results: Dictionary with fitting results from fit_model()
385
+ - fit_results: DataFrame with fitting results from fit_model()
385
386
  - option_chain: Optional market data for comparison
386
- - moneyness_params: Grid of log-moneyness values
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
- moneyness_array, iv_surface = get_iv_surface(fit_results, moneyness_params)
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(moneyness_array, iv_surface, option_chain)
402
-
403
- # Plot 3D surface
404
- plots['surface_3d'] = plot_3d_surface(moneyness_array, iv_surface)
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'], plots['jw_params'] = plot_parameters(raw_param_matrix, jw_param_matrix)
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 fit statistics if available
410
- plots['fit_performance'] = plot_fit_performance(fit_performance)
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: voly
3
- Version: 0.0.69
3
+ Version: 0.0.70
4
4
  Summary: Options & volatility research package
5
5
  Author-email: Manu de Cara <manu.de.cara@gmail.com>
6
6
  License: MIT