voly 0.0.68__py3-none-any.whl → 0.0.70__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 +30 -24
- voly/core/charts.py +81 -509
- {voly-0.0.68.dist-info → voly-0.0.70.dist-info}/METADATA +1 -1
- {voly-0.0.68.dist-info → voly-0.0.70.dist-info}/RECORD +7 -7
- {voly-0.0.68.dist-info → voly-0.0.70.dist-info}/LICENSE +0 -0
- {voly-0.0.68.dist-info → voly-0.0.70.dist-info}/WHEEL +0 -0
- {voly-0.0.68.dist-info → voly-0.0.70.dist-info}/top_level.txt +0 -0
voly/client.py
CHANGED
|
@@ -348,7 +348,7 @@ class VolyClient:
|
|
|
348
348
|
return fit_results
|
|
349
349
|
|
|
350
350
|
@staticmethod
|
|
351
|
-
def get_iv_surface(fit_results:
|
|
351
|
+
def get_iv_surface(fit_results: pd.DataFrame,
|
|
352
352
|
log_moneyness_params: Tuple[float, float, int] = (-2, 2, 500),
|
|
353
353
|
return_domain: str = 'log_moneyness',
|
|
354
354
|
) -> Dict[str, Any]:
|
|
@@ -370,47 +370,53 @@ class VolyClient:
|
|
|
370
370
|
return_domain=return_domain
|
|
371
371
|
)
|
|
372
372
|
|
|
373
|
-
return {
|
|
374
|
-
'iv_surface': iv_surface,
|
|
375
|
-
'x_surface': x_surface
|
|
376
|
-
}
|
|
373
|
+
return {'iv_surface': iv_surface, 'x_surface': x_surface}
|
|
377
374
|
|
|
378
375
|
@staticmethod
|
|
379
|
-
def plot_model(fit_results:
|
|
376
|
+
def plot_model(fit_results: pd.DataFrame,
|
|
380
377
|
option_chain: pd.DataFrame = None,
|
|
381
|
-
|
|
382
|
-
|
|
378
|
+
log_moneyness_params: Tuple[float, float, int] = (-2, 2, 500),
|
|
379
|
+
return_domain: str = 'log_moneyness',
|
|
380
|
+
) -> Dict[str, Any]:
|
|
383
381
|
"""
|
|
384
|
-
Generate all plots for the fitted model
|
|
382
|
+
Generate all plots for the fitted model.
|
|
385
383
|
|
|
386
384
|
Parameters:
|
|
387
|
-
- fit_results:
|
|
385
|
+
- fit_results: DataFrame with fitting results from fit_model()
|
|
388
386
|
- option_chain: Optional market data for comparison
|
|
389
|
-
-
|
|
387
|
+
- log_moneyness_params: Grid of log-moneyness values
|
|
388
|
+
- return_domain: Domain for x-axis values ('log_moneyness', 'moneyness', 'strikes', 'delta')
|
|
390
389
|
|
|
391
390
|
Returns:
|
|
392
391
|
- Dictionary of plot figures
|
|
393
392
|
"""
|
|
394
393
|
plots = {}
|
|
395
394
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
# Extract data from fit results
|
|
399
|
-
raw_param_matrix = fit_results['raw_param_matrix']
|
|
400
|
-
jw_param_matrix = fit_results['jw_param_matrix']
|
|
401
|
-
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)
|
|
402
397
|
|
|
403
398
|
# Plot volatility smiles
|
|
404
|
-
plots['smiles'] = plot_all_smiles(
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
+
)
|
|
408
405
|
|
|
409
406
|
# Plot parameters
|
|
410
|
-
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)
|
|
411
412
|
|
|
412
|
-
# Plot
|
|
413
|
-
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
|
+
)
|
|
414
420
|
|
|
415
421
|
return plots
|
|
416
422
|
|
voly/core/charts.py
CHANGED
|
@@ -73,11 +73,11 @@ def plot_volatility_smile(x_domain: np.ndarray,
|
|
|
73
73
|
s = maturity_data['underlying_price'].iloc[0]
|
|
74
74
|
market_x = s / np.exp(market_x)
|
|
75
75
|
elif domain_type == 'delta':
|
|
76
|
-
# For delta, we'd need more complex conversion -
|
|
77
|
-
|
|
76
|
+
# For delta, we'd need more complex conversion - skip market data for this domain
|
|
77
|
+
market_x = None
|
|
78
78
|
|
|
79
79
|
# Add bid and ask IVs if the domain type allows
|
|
80
|
-
if domain_type != 'delta':
|
|
80
|
+
if domain_type != 'delta' and market_x is not None:
|
|
81
81
|
for iv_type in ['bid_iv', 'ask_iv']:
|
|
82
82
|
if iv_type in maturity_data.columns:
|
|
83
83
|
fig.add_trace(
|
|
@@ -92,14 +92,21 @@ def plot_volatility_smile(x_domain: np.ndarray,
|
|
|
92
92
|
|
|
93
93
|
dte_value = maturity_data['dtm'].iloc[0]
|
|
94
94
|
|
|
95
|
-
# Update layout
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
+
)
|
|
103
110
|
|
|
104
111
|
return fig
|
|
105
112
|
|
|
@@ -191,7 +198,8 @@ def plot_parameters(fit_results: pd.DataFrame) -> go.Figure:
|
|
|
191
198
|
fig.update_layout(
|
|
192
199
|
title='Raw SVI Parameters Across Expiries',
|
|
193
200
|
template='plotly_dark',
|
|
194
|
-
showlegend=False
|
|
201
|
+
showlegend=False,
|
|
202
|
+
height=800
|
|
195
203
|
)
|
|
196
204
|
|
|
197
205
|
return fig
|
|
@@ -222,7 +230,7 @@ def plot_jw_parameters(fit_results: pd.DataFrame) -> go.Figure:
|
|
|
222
230
|
maturity_names = fit_results.index
|
|
223
231
|
|
|
224
232
|
# Create hover text with maturity info
|
|
225
|
-
tick_labels = [f"{m} (DTE: {fit_results.loc[m, 'dtm']:.1f})"
|
|
233
|
+
tick_labels = [f"{m} (DTE: {fit_results.loc[m, 'dtm']:.1f}, YTE: {fit_results.loc[m, 'ytm']:.4f})"
|
|
226
234
|
for m in maturity_names]
|
|
227
235
|
|
|
228
236
|
# Plot each parameter
|
|
@@ -255,7 +263,8 @@ def plot_jw_parameters(fit_results: pd.DataFrame) -> go.Figure:
|
|
|
255
263
|
fig.update_layout(
|
|
256
264
|
title='Jump-Wing Parameters Across Expiries',
|
|
257
265
|
template='plotly_dark',
|
|
258
|
-
showlegend=False
|
|
266
|
+
showlegend=False,
|
|
267
|
+
height=800
|
|
259
268
|
)
|
|
260
269
|
|
|
261
270
|
return fig
|
|
@@ -326,7 +335,8 @@ def plot_fit_performance(fit_results: pd.DataFrame) -> go.Figure:
|
|
|
326
335
|
fig.update_layout(
|
|
327
336
|
title='Model Fitting Accuracy Statistics',
|
|
328
337
|
template='plotly_dark',
|
|
329
|
-
showlegend=False
|
|
338
|
+
showlegend=False,
|
|
339
|
+
height=700
|
|
330
340
|
)
|
|
331
341
|
|
|
332
342
|
return fig
|
|
@@ -354,7 +364,7 @@ def plot_3d_surface(x_surface: Dict[str, np.ndarray],
|
|
|
354
364
|
'log_moneyness': 'Log Moneyness',
|
|
355
365
|
'moneyness': 'Moneyness (S/K)',
|
|
356
366
|
'strikes': 'Strike Price',
|
|
357
|
-
'delta': 'Delta'
|
|
367
|
+
'delta': 'Call Delta'
|
|
358
368
|
}
|
|
359
369
|
|
|
360
370
|
# Define custom colorscale
|
|
@@ -371,57 +381,54 @@ def plot_3d_surface(x_surface: Dict[str, np.ndarray],
|
|
|
371
381
|
# Default to sequential values
|
|
372
382
|
maturity_values = list(range(len(maturity_names)))
|
|
373
383
|
|
|
374
|
-
#
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
+
])
|
|
392
406
|
|
|
393
|
-
#
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
size=3,
|
|
399
|
-
color=Z,
|
|
400
|
-
colorscale=custom_blue_scale,
|
|
401
|
-
opacity=0.8
|
|
402
|
-
),
|
|
403
|
-
hovertemplate="<b>%{y:.1f} days</b><br>X: %{x:.4f}<br>IV: %{z:.2f}%"
|
|
404
|
-
))
|
|
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()
|
|
405
412
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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])
|
|
411
418
|
|
|
412
|
-
|
|
419
|
+
# Add a line for this maturity
|
|
413
420
|
fig.add_trace(go.Scatter3d(
|
|
414
|
-
x=
|
|
415
|
-
y=
|
|
416
|
-
z=
|
|
421
|
+
x=x_values,
|
|
422
|
+
y=y_values,
|
|
423
|
+
z=z_values,
|
|
417
424
|
mode='lines',
|
|
418
|
-
line=dict(
|
|
419
|
-
|
|
420
|
-
|
|
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
|
|
421
430
|
))
|
|
422
431
|
|
|
423
|
-
cumulative_index += points_count
|
|
424
|
-
|
|
425
432
|
# Update layout
|
|
426
433
|
fig.update_layout(
|
|
427
434
|
title='Implied Volatility 3D Surface',
|
|
@@ -431,7 +438,10 @@ def plot_3d_surface(x_surface: Dict[str, np.ndarray],
|
|
|
431
438
|
yaxis_title='Days to Expiry',
|
|
432
439
|
zaxis_title='Implied Volatility (%)',
|
|
433
440
|
aspectmode='manual',
|
|
434
|
-
aspectratio=dict(x=1.5, y=1, z=1)
|
|
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
|
+
)
|
|
435
445
|
),
|
|
436
446
|
margin=dict(l=65, r=50, b=65, t=90)
|
|
437
447
|
)
|
|
@@ -440,16 +450,17 @@ def plot_3d_surface(x_surface: Dict[str, np.ndarray],
|
|
|
440
450
|
|
|
441
451
|
|
|
442
452
|
@catch_exception
|
|
443
|
-
def plot_rnd(
|
|
453
|
+
def plot_rnd(x_domain: np.ndarray,
|
|
444
454
|
rnd_values: np.ndarray,
|
|
445
|
-
spot_price: float = 1.0
|
|
455
|
+
spot_price: float = 1.0,
|
|
456
|
+
title: str = 'Risk-Neutral Density') -> go.Figure:
|
|
446
457
|
"""
|
|
447
458
|
Plot risk-neutral density (RND).
|
|
448
459
|
|
|
449
460
|
Parameters:
|
|
450
|
-
-
|
|
461
|
+
- x_domain: Grid of domain values
|
|
451
462
|
- rnd_values: RND values
|
|
452
|
-
- spot_price: Spot price for
|
|
463
|
+
- spot_price: Spot price for reference
|
|
453
464
|
- title: Plot title
|
|
454
465
|
|
|
455
466
|
Returns:
|
|
@@ -458,11 +469,12 @@ def plot_rnd(moneyness_array: np.ndarray,
|
|
|
458
469
|
# Create figure
|
|
459
470
|
fig = go.Figure()
|
|
460
471
|
|
|
461
|
-
# Convert to prices
|
|
462
|
-
|
|
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)
|
|
463
475
|
|
|
464
476
|
# Normalize the RND to integrate to 1
|
|
465
|
-
dx =
|
|
477
|
+
dx = x_domain[1] - x_domain[0]
|
|
466
478
|
total_density = np.sum(rnd_values) * dx
|
|
467
479
|
normalized_rnd = rnd_values / total_density if total_density > 0 else rnd_values
|
|
468
480
|
|
|
@@ -496,453 +508,13 @@ def plot_rnd(moneyness_array: np.ndarray,
|
|
|
496
508
|
font=dict(color='red')
|
|
497
509
|
)
|
|
498
510
|
|
|
499
|
-
# Update layout
|
|
500
|
-
fig.update_layout(
|
|
501
|
-
title='Risk-Neutral Density',
|
|
502
|
-
xaxis_title='Price',
|
|
503
|
-
yaxis_title='Density',
|
|
504
|
-
template='plotly_dark',
|
|
505
|
-
showlegend=False
|
|
506
|
-
)
|
|
507
|
-
|
|
508
|
-
return fig
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
@catch_exception
|
|
512
|
-
def plot_rnd_all_expiries(moneyness_array: np.ndarray,
|
|
513
|
-
rnd_surface: Dict[str, np.ndarray],
|
|
514
|
-
fit_results: Dict[str, Any],
|
|
515
|
-
spot_price: float = 1.0) -> go.Figure:
|
|
516
|
-
"""
|
|
517
|
-
Plot risk-neutral densities for all expiries.
|
|
518
|
-
|
|
519
|
-
Parameters:
|
|
520
|
-
- moneyness_array: Grid of log-moneyness values
|
|
521
|
-
- rnd_surface: Dictionary mapping maturity names to RND arrays
|
|
522
|
-
- param_matrix: Matrix containing model parameters with maturity info
|
|
523
|
-
- spot_price: Spot price for converting to absolute prices
|
|
524
|
-
|
|
525
|
-
Returns:
|
|
526
|
-
- Plotly figure
|
|
527
|
-
"""
|
|
528
|
-
# Get maturity information
|
|
529
|
-
dte_values = fit_results['fit_performance']['DTE']
|
|
530
|
-
|
|
531
|
-
# Create figure
|
|
532
|
-
fig = go.Figure()
|
|
533
|
-
|
|
534
|
-
# Get maturity names in order by DTE
|
|
535
|
-
maturity_names = sorted(rnd_surface.keys(), key=lambda x: dte_values[x])
|
|
536
|
-
|
|
537
|
-
# Create color scale from purple to green
|
|
538
|
-
n_maturities = len(maturity_names)
|
|
539
|
-
colors = [f'rgb({int(255 - i * 255 / n_maturities)}, {int(i * 255 / n_maturities)}, 255)'
|
|
540
|
-
for i in range(n_maturities)]
|
|
541
|
-
|
|
542
|
-
# Convert to prices
|
|
543
|
-
prices = spot_price * np.exp(moneyness_array)
|
|
544
|
-
|
|
545
|
-
# Add traces for each expiry
|
|
546
|
-
for i, maturity_name in enumerate(maturity_names):
|
|
547
|
-
rnd = rnd_surface[maturity_name]
|
|
548
|
-
dte = dte_values[maturity_name]
|
|
549
|
-
|
|
550
|
-
# Normalize the RND
|
|
551
|
-
dx = moneyness_array[1] - moneyness_array[0]
|
|
552
|
-
total_density = np.sum(rnd) * dx
|
|
553
|
-
normalized_rnd = rnd / total_density if total_density > 0 else rnd
|
|
554
|
-
|
|
555
|
-
# Add trace
|
|
556
|
-
fig.add_trace(
|
|
557
|
-
go.Scatter(
|
|
558
|
-
x=prices,
|
|
559
|
-
y=normalized_rnd,
|
|
560
|
-
mode='lines',
|
|
561
|
-
name=f"{maturity_name} (DTE: {dte:.1f})",
|
|
562
|
-
line=dict(color=colors[i], width=2),
|
|
563
|
-
)
|
|
564
|
-
)
|
|
565
|
-
|
|
566
|
-
# Add vertical line at spot price
|
|
567
|
-
fig.add_shape(
|
|
568
|
-
type='line',
|
|
569
|
-
x0=spot_price, y0=0,
|
|
570
|
-
x1=spot_price, y1=1, # Will be scaled automatically
|
|
571
|
-
line=dict(color='red', width=2, dash='dash')
|
|
572
|
-
)
|
|
573
|
-
|
|
574
|
-
# Update layout
|
|
575
|
-
fig.update_layout(
|
|
576
|
-
title="Risk-Neutral Densities Across Expiries",
|
|
577
|
-
xaxis_title='Price',
|
|
578
|
-
yaxis_title='Density',
|
|
579
|
-
template='plotly_dark',
|
|
580
|
-
legend=dict(
|
|
581
|
-
yanchor="top",
|
|
582
|
-
y=0.99,
|
|
583
|
-
xanchor="left",
|
|
584
|
-
x=0.01
|
|
585
|
-
)
|
|
586
|
-
)
|
|
587
|
-
|
|
588
|
-
return fig
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
@catch_exception
|
|
592
|
-
def plot_rnd_3d(moneyness_array: np.ndarray,
|
|
593
|
-
rnd_surface: Dict[str, np.ndarray],
|
|
594
|
-
param_matrix: pd.DataFrame,
|
|
595
|
-
spot_price: float = 1.0) -> go.Figure:
|
|
596
|
-
"""
|
|
597
|
-
Plot 3D surface of risk-neutral densities.
|
|
598
|
-
|
|
599
|
-
Parameters:
|
|
600
|
-
- moneyness_array: Grid of log-moneyness values
|
|
601
|
-
- rnd_surface: Dictionary mapping maturity names to RND arrays
|
|
602
|
-
- param_matrix: Matrix containing model parameters with maturity info
|
|
603
|
-
- spot_price: Spot price for converting to absolute prices
|
|
604
|
-
|
|
605
|
-
Returns:
|
|
606
|
-
- Plotly figure
|
|
607
|
-
"""
|
|
608
|
-
# Get maturity information
|
|
609
|
-
dte_values = param_matrix.attrs['dte_values']
|
|
610
|
-
|
|
611
|
-
# Get maturity names in order by DTE
|
|
612
|
-
maturity_names = sorted(rnd_surface.keys(), key=lambda x: dte_values[x])
|
|
613
|
-
|
|
614
|
-
# Extract DTE values for z-axis
|
|
615
|
-
dte_list = [dte_values[name] for name in maturity_names]
|
|
616
|
-
|
|
617
|
-
# Convert to prices
|
|
618
|
-
prices = spot_price * np.exp(moneyness_array)
|
|
619
|
-
|
|
620
|
-
# Create z-data matrix and normalize RNDs
|
|
621
|
-
z_data = np.zeros((len(maturity_names), len(prices)))
|
|
622
|
-
|
|
623
|
-
for i, name in enumerate(maturity_names):
|
|
624
|
-
rnd = rnd_surface[name]
|
|
625
|
-
|
|
626
|
-
# Normalize the RND
|
|
627
|
-
dx = moneyness_array[1] - moneyness_array[0]
|
|
628
|
-
total_density = np.sum(rnd) * dx
|
|
629
|
-
normalized_rnd = rnd / total_density if total_density > 0 else rnd
|
|
630
|
-
|
|
631
|
-
z_data[i] = normalized_rnd
|
|
632
|
-
|
|
633
|
-
# Create mesh grid
|
|
634
|
-
X, Y = np.meshgrid(prices, dte_list)
|
|
635
|
-
|
|
636
|
-
# Create 3D surface
|
|
637
|
-
fig = go.Figure(data=[
|
|
638
|
-
go.Surface(
|
|
639
|
-
z=z_data,
|
|
640
|
-
x=X,
|
|
641
|
-
y=Y,
|
|
642
|
-
colorscale='Viridis',
|
|
643
|
-
showscale=True
|
|
644
|
-
)
|
|
645
|
-
])
|
|
646
|
-
|
|
647
|
-
# Update layout
|
|
648
|
-
fig.update_layout(
|
|
649
|
-
title="3D Risk-Neutral Density Surface",
|
|
650
|
-
scene=dict(
|
|
651
|
-
xaxis_title="Price",
|
|
652
|
-
yaxis_title="Days to Expiry",
|
|
653
|
-
zaxis_title="Density"
|
|
654
|
-
),
|
|
655
|
-
margin=dict(l=65, r=50, b=65, t=90),
|
|
656
|
-
template="plotly_dark"
|
|
657
|
-
)
|
|
658
|
-
|
|
659
|
-
return fig
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
@catch_exception
|
|
663
|
-
def plot_rnd_statistics(rnd_statistics: pd.DataFrame,
|
|
664
|
-
rnd_probabilities: pd.DataFrame) -> Tuple[go.Figure, go.Figure]:
|
|
665
|
-
"""
|
|
666
|
-
Plot RND statistics and probabilities.
|
|
667
|
-
|
|
668
|
-
Parameters:
|
|
669
|
-
- rnd_statistics: DataFrame with RND statistics
|
|
670
|
-
- rnd_probabilities: DataFrame with RND probabilities
|
|
671
|
-
|
|
672
|
-
Returns:
|
|
673
|
-
- Tuple of (statistics_fig, probabilities_fig)
|
|
674
|
-
"""
|
|
675
|
-
# Create subplot figure for key statistics
|
|
676
|
-
stats_fig = make_subplots(
|
|
677
|
-
rows=1, cols=3,
|
|
678
|
-
subplot_titles=("Standard Deviation (%) vs. DTE",
|
|
679
|
-
"Skewness vs. DTE",
|
|
680
|
-
"Excess Kurtosis vs. DTE")
|
|
681
|
-
)
|
|
682
|
-
|
|
683
|
-
# Add traces for each statistic
|
|
684
|
-
stats_fig.add_trace(
|
|
685
|
-
go.Scatter(
|
|
686
|
-
x=rnd_statistics["dte"],
|
|
687
|
-
y=rnd_statistics["std_dev_pct"],
|
|
688
|
-
mode="lines+markers",
|
|
689
|
-
name="Standard Deviation (%)",
|
|
690
|
-
hovertemplate="DTE: %{x:.1f}<br>Std Dev: %{y:.2f}%"
|
|
691
|
-
),
|
|
692
|
-
row=1, col=1
|
|
693
|
-
)
|
|
694
|
-
|
|
695
|
-
stats_fig.add_trace(
|
|
696
|
-
go.Scatter(
|
|
697
|
-
x=rnd_statistics["dte"],
|
|
698
|
-
y=rnd_statistics["skewness"],
|
|
699
|
-
mode="lines+markers",
|
|
700
|
-
name="Skewness",
|
|
701
|
-
hovertemplate="DTE: %{x:.1f}<br>Skewness: %{y:.4f}"
|
|
702
|
-
),
|
|
703
|
-
row=1, col=2
|
|
704
|
-
)
|
|
705
|
-
|
|
706
|
-
stats_fig.add_trace(
|
|
707
|
-
go.Scatter(
|
|
708
|
-
x=rnd_statistics["dte"],
|
|
709
|
-
y=rnd_statistics["excess_kurtosis"],
|
|
710
|
-
mode="lines+markers",
|
|
711
|
-
name="Excess Kurtosis",
|
|
712
|
-
hovertemplate="DTE: %{x:.1f}<br>Excess Kurtosis: %{y:.4f}"
|
|
713
|
-
),
|
|
714
|
-
row=1, col=3
|
|
715
|
-
)
|
|
716
|
-
|
|
717
|
-
# Update layout
|
|
718
|
-
stats_fig.update_layout(
|
|
719
|
-
title="Risk-Neutral Density Statistics Across Expiries",
|
|
720
|
-
template="plotly_dark",
|
|
721
|
-
height=500,
|
|
722
|
-
showlegend=False
|
|
723
|
-
)
|
|
724
|
-
|
|
725
|
-
# Update axes
|
|
726
|
-
stats_fig.update_xaxes(title_text="Days to Expiry", row=1, col=1)
|
|
727
|
-
stats_fig.update_xaxes(title_text="Days to Expiry", row=1, col=2)
|
|
728
|
-
stats_fig.update_xaxes(title_text="Days to Expiry", row=1, col=3)
|
|
729
|
-
|
|
730
|
-
stats_fig.update_yaxes(title_text="Standard Deviation (%)", row=1, col=1)
|
|
731
|
-
stats_fig.update_yaxes(title_text="Skewness", row=1, col=2)
|
|
732
|
-
stats_fig.update_yaxes(title_text="Excess Kurtosis", row=1, col=3)
|
|
733
|
-
|
|
734
|
-
# Create a second figure for probability thresholds
|
|
735
|
-
prob_fig = go.Figure()
|
|
736
|
-
|
|
737
|
-
# Get probability columns (those starting with "p_")
|
|
738
|
-
prob_cols = [col for col in rnd_probabilities.columns if col.startswith("p_")]
|
|
739
|
-
|
|
740
|
-
# Sort the columns to ensure they're in order by threshold value
|
|
741
|
-
prob_cols_above = sorted([col for col in prob_cols if "above" in col],
|
|
742
|
-
key=lambda x: float(x.split("_")[2]))
|
|
743
|
-
prob_cols_below = sorted([col for col in prob_cols if "below" in col],
|
|
744
|
-
key=lambda x: float(x.split("_")[2]))
|
|
745
|
-
|
|
746
|
-
# Color gradients
|
|
747
|
-
green_colors = [
|
|
748
|
-
'rgba(144, 238, 144, 1)', # Light green
|
|
749
|
-
'rgba(50, 205, 50, 1)', # Lime green
|
|
750
|
-
'rgba(34, 139, 34, 1)', # Forest green
|
|
751
|
-
'rgba(0, 100, 0, 1)' # Dark green
|
|
752
|
-
]
|
|
753
|
-
|
|
754
|
-
red_colors = [
|
|
755
|
-
'rgba(139, 0, 0, 1)', # Dark red
|
|
756
|
-
'rgba(220, 20, 60, 1)', # Crimson
|
|
757
|
-
'rgba(240, 128, 128, 1)', # Light coral
|
|
758
|
-
'rgba(255, 182, 193, 1)' # Light pink/red
|
|
759
|
-
]
|
|
760
|
-
|
|
761
|
-
# Add lines for upside probabilities (green)
|
|
762
|
-
for i, col in enumerate(prob_cols_above):
|
|
763
|
-
threshold = float(col.split("_")[2])
|
|
764
|
-
label = f"P(X > {threshold})"
|
|
765
|
-
|
|
766
|
-
# Select color based on how far OTM
|
|
767
|
-
color_idx = min(i, len(green_colors) - 1)
|
|
768
|
-
|
|
769
|
-
prob_fig.add_trace(
|
|
770
|
-
go.Scatter(
|
|
771
|
-
x=rnd_probabilities["dte"],
|
|
772
|
-
y=rnd_probabilities[col] * 100, # Convert to percentage
|
|
773
|
-
mode="lines+markers",
|
|
774
|
-
name=label,
|
|
775
|
-
line=dict(color=green_colors[color_idx], width=3),
|
|
776
|
-
marker=dict(size=8, color=green_colors[color_idx]),
|
|
777
|
-
hovertemplate="DTE: %{x:.1f}<br>" + label + ": %{y:.2f}%"
|
|
778
|
-
)
|
|
779
|
-
)
|
|
780
|
-
|
|
781
|
-
# Add lines for downside probabilities (red)
|
|
782
|
-
for i, col in enumerate(prob_cols_below):
|
|
783
|
-
threshold = float(col.split("_")[2])
|
|
784
|
-
label = f"P(X < {threshold})"
|
|
785
|
-
|
|
786
|
-
# Select color based on how far OTM
|
|
787
|
-
color_idx = min(i, len(red_colors) - 1)
|
|
788
|
-
|
|
789
|
-
prob_fig.add_trace(
|
|
790
|
-
go.Scatter(
|
|
791
|
-
x=rnd_probabilities["dte"],
|
|
792
|
-
y=rnd_probabilities[col] * 100, # Convert to percentage
|
|
793
|
-
mode="lines+markers",
|
|
794
|
-
name=label,
|
|
795
|
-
line=dict(color=red_colors[color_idx], width=3),
|
|
796
|
-
marker=dict(size=8, color=red_colors[color_idx]),
|
|
797
|
-
hovertemplate="DTE: %{x:.1f}<br>" + label + ": %{y:.2f}%"
|
|
798
|
-
)
|
|
799
|
-
)
|
|
800
|
-
|
|
801
|
-
# Update layout
|
|
802
|
-
prob_fig.update_layout(
|
|
803
|
-
title="Probability Thresholds Across Expiries",
|
|
804
|
-
xaxis_title="Days to Expiry",
|
|
805
|
-
yaxis_title="Probability (%)",
|
|
806
|
-
template="plotly_dark",
|
|
807
|
-
legend=dict(
|
|
808
|
-
yanchor="top",
|
|
809
|
-
y=0.99,
|
|
810
|
-
xanchor="right",
|
|
811
|
-
x=0.99
|
|
812
|
-
)
|
|
813
|
-
)
|
|
814
|
-
|
|
815
|
-
return stats_fig, prob_fig
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
@catch_exception
|
|
819
|
-
def plot_cdf(moneyness_array: np.ndarray,
|
|
820
|
-
rnd_values: np.ndarray,
|
|
821
|
-
spot_price: float = 1.0,
|
|
822
|
-
title: str = 'Cumulative Distribution Function') -> go.Figure:
|
|
823
|
-
"""
|
|
824
|
-
Plot the cumulative distribution function (CDF) from RND values.
|
|
825
|
-
|
|
826
|
-
Parameters:
|
|
827
|
-
- moneyness_array: Grid of log-moneyness values
|
|
828
|
-
- rnd_values: RND values
|
|
829
|
-
- spot_price: Spot price for converting to absolute prices
|
|
830
|
-
- title: Plot title
|
|
831
|
-
|
|
832
|
-
Returns:
|
|
833
|
-
- Plotly figure
|
|
834
|
-
"""
|
|
835
|
-
# Convert to prices and normalize RND
|
|
836
|
-
prices = spot_price * np.exp(moneyness_array)
|
|
837
|
-
|
|
838
|
-
# Normalize the RND
|
|
839
|
-
dx = moneyness_array[1] - moneyness_array[0]
|
|
840
|
-
total_density = np.sum(rnd_values) * dx
|
|
841
|
-
normalized_rnd = rnd_values / total_density if total_density > 0 else rnd_values
|
|
842
|
-
|
|
843
|
-
# Calculate CDF
|
|
844
|
-
cdf = np.cumsum(normalized_rnd) * dx
|
|
845
|
-
|
|
846
|
-
# Create figure
|
|
847
|
-
fig = go.Figure()
|
|
848
|
-
|
|
849
|
-
# Add CDF trace
|
|
850
|
-
fig.add_trace(
|
|
851
|
-
go.Scatter(
|
|
852
|
-
x=prices,
|
|
853
|
-
y=cdf,
|
|
854
|
-
mode='lines',
|
|
855
|
-
name='CDF',
|
|
856
|
-
line=dict(color='#00FFC1', width=2)
|
|
857
|
-
)
|
|
858
|
-
)
|
|
859
|
-
|
|
860
|
-
# Add vertical line at spot price
|
|
861
|
-
fig.add_shape(
|
|
862
|
-
type='line',
|
|
863
|
-
x0=spot_price, y0=0,
|
|
864
|
-
x1=spot_price, y1=1,
|
|
865
|
-
line=dict(color='red', width=2, dash='dash')
|
|
866
|
-
)
|
|
867
|
-
|
|
868
|
-
# Add horizontal line at CDF=0.5 (median)
|
|
869
|
-
fig.add_shape(
|
|
870
|
-
type='line',
|
|
871
|
-
x0=prices[0], y0=0.5,
|
|
872
|
-
x1=prices[-1], y1=0.5,
|
|
873
|
-
line=dict(color='orange', width=2, dash='dash')
|
|
874
|
-
)
|
|
875
|
-
|
|
876
|
-
# Add annotation for spot price
|
|
877
|
-
fig.add_annotation(
|
|
878
|
-
x=spot_price,
|
|
879
|
-
y=1.05,
|
|
880
|
-
text=f"Spot: {spot_price}",
|
|
881
|
-
showarrow=False,
|
|
882
|
-
font=dict(color='red')
|
|
883
|
-
)
|
|
884
|
-
|
|
885
511
|
# Update layout
|
|
886
512
|
fig.update_layout(
|
|
887
513
|
title=title,
|
|
888
514
|
xaxis_title='Price',
|
|
889
|
-
yaxis_title='
|
|
515
|
+
yaxis_title='Density',
|
|
890
516
|
template='plotly_dark',
|
|
891
|
-
yaxis=dict(range=[0, 1.1]),
|
|
892
517
|
showlegend=False
|
|
893
518
|
)
|
|
894
519
|
|
|
895
520
|
return fig
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
@catch_exception
|
|
899
|
-
def plot_pdf(moneyness_array: np.ndarray,
|
|
900
|
-
rnd_values: np.ndarray,
|
|
901
|
-
spot_price: float = 1.0,
|
|
902
|
-
title: str = 'Probability Density Function') -> go.Figure:
|
|
903
|
-
"""
|
|
904
|
-
Plot the probability density function (PDF) from RND values.
|
|
905
|
-
|
|
906
|
-
Parameters:
|
|
907
|
-
- moneyness_array: Grid of log-moneyness values
|
|
908
|
-
- rnd_values: RND values
|
|
909
|
-
- spot_price: Spot price for converting to absolute prices
|
|
910
|
-
- title: Plot title
|
|
911
|
-
|
|
912
|
-
Returns:
|
|
913
|
-
- Plotly figure
|
|
914
|
-
"""
|
|
915
|
-
# This is essentially the same as plot_rnd but with a different title
|
|
916
|
-
return plot_rnd(moneyness_array, rnd_values, spot_price, title)
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
@catch_exception
|
|
920
|
-
def plot_interpolated_surface(
|
|
921
|
-
interp_results: Dict[str, Any],
|
|
922
|
-
title: str = 'Interpolated Implied Volatility Surface'
|
|
923
|
-
) -> go.Figure:
|
|
924
|
-
"""
|
|
925
|
-
Plot interpolated implied volatility surface.
|
|
926
|
-
|
|
927
|
-
Parameters:
|
|
928
|
-
- interp_results: Dictionary with interpolation results
|
|
929
|
-
- title: Plot title
|
|
930
|
-
|
|
931
|
-
Returns:
|
|
932
|
-
- Plotly figure
|
|
933
|
-
"""
|
|
934
|
-
# Extract data from interpolation results
|
|
935
|
-
moneyness_array = interp_results['moneyness_array']
|
|
936
|
-
target_expiries_years = interp_results['target_expiries_years']
|
|
937
|
-
iv_surface = interp_results['iv_surface']
|
|
938
|
-
|
|
939
|
-
# Create a 3D surface plot
|
|
940
|
-
fig = plot_3d_surface(
|
|
941
|
-
moneyness=moneyness_array,
|
|
942
|
-
expiries=target_expiries_years,
|
|
943
|
-
iv_surface=iv_surface,
|
|
944
|
-
title=title
|
|
945
|
-
)
|
|
946
|
-
|
|
947
|
-
return fig
|
|
948
|
-
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
voly/__init__.py,sha256=8xyDk7rFCn_MOD5hxuv5cxxKZvBVRiSIM7TgaMPpwpw,211
|
|
2
|
-
voly/client.py,sha256
|
|
2
|
+
voly/client.py,sha256=-5-eMMYZwyRZcyupkTyNDUDhCnUwCv4NJzqb20_IRy4,20725
|
|
3
3
|
voly/exceptions.py,sha256=PBsbn1vNMvKcCJwwJ4lBO6glD85jo1h2qiEmD7ArAjs,92
|
|
4
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=4tl-FpaWOKhDT6tPNDYtFLAOjekmwu-pV1vDxIIPZ8c,16134
|
|
8
8
|
voly/core/data.py,sha256=e8qBArubNqPkrfuIYB_q2WhRf7TKzA4Z3FhMC-xyLEE,8862
|
|
9
9
|
voly/core/fit.py,sha256=JOr2XjM-I9HtfbyEN0tdGuNCZimQ2ttm4lNUpF-tKb4,9226
|
|
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.70.dist-info/LICENSE,sha256=wcHIVbE12jfcBOai_wqBKY6xvNQU5E909xL1zZNq_2Q,1065
|
|
15
|
+
voly-0.0.70.dist-info/METADATA,sha256=dqgqKzVmRO4xaFLzzgyfZuCZfZgK-SKQy6rOx9sIIUo,4092
|
|
16
|
+
voly-0.0.70.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
|
17
|
+
voly-0.0.70.dist-info/top_level.txt,sha256=ZfLw2sSxF-LrKAkgGjOmeTcw6_gD-30zvtdEY5W4B7c,5
|
|
18
|
+
voly-0.0.70.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|