voly 0.0.86__py3-none-any.whl → 0.0.87__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 +18 -201
- voly/core/charts.py +10 -517
- voly/core/data.py +1 -4
- voly/core/fit.py +34 -42
- voly/core/interpolate.py +5 -6
- voly/core/rnd.py +255 -334
- voly/formulas.py +27 -29
- voly/models.py +0 -2
- {voly-0.0.86.dist-info → voly-0.0.87.dist-info}/METADATA +1 -1
- voly-0.0.87.dist-info/RECORD +18 -0
- {voly-0.0.86.dist-info → voly-0.0.87.dist-info}/WHEEL +1 -1
- voly-0.0.86.dist-info/RECORD +0 -18
- {voly-0.0.86.dist-info → voly-0.0.87.dist-info}/LICENSE +0 -0
- {voly-0.0.86.dist-info → voly-0.0.87.dist-info}/top_level.txt +0 -0
voly/core/charts.py
CHANGED
|
@@ -42,6 +42,7 @@ def plot_volatility_smile(x_array: np.ndarray,
|
|
|
42
42
|
domain_labels = {
|
|
43
43
|
'log_moneyness': 'Log Moneyness',
|
|
44
44
|
'moneyness': 'Moneyness',
|
|
45
|
+
'returns': 'Returns',
|
|
45
46
|
'strikes': 'Strike Price',
|
|
46
47
|
'delta': 'Delta'
|
|
47
48
|
}
|
|
@@ -63,7 +64,7 @@ def plot_volatility_smile(x_array: np.ndarray,
|
|
|
63
64
|
if option_chain is not None and maturity is not None:
|
|
64
65
|
maturity_data = option_chain[option_chain['maturity_name'] == maturity]
|
|
65
66
|
if return_domain == 'delta':
|
|
66
|
-
maturity_data = maturity_data[maturity_data['option_type']=='C']
|
|
67
|
+
maturity_data = maturity_data[maturity_data['option_type'] == 'C']
|
|
67
68
|
|
|
68
69
|
if not maturity_data.empty:
|
|
69
70
|
# Add bid and ask IVs if available
|
|
@@ -79,7 +80,7 @@ def plot_volatility_smile(x_array: np.ndarray,
|
|
|
79
80
|
)
|
|
80
81
|
)
|
|
81
82
|
|
|
82
|
-
title = f'Vol Smile for {maturity}
|
|
83
|
+
title = f'Vol Smile for {maturity}'
|
|
83
84
|
else:
|
|
84
85
|
title = f'Vol Smile for {maturity}'
|
|
85
86
|
else:
|
|
@@ -151,7 +152,7 @@ def plot_raw_parameters(fit_results: pd.DataFrame) -> go.Figure:
|
|
|
151
152
|
maturity_names = fit_results.index
|
|
152
153
|
|
|
153
154
|
# Create hover text with maturity info
|
|
154
|
-
tick_labels = [f"{m}
|
|
155
|
+
tick_labels = [f"{m}" for m in maturity_names]
|
|
155
156
|
|
|
156
157
|
# Plot each parameter
|
|
157
158
|
for i, param in enumerate(param_names):
|
|
@@ -214,7 +215,7 @@ def plot_jw_parameters(fit_results: pd.DataFrame) -> go.Figure:
|
|
|
214
215
|
maturity_names = fit_results.index
|
|
215
216
|
|
|
216
217
|
# Create hover text with maturity info
|
|
217
|
-
tick_labels = [f"{m}
|
|
218
|
+
tick_labels = [f"{m}" for m in maturity_names]
|
|
218
219
|
|
|
219
220
|
# Plot each parameter
|
|
220
221
|
for i, param in enumerate(param_names):
|
|
@@ -282,7 +283,7 @@ def plot_fit_performance(fit_results: pd.DataFrame) -> go.Figure:
|
|
|
282
283
|
x_indices = list(range(len(maturity_names)))
|
|
283
284
|
|
|
284
285
|
# Create hover labels
|
|
285
|
-
hover_labels = [f"{m}
|
|
286
|
+
hover_labels = [f"{m}" for m in maturity_names]
|
|
286
287
|
|
|
287
288
|
# Plot each metric
|
|
288
289
|
for metric, config in metrics.items():
|
|
@@ -345,14 +346,15 @@ def plot_3d_surface(x_surface: Dict[str, np.ndarray],
|
|
|
345
346
|
domain_labels = {
|
|
346
347
|
'log_moneyness': 'Log Moneyness',
|
|
347
348
|
'moneyness': 'Moneyness',
|
|
349
|
+
'returns': 'Returns',
|
|
348
350
|
'strikes': 'Strike Price',
|
|
349
351
|
'delta': 'Delta'
|
|
350
352
|
}
|
|
351
353
|
|
|
352
|
-
# Get maturity names and sort by
|
|
354
|
+
# Get maturity names and sort by YTM
|
|
353
355
|
maturity_names = list(iv_surface.keys())
|
|
354
356
|
if fit_results is not None:
|
|
355
|
-
maturity_values = [fit_results.loc[name, '
|
|
357
|
+
maturity_values = [fit_results.loc[name, 't'] for name in maturity_names]
|
|
356
358
|
# Sort by maturity
|
|
357
359
|
sorted_indices = np.argsort(maturity_values)
|
|
358
360
|
maturity_names = [maturity_names[i] for i in sorted_indices]
|
|
@@ -364,7 +366,7 @@ def plot_3d_surface(x_surface: Dict[str, np.ndarray],
|
|
|
364
366
|
# Use 100 points between the min and max x-values across all maturities
|
|
365
367
|
all_x = np.concatenate([x_surface[m] for m in maturity_names])
|
|
366
368
|
x_min, x_max = np.min(all_x), np.max(all_x)
|
|
367
|
-
x_grid = np.linspace(x_min, x_max,
|
|
369
|
+
x_grid = np.linspace(x_min, x_max, 400)
|
|
368
370
|
|
|
369
371
|
# Create a matrix for the surface
|
|
370
372
|
z_matrix = np.zeros((len(maturity_names), len(x_grid)))
|
|
@@ -411,512 +413,3 @@ def plot_3d_surface(x_surface: Dict[str, np.ndarray],
|
|
|
411
413
|
)
|
|
412
414
|
|
|
413
415
|
return fig
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
@catch_exception
|
|
417
|
-
def plot_rnd(moneyness_array: np.ndarray,
|
|
418
|
-
rnd_values: np.ndarray,
|
|
419
|
-
spot_price: float = 1.0) -> go.Figure:
|
|
420
|
-
"""
|
|
421
|
-
Plot risk-neutral density (RND).
|
|
422
|
-
|
|
423
|
-
Parameters:
|
|
424
|
-
- moneyness_array: Grid of log-moneyness values
|
|
425
|
-
- rnd_values: RND values
|
|
426
|
-
- spot_price: Spot price for converting to absolute prices
|
|
427
|
-
- title: Plot title
|
|
428
|
-
|
|
429
|
-
Returns:
|
|
430
|
-
- Plotly figure
|
|
431
|
-
"""
|
|
432
|
-
# Create figure
|
|
433
|
-
fig = go.Figure()
|
|
434
|
-
|
|
435
|
-
# Convert to prices and normalize RND
|
|
436
|
-
prices = spot_price * np.exp(moneyness_array)
|
|
437
|
-
|
|
438
|
-
# Normalize the RND to integrate to 1
|
|
439
|
-
dx = moneyness_array[1] - moneyness_array[0]
|
|
440
|
-
total_density = np.sum(rnd_values) * dx
|
|
441
|
-
normalized_rnd = rnd_values / total_density if total_density > 0 else rnd_values
|
|
442
|
-
|
|
443
|
-
# Add trace
|
|
444
|
-
fig.add_trace(
|
|
445
|
-
go.Scatter(
|
|
446
|
-
x=prices,
|
|
447
|
-
y=normalized_rnd,
|
|
448
|
-
mode='lines',
|
|
449
|
-
name='RND',
|
|
450
|
-
line=dict(color='#00FFC1', width=2),
|
|
451
|
-
fill='tozeroy',
|
|
452
|
-
fillcolor='rgba(0, 255, 193, 0.2)'
|
|
453
|
-
)
|
|
454
|
-
)
|
|
455
|
-
|
|
456
|
-
# Add vertical line at spot price
|
|
457
|
-
fig.add_shape(
|
|
458
|
-
type='line',
|
|
459
|
-
x0=spot_price, y0=0,
|
|
460
|
-
x1=spot_price, y1=max(normalized_rnd) * 1.1,
|
|
461
|
-
line=dict(color='red', width=2, dash='dash')
|
|
462
|
-
)
|
|
463
|
-
|
|
464
|
-
# Add annotation for spot price
|
|
465
|
-
fig.add_annotation(
|
|
466
|
-
x=spot_price,
|
|
467
|
-
y=max(normalized_rnd) * 1.15,
|
|
468
|
-
text=f"Spot: {spot_price}",
|
|
469
|
-
showarrow=False,
|
|
470
|
-
font=dict(color='red')
|
|
471
|
-
)
|
|
472
|
-
|
|
473
|
-
# Update layout
|
|
474
|
-
fig.update_layout(
|
|
475
|
-
title='Risk-Neutral Density',
|
|
476
|
-
xaxis_title='Price',
|
|
477
|
-
yaxis_title='Density',
|
|
478
|
-
template='plotly_dark',
|
|
479
|
-
showlegend=False
|
|
480
|
-
)
|
|
481
|
-
|
|
482
|
-
return fig
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
@catch_exception
|
|
486
|
-
def plot_rnd_all_expiries(moneyness_array: np.ndarray,
|
|
487
|
-
rnd_surface: Dict[str, np.ndarray],
|
|
488
|
-
fit_results: Dict[str, Any],
|
|
489
|
-
spot_price: float = 1.0) -> go.Figure:
|
|
490
|
-
"""
|
|
491
|
-
Plot risk-neutral densities for all expiries.
|
|
492
|
-
|
|
493
|
-
Parameters:
|
|
494
|
-
- moneyness_array: Grid of log-moneyness values
|
|
495
|
-
- rnd_surface: Dictionary mapping maturity names to RND arrays
|
|
496
|
-
- param_matrix: Matrix containing model parameters with maturity info
|
|
497
|
-
- spot_price: Spot price for converting to absolute prices
|
|
498
|
-
|
|
499
|
-
Returns:
|
|
500
|
-
- Plotly figure
|
|
501
|
-
"""
|
|
502
|
-
# Get maturity information
|
|
503
|
-
dte_values = fit_results['fit_performance']['DTE']
|
|
504
|
-
|
|
505
|
-
# Create figure
|
|
506
|
-
fig = go.Figure()
|
|
507
|
-
|
|
508
|
-
# Get maturity names in order by DTE
|
|
509
|
-
maturity_names = sorted(rnd_surface.keys(), key=lambda x: dte_values[x])
|
|
510
|
-
|
|
511
|
-
# Create color scale from purple to green
|
|
512
|
-
n_maturities = len(maturity_names)
|
|
513
|
-
colors = [f'rgb({int(255 - i * 255 / n_maturities)}, {int(i * 255 / n_maturities)}, 255)'
|
|
514
|
-
for i in range(n_maturities)]
|
|
515
|
-
|
|
516
|
-
# Convert to prices
|
|
517
|
-
prices = spot_price * np.exp(moneyness_array)
|
|
518
|
-
|
|
519
|
-
# Add traces for each expiry
|
|
520
|
-
for i, maturity_name in enumerate(maturity_names):
|
|
521
|
-
rnd = rnd_surface[maturity_name]
|
|
522
|
-
dte = dte_values[maturity_name]
|
|
523
|
-
|
|
524
|
-
# Normalize the RND
|
|
525
|
-
dx = moneyness_array[1] - moneyness_array[0]
|
|
526
|
-
total_density = np.sum(rnd) * dx
|
|
527
|
-
normalized_rnd = rnd / total_density if total_density > 0 else rnd
|
|
528
|
-
|
|
529
|
-
# Add trace
|
|
530
|
-
fig.add_trace(
|
|
531
|
-
go.Scatter(
|
|
532
|
-
x=prices,
|
|
533
|
-
y=normalized_rnd,
|
|
534
|
-
mode='lines',
|
|
535
|
-
name=f"{maturity_name} (DTE: {dte:.1f})",
|
|
536
|
-
line=dict(color=colors[i], width=2),
|
|
537
|
-
)
|
|
538
|
-
)
|
|
539
|
-
|
|
540
|
-
# Add vertical line at spot price
|
|
541
|
-
fig.add_shape(
|
|
542
|
-
type='line',
|
|
543
|
-
x0=spot_price, y0=0,
|
|
544
|
-
x1=spot_price, y1=1, # Will be scaled automatically
|
|
545
|
-
line=dict(color='red', width=2, dash='dash')
|
|
546
|
-
)
|
|
547
|
-
|
|
548
|
-
# Update layout
|
|
549
|
-
fig.update_layout(
|
|
550
|
-
title="Risk-Neutral Densities Across Expiries",
|
|
551
|
-
xaxis_title='Price',
|
|
552
|
-
yaxis_title='Density',
|
|
553
|
-
template='plotly_dark',
|
|
554
|
-
legend=dict(
|
|
555
|
-
yanchor="top",
|
|
556
|
-
y=0.99,
|
|
557
|
-
xanchor="left",
|
|
558
|
-
x=0.01
|
|
559
|
-
)
|
|
560
|
-
)
|
|
561
|
-
|
|
562
|
-
return fig
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
@catch_exception
|
|
566
|
-
def plot_rnd_3d(moneyness_array: np.ndarray,
|
|
567
|
-
rnd_surface: Dict[str, np.ndarray],
|
|
568
|
-
param_matrix: pd.DataFrame,
|
|
569
|
-
spot_price: float = 1.0) -> go.Figure:
|
|
570
|
-
"""
|
|
571
|
-
Plot 3D surface of risk-neutral densities.
|
|
572
|
-
|
|
573
|
-
Parameters:
|
|
574
|
-
- moneyness_array: Grid of log-moneyness values
|
|
575
|
-
- rnd_surface: Dictionary mapping maturity names to RND arrays
|
|
576
|
-
- param_matrix: Matrix containing model parameters with maturity info
|
|
577
|
-
- spot_price: Spot price for converting to absolute prices
|
|
578
|
-
|
|
579
|
-
Returns:
|
|
580
|
-
- Plotly figure
|
|
581
|
-
"""
|
|
582
|
-
# Get maturity information
|
|
583
|
-
dte_values = param_matrix.attrs['dte_values']
|
|
584
|
-
|
|
585
|
-
# Get maturity names in order by DTE
|
|
586
|
-
maturity_names = sorted(rnd_surface.keys(), key=lambda x: dte_values[x])
|
|
587
|
-
|
|
588
|
-
# Extract DTE values for z-axis
|
|
589
|
-
dte_list = [dte_values[name] for name in maturity_names]
|
|
590
|
-
|
|
591
|
-
# Convert to prices
|
|
592
|
-
prices = spot_price * np.exp(moneyness_array)
|
|
593
|
-
|
|
594
|
-
# Create z-data matrix and normalize RNDs
|
|
595
|
-
z_data = np.zeros((len(maturity_names), len(prices)))
|
|
596
|
-
|
|
597
|
-
for i, name in enumerate(maturity_names):
|
|
598
|
-
rnd = rnd_surface[name]
|
|
599
|
-
|
|
600
|
-
# Normalize the RND
|
|
601
|
-
dx = moneyness_array[1] - moneyness_array[0]
|
|
602
|
-
total_density = np.sum(rnd) * dx
|
|
603
|
-
normalized_rnd = rnd / total_density if total_density > 0 else rnd
|
|
604
|
-
|
|
605
|
-
z_data[i] = normalized_rnd
|
|
606
|
-
|
|
607
|
-
# Create mesh grid
|
|
608
|
-
X, Y = np.meshgrid(prices, dte_list)
|
|
609
|
-
|
|
610
|
-
# Create 3D surface
|
|
611
|
-
fig = go.Figure(data=[
|
|
612
|
-
go.Surface(
|
|
613
|
-
z=z_data,
|
|
614
|
-
x=X,
|
|
615
|
-
y=Y,
|
|
616
|
-
colorscale='Viridis',
|
|
617
|
-
showscale=True
|
|
618
|
-
)
|
|
619
|
-
])
|
|
620
|
-
|
|
621
|
-
# Update layout
|
|
622
|
-
fig.update_layout(
|
|
623
|
-
title="3D Risk-Neutral Density Surface",
|
|
624
|
-
scene=dict(
|
|
625
|
-
xaxis_title="Price",
|
|
626
|
-
yaxis_title="Days to Expiry",
|
|
627
|
-
zaxis_title="Density"
|
|
628
|
-
),
|
|
629
|
-
margin=dict(l=65, r=50, b=65, t=90),
|
|
630
|
-
template="plotly_dark"
|
|
631
|
-
)
|
|
632
|
-
|
|
633
|
-
return fig
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
@catch_exception
|
|
637
|
-
def plot_rnd_statistics(rnd_statistics: pd.DataFrame,
|
|
638
|
-
rnd_probabilities: pd.DataFrame) -> Tuple[go.Figure, go.Figure]:
|
|
639
|
-
"""
|
|
640
|
-
Plot RND statistics and probabilities.
|
|
641
|
-
|
|
642
|
-
Parameters:
|
|
643
|
-
- rnd_statistics: DataFrame with RND statistics
|
|
644
|
-
- rnd_probabilities: DataFrame with RND probabilities
|
|
645
|
-
|
|
646
|
-
Returns:
|
|
647
|
-
- Tuple of (statistics_fig, probabilities_fig)
|
|
648
|
-
"""
|
|
649
|
-
# Create subplot figure for key statistics
|
|
650
|
-
stats_fig = make_subplots(
|
|
651
|
-
rows=1, cols=3,
|
|
652
|
-
subplot_titles=("Standard Deviation (%) vs. DTE",
|
|
653
|
-
"Skewness vs. DTE",
|
|
654
|
-
"Excess Kurtosis vs. DTE")
|
|
655
|
-
)
|
|
656
|
-
|
|
657
|
-
# Add traces for each statistic
|
|
658
|
-
stats_fig.add_trace(
|
|
659
|
-
go.Scatter(
|
|
660
|
-
x=rnd_statistics["dte"],
|
|
661
|
-
y=rnd_statistics["std_dev_pct"],
|
|
662
|
-
mode="lines+markers",
|
|
663
|
-
name="Standard Deviation (%)",
|
|
664
|
-
hovertemplate="DTE: %{x:.1f}<br>Std Dev: %{y:.2f}%"
|
|
665
|
-
),
|
|
666
|
-
row=1, col=1
|
|
667
|
-
)
|
|
668
|
-
|
|
669
|
-
stats_fig.add_trace(
|
|
670
|
-
go.Scatter(
|
|
671
|
-
x=rnd_statistics["dte"],
|
|
672
|
-
y=rnd_statistics["skewness"],
|
|
673
|
-
mode="lines+markers",
|
|
674
|
-
name="Skewness",
|
|
675
|
-
hovertemplate="DTE: %{x:.1f}<br>Skewness: %{y:.4f}"
|
|
676
|
-
),
|
|
677
|
-
row=1, col=2
|
|
678
|
-
)
|
|
679
|
-
|
|
680
|
-
stats_fig.add_trace(
|
|
681
|
-
go.Scatter(
|
|
682
|
-
x=rnd_statistics["dte"],
|
|
683
|
-
y=rnd_statistics["excess_kurtosis"],
|
|
684
|
-
mode="lines+markers",
|
|
685
|
-
name="Excess Kurtosis",
|
|
686
|
-
hovertemplate="DTE: %{x:.1f}<br>Excess Kurtosis: %{y:.4f}"
|
|
687
|
-
),
|
|
688
|
-
row=1, col=3
|
|
689
|
-
)
|
|
690
|
-
|
|
691
|
-
# Update layout
|
|
692
|
-
stats_fig.update_layout(
|
|
693
|
-
title="Risk-Neutral Density Statistics Across Expiries",
|
|
694
|
-
template="plotly_dark",
|
|
695
|
-
height=500,
|
|
696
|
-
showlegend=False
|
|
697
|
-
)
|
|
698
|
-
|
|
699
|
-
# Update axes
|
|
700
|
-
stats_fig.update_xaxes(title_text="Days to Expiry", row=1, col=1)
|
|
701
|
-
stats_fig.update_xaxes(title_text="Days to Expiry", row=1, col=2)
|
|
702
|
-
stats_fig.update_xaxes(title_text="Days to Expiry", row=1, col=3)
|
|
703
|
-
|
|
704
|
-
stats_fig.update_yaxes(title_text="Standard Deviation (%)", row=1, col=1)
|
|
705
|
-
stats_fig.update_yaxes(title_text="Skewness", row=1, col=2)
|
|
706
|
-
stats_fig.update_yaxes(title_text="Excess Kurtosis", row=1, col=3)
|
|
707
|
-
|
|
708
|
-
# Create a second figure for probability thresholds
|
|
709
|
-
prob_fig = go.Figure()
|
|
710
|
-
|
|
711
|
-
# Get probability columns (those starting with "p_")
|
|
712
|
-
prob_cols = [col for col in rnd_probabilities.columns if col.startswith("p_")]
|
|
713
|
-
|
|
714
|
-
# Sort the columns to ensure they're in order by threshold value
|
|
715
|
-
prob_cols_above = sorted([col for col in prob_cols if "above" in col],
|
|
716
|
-
key=lambda x: float(x.split("_")[2]))
|
|
717
|
-
prob_cols_below = sorted([col for col in prob_cols if "below" in col],
|
|
718
|
-
key=lambda x: float(x.split("_")[2]))
|
|
719
|
-
|
|
720
|
-
# Color gradients
|
|
721
|
-
green_colors = [
|
|
722
|
-
'rgba(144, 238, 144, 1)', # Light green
|
|
723
|
-
'rgba(50, 205, 50, 1)', # Lime green
|
|
724
|
-
'rgba(34, 139, 34, 1)', # Forest green
|
|
725
|
-
'rgba(0, 100, 0, 1)' # Dark green
|
|
726
|
-
]
|
|
727
|
-
|
|
728
|
-
red_colors = [
|
|
729
|
-
'rgba(139, 0, 0, 1)', # Dark red
|
|
730
|
-
'rgba(220, 20, 60, 1)', # Crimson
|
|
731
|
-
'rgba(240, 128, 128, 1)', # Light coral
|
|
732
|
-
'rgba(255, 182, 193, 1)' # Light pink/red
|
|
733
|
-
]
|
|
734
|
-
|
|
735
|
-
# Add lines for upside probabilities (green)
|
|
736
|
-
for i, col in enumerate(prob_cols_above):
|
|
737
|
-
threshold = float(col.split("_")[2])
|
|
738
|
-
label = f"P(X > {threshold})"
|
|
739
|
-
|
|
740
|
-
# Select color based on how far OTM
|
|
741
|
-
color_idx = min(i, len(green_colors) - 1)
|
|
742
|
-
|
|
743
|
-
prob_fig.add_trace(
|
|
744
|
-
go.Scatter(
|
|
745
|
-
x=rnd_probabilities["dte"],
|
|
746
|
-
y=rnd_probabilities[col] * 100, # Convert to percentage
|
|
747
|
-
mode="lines+markers",
|
|
748
|
-
name=label,
|
|
749
|
-
line=dict(color=green_colors[color_idx], width=3),
|
|
750
|
-
marker=dict(size=8, color=green_colors[color_idx]),
|
|
751
|
-
hovertemplate="DTE: %{x:.1f}<br>" + label + ": %{y:.2f}%"
|
|
752
|
-
)
|
|
753
|
-
)
|
|
754
|
-
|
|
755
|
-
# Add lines for downside probabilities (red)
|
|
756
|
-
for i, col in enumerate(prob_cols_below):
|
|
757
|
-
threshold = float(col.split("_")[2])
|
|
758
|
-
label = f"P(X < {threshold})"
|
|
759
|
-
|
|
760
|
-
# Select color based on how far OTM
|
|
761
|
-
color_idx = min(i, len(red_colors) - 1)
|
|
762
|
-
|
|
763
|
-
prob_fig.add_trace(
|
|
764
|
-
go.Scatter(
|
|
765
|
-
x=rnd_probabilities["dte"],
|
|
766
|
-
y=rnd_probabilities[col] * 100, # Convert to percentage
|
|
767
|
-
mode="lines+markers",
|
|
768
|
-
name=label,
|
|
769
|
-
line=dict(color=red_colors[color_idx], width=3),
|
|
770
|
-
marker=dict(size=8, color=red_colors[color_idx]),
|
|
771
|
-
hovertemplate="DTE: %{x:.1f}<br>" + label + ": %{y:.2f}%"
|
|
772
|
-
)
|
|
773
|
-
)
|
|
774
|
-
|
|
775
|
-
# Update layout
|
|
776
|
-
prob_fig.update_layout(
|
|
777
|
-
title="Probability Thresholds Across Expiries",
|
|
778
|
-
xaxis_title="Days to Expiry",
|
|
779
|
-
yaxis_title="Probability (%)",
|
|
780
|
-
template="plotly_dark",
|
|
781
|
-
legend=dict(
|
|
782
|
-
yanchor="top",
|
|
783
|
-
y=0.99,
|
|
784
|
-
xanchor="right",
|
|
785
|
-
x=0.99
|
|
786
|
-
)
|
|
787
|
-
)
|
|
788
|
-
|
|
789
|
-
return stats_fig, prob_fig
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
@catch_exception
|
|
793
|
-
def plot_cdf(moneyness_array: np.ndarray,
|
|
794
|
-
rnd_values: np.ndarray,
|
|
795
|
-
spot_price: float = 1.0,
|
|
796
|
-
title: str = 'Cumulative Distribution Function') -> go.Figure:
|
|
797
|
-
"""
|
|
798
|
-
Plot the cumulative distribution function (CDF) from RND values.
|
|
799
|
-
|
|
800
|
-
Parameters:
|
|
801
|
-
- moneyness_array: Grid of log-moneyness values
|
|
802
|
-
- rnd_values: RND values
|
|
803
|
-
- spot_price: Spot price for converting to absolute prices
|
|
804
|
-
- title: Plot title
|
|
805
|
-
|
|
806
|
-
Returns:
|
|
807
|
-
- Plotly figure
|
|
808
|
-
"""
|
|
809
|
-
# Convert to prices and normalize RND
|
|
810
|
-
prices = spot_price * np.exp(moneyness_array)
|
|
811
|
-
|
|
812
|
-
# Normalize the RND
|
|
813
|
-
dx = moneyness_array[1] - moneyness_array[0]
|
|
814
|
-
total_density = np.sum(rnd_values) * dx
|
|
815
|
-
normalized_rnd = rnd_values / total_density if total_density > 0 else rnd_values
|
|
816
|
-
|
|
817
|
-
# Calculate CDF
|
|
818
|
-
cdf = np.cumsum(normalized_rnd) * dx
|
|
819
|
-
|
|
820
|
-
# Create figure
|
|
821
|
-
fig = go.Figure()
|
|
822
|
-
|
|
823
|
-
# Add CDF trace
|
|
824
|
-
fig.add_trace(
|
|
825
|
-
go.Scatter(
|
|
826
|
-
x=prices,
|
|
827
|
-
y=cdf,
|
|
828
|
-
mode='lines',
|
|
829
|
-
name='CDF',
|
|
830
|
-
line=dict(color='#00FFC1', width=2)
|
|
831
|
-
)
|
|
832
|
-
)
|
|
833
|
-
|
|
834
|
-
# Add vertical line at spot price
|
|
835
|
-
fig.add_shape(
|
|
836
|
-
type='line',
|
|
837
|
-
x0=spot_price, y0=0,
|
|
838
|
-
x1=spot_price, y1=1,
|
|
839
|
-
line=dict(color='red', width=2, dash='dash')
|
|
840
|
-
)
|
|
841
|
-
|
|
842
|
-
# Add horizontal line at CDF=0.5 (median)
|
|
843
|
-
fig.add_shape(
|
|
844
|
-
type='line',
|
|
845
|
-
x0=prices[0], y0=0.5,
|
|
846
|
-
x1=prices[-1], y1=0.5,
|
|
847
|
-
line=dict(color='orange', width=2, dash='dash')
|
|
848
|
-
)
|
|
849
|
-
|
|
850
|
-
# Add annotation for spot price
|
|
851
|
-
fig.add_annotation(
|
|
852
|
-
x=spot_price,
|
|
853
|
-
y=1.05,
|
|
854
|
-
text=f"Spot: {spot_price}",
|
|
855
|
-
showarrow=False,
|
|
856
|
-
font=dict(color='red')
|
|
857
|
-
)
|
|
858
|
-
|
|
859
|
-
# Update layout
|
|
860
|
-
fig.update_layout(
|
|
861
|
-
title=title,
|
|
862
|
-
xaxis_title='Price',
|
|
863
|
-
yaxis_title='Cumulative Probability',
|
|
864
|
-
template='plotly_dark',
|
|
865
|
-
yaxis=dict(range=[0, 1.1]),
|
|
866
|
-
showlegend=False
|
|
867
|
-
)
|
|
868
|
-
|
|
869
|
-
return fig
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
@catch_exception
|
|
873
|
-
def plot_pdf(moneyness_array: np.ndarray,
|
|
874
|
-
rnd_values: np.ndarray,
|
|
875
|
-
spot_price: float = 1.0,
|
|
876
|
-
title: str = 'Probability Density Function') -> go.Figure:
|
|
877
|
-
"""
|
|
878
|
-
Plot the probability density function (PDF) from RND values.
|
|
879
|
-
|
|
880
|
-
Parameters:
|
|
881
|
-
- moneyness_array: Grid of log-moneyness values
|
|
882
|
-
- rnd_values: RND values
|
|
883
|
-
- spot_price: Spot price for converting to absolute prices
|
|
884
|
-
- title: Plot title
|
|
885
|
-
|
|
886
|
-
Returns:
|
|
887
|
-
- Plotly figure
|
|
888
|
-
"""
|
|
889
|
-
# This is essentially the same as plot_rnd but with a different title
|
|
890
|
-
return plot_rnd(moneyness_array, rnd_values, spot_price, title)
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
@catch_exception
|
|
894
|
-
def plot_interpolated_surface(
|
|
895
|
-
interp_results: Dict[str, Any],
|
|
896
|
-
title: str = 'Interpolated Implied Volatility Surface'
|
|
897
|
-
) -> go.Figure:
|
|
898
|
-
"""
|
|
899
|
-
Plot interpolated implied volatility surface.
|
|
900
|
-
|
|
901
|
-
Parameters:
|
|
902
|
-
- interp_results: Dictionary with interpolation results
|
|
903
|
-
- title: Plot title
|
|
904
|
-
|
|
905
|
-
Returns:
|
|
906
|
-
- Plotly figure
|
|
907
|
-
"""
|
|
908
|
-
# Extract data from interpolation results
|
|
909
|
-
moneyness_array = interp_results['moneyness_array']
|
|
910
|
-
target_expiries_years = interp_results['target_expiries_years']
|
|
911
|
-
iv_surface = interp_results['iv_surface']
|
|
912
|
-
|
|
913
|
-
# Create a 3D surface plot
|
|
914
|
-
fig = plot_3d_surface(
|
|
915
|
-
moneyness=moneyness_array,
|
|
916
|
-
expiries=target_expiries_years,
|
|
917
|
-
iv_surface=iv_surface,
|
|
918
|
-
title=title
|
|
919
|
-
)
|
|
920
|
-
|
|
921
|
-
return fig
|
|
922
|
-
|
voly/core/data.py
CHANGED
|
@@ -212,11 +212,8 @@ def process_option_chain(df: pd.DataFrame, currency: str) -> pd.DataFrame:
|
|
|
212
212
|
# Get reference time from timestamp
|
|
213
213
|
reference_time = dt.datetime.fromtimestamp(df['timestamp'].iloc[0] / 1000)
|
|
214
214
|
|
|
215
|
-
# Calculate days to expiry (DTE)
|
|
216
|
-
df['dtm'] = (df['maturity_date'] - reference_time).dt.total_seconds() / (24 * 60 * 60)
|
|
217
|
-
|
|
218
215
|
# Calculate time to expiry in years
|
|
219
|
-
df['
|
|
216
|
+
df['t'] = ((df['maturity_date'] - reference_time).dt.total_seconds() / (24 * 60 * 60)) / 365.25
|
|
220
217
|
|
|
221
218
|
# Calculate implied volatility (convert from percentage)
|
|
222
219
|
df['mark_iv'] = df['mark_iv'] / 100
|