voly 0.0.70__tar.gz → 0.0.72__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {voly-0.0.70/src/voly.egg-info → voly-0.0.72}/PKG-INFO +1 -1
- {voly-0.0.70 → voly-0.0.72}/pyproject.toml +2 -2
- {voly-0.0.70 → voly-0.0.72}/src/voly/client.py +2 -2
- {voly-0.0.70 → voly-0.0.72}/src/voly/core/charts.py +450 -12
- {voly-0.0.70 → voly-0.0.72/src/voly.egg-info}/PKG-INFO +1 -1
- {voly-0.0.70 → voly-0.0.72}/LICENSE +0 -0
- {voly-0.0.70 → voly-0.0.72}/README.md +0 -0
- {voly-0.0.70 → voly-0.0.72}/setup.cfg +0 -0
- {voly-0.0.70 → voly-0.0.72}/setup.py +0 -0
- {voly-0.0.70 → voly-0.0.72}/src/voly/__init__.py +0 -0
- {voly-0.0.70 → voly-0.0.72}/src/voly/core/__init__.py +0 -0
- {voly-0.0.70 → voly-0.0.72}/src/voly/core/data.py +0 -0
- {voly-0.0.70 → voly-0.0.72}/src/voly/core/fit.py +0 -0
- {voly-0.0.70 → voly-0.0.72}/src/voly/core/interpolate.py +0 -0
- {voly-0.0.70 → voly-0.0.72}/src/voly/core/rnd.py +0 -0
- {voly-0.0.70 → voly-0.0.72}/src/voly/exceptions.py +0 -0
- {voly-0.0.70 → voly-0.0.72}/src/voly/formulas.py +0 -0
- {voly-0.0.70 → voly-0.0.72}/src/voly/models.py +0 -0
- {voly-0.0.70 → voly-0.0.72}/src/voly/utils/__init__.py +0 -0
- {voly-0.0.70 → voly-0.0.72}/src/voly/utils/logger.py +0 -0
- {voly-0.0.70 → voly-0.0.72}/src/voly.egg-info/SOURCES.txt +0 -0
- {voly-0.0.70 → voly-0.0.72}/src/voly.egg-info/dependency_links.txt +0 -0
- {voly-0.0.70 → voly-0.0.72}/src/voly.egg-info/requires.txt +0 -0
- {voly-0.0.70 → voly-0.0.72}/src/voly.egg-info/top_level.txt +0 -0
- {voly-0.0.70 → voly-0.0.72}/tests/test_client.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "voly"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.72"
|
|
8
8
|
description = "Options & volatility research package"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -60,7 +60,7 @@ line_length = 100
|
|
|
60
60
|
multi_line_output = 3
|
|
61
61
|
|
|
62
62
|
[tool.mypy]
|
|
63
|
-
python_version = "0.0.
|
|
63
|
+
python_version = "0.0.72"
|
|
64
64
|
warn_return_any = true
|
|
65
65
|
warn_unused_configs = true
|
|
66
66
|
disallow_untyped_defs = true
|
|
@@ -22,7 +22,7 @@ from voly.core.fit import fit_model, get_iv_surface
|
|
|
22
22
|
from voly.core.rnd import get_rnd_surface, calculate_pdf, calculate_cdf, calculate_strike_probability
|
|
23
23
|
from voly.core.interpolate import interpolate_model
|
|
24
24
|
from voly.core.charts import (
|
|
25
|
-
plot_all_smiles,
|
|
25
|
+
plot_all_smiles, plot_raw_parameters, plot_jw_parameters, plot_fit_performance, plot_3d_surface,
|
|
26
26
|
plot_fit_performance, plot_rnd, plot_pdf, plot_cdf, plot_rnd_all_expiries,
|
|
27
27
|
plot_rnd_3d, plot_rnd_statistics, plot_interpolated_surface
|
|
28
28
|
)
|
|
@@ -404,7 +404,7 @@ class VolyClient:
|
|
|
404
404
|
)
|
|
405
405
|
|
|
406
406
|
# Plot parameters
|
|
407
|
-
plots['raw_params'] =
|
|
407
|
+
plots['raw_params'] = plot_raw_parameters(fit_results)
|
|
408
408
|
plots['jw_params'] = plot_jw_parameters(fit_results)
|
|
409
409
|
|
|
410
410
|
# Plot fit statistics
|
|
@@ -141,7 +141,7 @@ def plot_all_smiles(x_surface: Dict[str, np.ndarray],
|
|
|
141
141
|
|
|
142
142
|
|
|
143
143
|
@catch_exception
|
|
144
|
-
def
|
|
144
|
+
def plot_raw_parameters(fit_results: pd.DataFrame) -> go.Figure:
|
|
145
145
|
"""
|
|
146
146
|
Plot raw SVI parameters across different expiries.
|
|
147
147
|
|
|
@@ -165,7 +165,7 @@ def plot_parameters(fit_results: pd.DataFrame) -> go.Figure:
|
|
|
165
165
|
maturity_names = fit_results.index
|
|
166
166
|
|
|
167
167
|
# Create hover text with maturity info
|
|
168
|
-
tick_labels = [f"{m} (DTE: {fit_results.loc[m, 'dtm']:.1f}
|
|
168
|
+
tick_labels = [f"{m} (DTE: {fit_results.loc[m, 'dtm']:.1f}"
|
|
169
169
|
for m in maturity_names]
|
|
170
170
|
|
|
171
171
|
# Plot each parameter
|
|
@@ -450,17 +450,16 @@ def plot_3d_surface(x_surface: Dict[str, np.ndarray],
|
|
|
450
450
|
|
|
451
451
|
|
|
452
452
|
@catch_exception
|
|
453
|
-
def plot_rnd(
|
|
453
|
+
def plot_rnd(moneyness_array: np.ndarray,
|
|
454
454
|
rnd_values: np.ndarray,
|
|
455
|
-
spot_price: float = 1.0
|
|
456
|
-
title: str = 'Risk-Neutral Density') -> go.Figure:
|
|
455
|
+
spot_price: float = 1.0) -> go.Figure:
|
|
457
456
|
"""
|
|
458
457
|
Plot risk-neutral density (RND).
|
|
459
458
|
|
|
460
459
|
Parameters:
|
|
461
|
-
-
|
|
460
|
+
- moneyness_array: Grid of log-moneyness values
|
|
462
461
|
- rnd_values: RND values
|
|
463
|
-
- spot_price: Spot price for
|
|
462
|
+
- spot_price: Spot price for converting to absolute prices
|
|
464
463
|
- title: Plot title
|
|
465
464
|
|
|
466
465
|
Returns:
|
|
@@ -469,12 +468,11 @@ def plot_rnd(x_domain: np.ndarray,
|
|
|
469
468
|
# Create figure
|
|
470
469
|
fig = go.Figure()
|
|
471
470
|
|
|
472
|
-
# Convert
|
|
473
|
-
|
|
474
|
-
prices = spot_price * np.exp(x_domain)
|
|
471
|
+
# Convert to prices and normalize RND
|
|
472
|
+
prices = spot_price * np.exp(moneyness_array)
|
|
475
473
|
|
|
476
474
|
# Normalize the RND to integrate to 1
|
|
477
|
-
dx =
|
|
475
|
+
dx = moneyness_array[1] - moneyness_array[0]
|
|
478
476
|
total_density = np.sum(rnd_values) * dx
|
|
479
477
|
normalized_rnd = rnd_values / total_density if total_density > 0 else rnd_values
|
|
480
478
|
|
|
@@ -510,7 +508,7 @@ def plot_rnd(x_domain: np.ndarray,
|
|
|
510
508
|
|
|
511
509
|
# Update layout
|
|
512
510
|
fig.update_layout(
|
|
513
|
-
title=
|
|
511
|
+
title='Risk-Neutral Density',
|
|
514
512
|
xaxis_title='Price',
|
|
515
513
|
yaxis_title='Density',
|
|
516
514
|
template='plotly_dark',
|
|
@@ -518,3 +516,443 @@ def plot_rnd(x_domain: np.ndarray,
|
|
|
518
516
|
)
|
|
519
517
|
|
|
520
518
|
return fig
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
@catch_exception
|
|
522
|
+
def plot_rnd_all_expiries(moneyness_array: np.ndarray,
|
|
523
|
+
rnd_surface: Dict[str, np.ndarray],
|
|
524
|
+
fit_results: Dict[str, Any],
|
|
525
|
+
spot_price: float = 1.0) -> go.Figure:
|
|
526
|
+
"""
|
|
527
|
+
Plot risk-neutral densities for all expiries.
|
|
528
|
+
|
|
529
|
+
Parameters:
|
|
530
|
+
- moneyness_array: Grid of log-moneyness values
|
|
531
|
+
- rnd_surface: Dictionary mapping maturity names to RND arrays
|
|
532
|
+
- param_matrix: Matrix containing model parameters with maturity info
|
|
533
|
+
- spot_price: Spot price for converting to absolute prices
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
- Plotly figure
|
|
537
|
+
"""
|
|
538
|
+
# Get maturity information
|
|
539
|
+
dte_values = fit_results['fit_performance']['DTE']
|
|
540
|
+
|
|
541
|
+
# Create figure
|
|
542
|
+
fig = go.Figure()
|
|
543
|
+
|
|
544
|
+
# Get maturity names in order by DTE
|
|
545
|
+
maturity_names = sorted(rnd_surface.keys(), key=lambda x: dte_values[x])
|
|
546
|
+
|
|
547
|
+
# Create color scale from purple to green
|
|
548
|
+
n_maturities = len(maturity_names)
|
|
549
|
+
colors = [f'rgb({int(255 - i * 255 / n_maturities)}, {int(i * 255 / n_maturities)}, 255)'
|
|
550
|
+
for i in range(n_maturities)]
|
|
551
|
+
|
|
552
|
+
# Convert to prices
|
|
553
|
+
prices = spot_price * np.exp(moneyness_array)
|
|
554
|
+
|
|
555
|
+
# Add traces for each expiry
|
|
556
|
+
for i, maturity_name in enumerate(maturity_names):
|
|
557
|
+
rnd = rnd_surface[maturity_name]
|
|
558
|
+
dte = dte_values[maturity_name]
|
|
559
|
+
|
|
560
|
+
# Normalize the RND
|
|
561
|
+
dx = moneyness_array[1] - moneyness_array[0]
|
|
562
|
+
total_density = np.sum(rnd) * dx
|
|
563
|
+
normalized_rnd = rnd / total_density if total_density > 0 else rnd
|
|
564
|
+
|
|
565
|
+
# Add trace
|
|
566
|
+
fig.add_trace(
|
|
567
|
+
go.Scatter(
|
|
568
|
+
x=prices,
|
|
569
|
+
y=normalized_rnd,
|
|
570
|
+
mode='lines',
|
|
571
|
+
name=f"{maturity_name} (DTE: {dte:.1f})",
|
|
572
|
+
line=dict(color=colors[i], width=2),
|
|
573
|
+
)
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
# Add vertical line at spot price
|
|
577
|
+
fig.add_shape(
|
|
578
|
+
type='line',
|
|
579
|
+
x0=spot_price, y0=0,
|
|
580
|
+
x1=spot_price, y1=1, # Will be scaled automatically
|
|
581
|
+
line=dict(color='red', width=2, dash='dash')
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
# Update layout
|
|
585
|
+
fig.update_layout(
|
|
586
|
+
title="Risk-Neutral Densities Across Expiries",
|
|
587
|
+
xaxis_title='Price',
|
|
588
|
+
yaxis_title='Density',
|
|
589
|
+
template='plotly_dark',
|
|
590
|
+
legend=dict(
|
|
591
|
+
yanchor="top",
|
|
592
|
+
y=0.99,
|
|
593
|
+
xanchor="left",
|
|
594
|
+
x=0.01
|
|
595
|
+
)
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
return fig
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
@catch_exception
|
|
602
|
+
def plot_rnd_3d(moneyness_array: np.ndarray,
|
|
603
|
+
rnd_surface: Dict[str, np.ndarray],
|
|
604
|
+
param_matrix: pd.DataFrame,
|
|
605
|
+
spot_price: float = 1.0) -> go.Figure:
|
|
606
|
+
"""
|
|
607
|
+
Plot 3D surface of risk-neutral densities.
|
|
608
|
+
|
|
609
|
+
Parameters:
|
|
610
|
+
- moneyness_array: Grid of log-moneyness values
|
|
611
|
+
- rnd_surface: Dictionary mapping maturity names to RND arrays
|
|
612
|
+
- param_matrix: Matrix containing model parameters with maturity info
|
|
613
|
+
- spot_price: Spot price for converting to absolute prices
|
|
614
|
+
|
|
615
|
+
Returns:
|
|
616
|
+
- Plotly figure
|
|
617
|
+
"""
|
|
618
|
+
# Get maturity information
|
|
619
|
+
dte_values = param_matrix.attrs['dte_values']
|
|
620
|
+
|
|
621
|
+
# Get maturity names in order by DTE
|
|
622
|
+
maturity_names = sorted(rnd_surface.keys(), key=lambda x: dte_values[x])
|
|
623
|
+
|
|
624
|
+
# Extract DTE values for z-axis
|
|
625
|
+
dte_list = [dte_values[name] for name in maturity_names]
|
|
626
|
+
|
|
627
|
+
# Convert to prices
|
|
628
|
+
prices = spot_price * np.exp(moneyness_array)
|
|
629
|
+
|
|
630
|
+
# Create z-data matrix and normalize RNDs
|
|
631
|
+
z_data = np.zeros((len(maturity_names), len(prices)))
|
|
632
|
+
|
|
633
|
+
for i, name in enumerate(maturity_names):
|
|
634
|
+
rnd = rnd_surface[name]
|
|
635
|
+
|
|
636
|
+
# Normalize the RND
|
|
637
|
+
dx = moneyness_array[1] - moneyness_array[0]
|
|
638
|
+
total_density = np.sum(rnd) * dx
|
|
639
|
+
normalized_rnd = rnd / total_density if total_density > 0 else rnd
|
|
640
|
+
|
|
641
|
+
z_data[i] = normalized_rnd
|
|
642
|
+
|
|
643
|
+
# Create mesh grid
|
|
644
|
+
X, Y = np.meshgrid(prices, dte_list)
|
|
645
|
+
|
|
646
|
+
# Create 3D surface
|
|
647
|
+
fig = go.Figure(data=[
|
|
648
|
+
go.Surface(
|
|
649
|
+
z=z_data,
|
|
650
|
+
x=X,
|
|
651
|
+
y=Y,
|
|
652
|
+
colorscale='Viridis',
|
|
653
|
+
showscale=True
|
|
654
|
+
)
|
|
655
|
+
])
|
|
656
|
+
|
|
657
|
+
# Update layout
|
|
658
|
+
fig.update_layout(
|
|
659
|
+
title="3D Risk-Neutral Density Surface",
|
|
660
|
+
scene=dict(
|
|
661
|
+
xaxis_title="Price",
|
|
662
|
+
yaxis_title="Days to Expiry",
|
|
663
|
+
zaxis_title="Density"
|
|
664
|
+
),
|
|
665
|
+
margin=dict(l=65, r=50, b=65, t=90),
|
|
666
|
+
template="plotly_dark"
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
return fig
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
@catch_exception
|
|
673
|
+
def plot_rnd_statistics(rnd_statistics: pd.DataFrame,
|
|
674
|
+
rnd_probabilities: pd.DataFrame) -> Tuple[go.Figure, go.Figure]:
|
|
675
|
+
"""
|
|
676
|
+
Plot RND statistics and probabilities.
|
|
677
|
+
|
|
678
|
+
Parameters:
|
|
679
|
+
- rnd_statistics: DataFrame with RND statistics
|
|
680
|
+
- rnd_probabilities: DataFrame with RND probabilities
|
|
681
|
+
|
|
682
|
+
Returns:
|
|
683
|
+
- Tuple of (statistics_fig, probabilities_fig)
|
|
684
|
+
"""
|
|
685
|
+
# Create subplot figure for key statistics
|
|
686
|
+
stats_fig = make_subplots(
|
|
687
|
+
rows=1, cols=3,
|
|
688
|
+
subplot_titles=("Standard Deviation (%) vs. DTE",
|
|
689
|
+
"Skewness vs. DTE",
|
|
690
|
+
"Excess Kurtosis vs. DTE")
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
# Add traces for each statistic
|
|
694
|
+
stats_fig.add_trace(
|
|
695
|
+
go.Scatter(
|
|
696
|
+
x=rnd_statistics["dte"],
|
|
697
|
+
y=rnd_statistics["std_dev_pct"],
|
|
698
|
+
mode="lines+markers",
|
|
699
|
+
name="Standard Deviation (%)",
|
|
700
|
+
hovertemplate="DTE: %{x:.1f}<br>Std Dev: %{y:.2f}%"
|
|
701
|
+
),
|
|
702
|
+
row=1, col=1
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
stats_fig.add_trace(
|
|
706
|
+
go.Scatter(
|
|
707
|
+
x=rnd_statistics["dte"],
|
|
708
|
+
y=rnd_statistics["skewness"],
|
|
709
|
+
mode="lines+markers",
|
|
710
|
+
name="Skewness",
|
|
711
|
+
hovertemplate="DTE: %{x:.1f}<br>Skewness: %{y:.4f}"
|
|
712
|
+
),
|
|
713
|
+
row=1, col=2
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
stats_fig.add_trace(
|
|
717
|
+
go.Scatter(
|
|
718
|
+
x=rnd_statistics["dte"],
|
|
719
|
+
y=rnd_statistics["excess_kurtosis"],
|
|
720
|
+
mode="lines+markers",
|
|
721
|
+
name="Excess Kurtosis",
|
|
722
|
+
hovertemplate="DTE: %{x:.1f}<br>Excess Kurtosis: %{y:.4f}"
|
|
723
|
+
),
|
|
724
|
+
row=1, col=3
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
# Update layout
|
|
728
|
+
stats_fig.update_layout(
|
|
729
|
+
title="Risk-Neutral Density Statistics Across Expiries",
|
|
730
|
+
template="plotly_dark",
|
|
731
|
+
height=500,
|
|
732
|
+
showlegend=False
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
# Update axes
|
|
736
|
+
stats_fig.update_xaxes(title_text="Days to Expiry", row=1, col=1)
|
|
737
|
+
stats_fig.update_xaxes(title_text="Days to Expiry", row=1, col=2)
|
|
738
|
+
stats_fig.update_xaxes(title_text="Days to Expiry", row=1, col=3)
|
|
739
|
+
|
|
740
|
+
stats_fig.update_yaxes(title_text="Standard Deviation (%)", row=1, col=1)
|
|
741
|
+
stats_fig.update_yaxes(title_text="Skewness", row=1, col=2)
|
|
742
|
+
stats_fig.update_yaxes(title_text="Excess Kurtosis", row=1, col=3)
|
|
743
|
+
|
|
744
|
+
# Create a second figure for probability thresholds
|
|
745
|
+
prob_fig = go.Figure()
|
|
746
|
+
|
|
747
|
+
# Get probability columns (those starting with "p_")
|
|
748
|
+
prob_cols = [col for col in rnd_probabilities.columns if col.startswith("p_")]
|
|
749
|
+
|
|
750
|
+
# Sort the columns to ensure they're in order by threshold value
|
|
751
|
+
prob_cols_above = sorted([col for col in prob_cols if "above" in col],
|
|
752
|
+
key=lambda x: float(x.split("_")[2]))
|
|
753
|
+
prob_cols_below = sorted([col for col in prob_cols if "below" in col],
|
|
754
|
+
key=lambda x: float(x.split("_")[2]))
|
|
755
|
+
|
|
756
|
+
# Color gradients
|
|
757
|
+
green_colors = [
|
|
758
|
+
'rgba(144, 238, 144, 1)', # Light green
|
|
759
|
+
'rgba(50, 205, 50, 1)', # Lime green
|
|
760
|
+
'rgba(34, 139, 34, 1)', # Forest green
|
|
761
|
+
'rgba(0, 100, 0, 1)' # Dark green
|
|
762
|
+
]
|
|
763
|
+
|
|
764
|
+
red_colors = [
|
|
765
|
+
'rgba(139, 0, 0, 1)', # Dark red
|
|
766
|
+
'rgba(220, 20, 60, 1)', # Crimson
|
|
767
|
+
'rgba(240, 128, 128, 1)', # Light coral
|
|
768
|
+
'rgba(255, 182, 193, 1)' # Light pink/red
|
|
769
|
+
]
|
|
770
|
+
|
|
771
|
+
# Add lines for upside probabilities (green)
|
|
772
|
+
for i, col in enumerate(prob_cols_above):
|
|
773
|
+
threshold = float(col.split("_")[2])
|
|
774
|
+
label = f"P(X > {threshold})"
|
|
775
|
+
|
|
776
|
+
# Select color based on how far OTM
|
|
777
|
+
color_idx = min(i, len(green_colors) - 1)
|
|
778
|
+
|
|
779
|
+
prob_fig.add_trace(
|
|
780
|
+
go.Scatter(
|
|
781
|
+
x=rnd_probabilities["dte"],
|
|
782
|
+
y=rnd_probabilities[col] * 100, # Convert to percentage
|
|
783
|
+
mode="lines+markers",
|
|
784
|
+
name=label,
|
|
785
|
+
line=dict(color=green_colors[color_idx], width=3),
|
|
786
|
+
marker=dict(size=8, color=green_colors[color_idx]),
|
|
787
|
+
hovertemplate="DTE: %{x:.1f}<br>" + label + ": %{y:.2f}%"
|
|
788
|
+
)
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
# Add lines for downside probabilities (red)
|
|
792
|
+
for i, col in enumerate(prob_cols_below):
|
|
793
|
+
threshold = float(col.split("_")[2])
|
|
794
|
+
label = f"P(X < {threshold})"
|
|
795
|
+
|
|
796
|
+
# Select color based on how far OTM
|
|
797
|
+
color_idx = min(i, len(red_colors) - 1)
|
|
798
|
+
|
|
799
|
+
prob_fig.add_trace(
|
|
800
|
+
go.Scatter(
|
|
801
|
+
x=rnd_probabilities["dte"],
|
|
802
|
+
y=rnd_probabilities[col] * 100, # Convert to percentage
|
|
803
|
+
mode="lines+markers",
|
|
804
|
+
name=label,
|
|
805
|
+
line=dict(color=red_colors[color_idx], width=3),
|
|
806
|
+
marker=dict(size=8, color=red_colors[color_idx]),
|
|
807
|
+
hovertemplate="DTE: %{x:.1f}<br>" + label + ": %{y:.2f}%"
|
|
808
|
+
)
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
# Update layout
|
|
812
|
+
prob_fig.update_layout(
|
|
813
|
+
title="Probability Thresholds Across Expiries",
|
|
814
|
+
xaxis_title="Days to Expiry",
|
|
815
|
+
yaxis_title="Probability (%)",
|
|
816
|
+
template="plotly_dark",
|
|
817
|
+
legend=dict(
|
|
818
|
+
yanchor="top",
|
|
819
|
+
y=0.99,
|
|
820
|
+
xanchor="right",
|
|
821
|
+
x=0.99
|
|
822
|
+
)
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
return stats_fig, prob_fig
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
@catch_exception
|
|
829
|
+
def plot_cdf(moneyness_array: np.ndarray,
|
|
830
|
+
rnd_values: np.ndarray,
|
|
831
|
+
spot_price: float = 1.0,
|
|
832
|
+
title: str = 'Cumulative Distribution Function') -> go.Figure:
|
|
833
|
+
"""
|
|
834
|
+
Plot the cumulative distribution function (CDF) from RND values.
|
|
835
|
+
|
|
836
|
+
Parameters:
|
|
837
|
+
- moneyness_array: Grid of log-moneyness values
|
|
838
|
+
- rnd_values: RND values
|
|
839
|
+
- spot_price: Spot price for converting to absolute prices
|
|
840
|
+
- title: Plot title
|
|
841
|
+
|
|
842
|
+
Returns:
|
|
843
|
+
- Plotly figure
|
|
844
|
+
"""
|
|
845
|
+
# Convert to prices and normalize RND
|
|
846
|
+
prices = spot_price * np.exp(moneyness_array)
|
|
847
|
+
|
|
848
|
+
# Normalize the RND
|
|
849
|
+
dx = moneyness_array[1] - moneyness_array[0]
|
|
850
|
+
total_density = np.sum(rnd_values) * dx
|
|
851
|
+
normalized_rnd = rnd_values / total_density if total_density > 0 else rnd_values
|
|
852
|
+
|
|
853
|
+
# Calculate CDF
|
|
854
|
+
cdf = np.cumsum(normalized_rnd) * dx
|
|
855
|
+
|
|
856
|
+
# Create figure
|
|
857
|
+
fig = go.Figure()
|
|
858
|
+
|
|
859
|
+
# Add CDF trace
|
|
860
|
+
fig.add_trace(
|
|
861
|
+
go.Scatter(
|
|
862
|
+
x=prices,
|
|
863
|
+
y=cdf,
|
|
864
|
+
mode='lines',
|
|
865
|
+
name='CDF',
|
|
866
|
+
line=dict(color='#00FFC1', width=2)
|
|
867
|
+
)
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
# Add vertical line at spot price
|
|
871
|
+
fig.add_shape(
|
|
872
|
+
type='line',
|
|
873
|
+
x0=spot_price, y0=0,
|
|
874
|
+
x1=spot_price, y1=1,
|
|
875
|
+
line=dict(color='red', width=2, dash='dash')
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
# Add horizontal line at CDF=0.5 (median)
|
|
879
|
+
fig.add_shape(
|
|
880
|
+
type='line',
|
|
881
|
+
x0=prices[0], y0=0.5,
|
|
882
|
+
x1=prices[-1], y1=0.5,
|
|
883
|
+
line=dict(color='orange', width=2, dash='dash')
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
# Add annotation for spot price
|
|
887
|
+
fig.add_annotation(
|
|
888
|
+
x=spot_price,
|
|
889
|
+
y=1.05,
|
|
890
|
+
text=f"Spot: {spot_price}",
|
|
891
|
+
showarrow=False,
|
|
892
|
+
font=dict(color='red')
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
# Update layout
|
|
896
|
+
fig.update_layout(
|
|
897
|
+
title=title,
|
|
898
|
+
xaxis_title='Price',
|
|
899
|
+
yaxis_title='Cumulative Probability',
|
|
900
|
+
template='plotly_dark',
|
|
901
|
+
yaxis=dict(range=[0, 1.1]),
|
|
902
|
+
showlegend=False
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
return fig
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
@catch_exception
|
|
909
|
+
def plot_pdf(moneyness_array: np.ndarray,
|
|
910
|
+
rnd_values: np.ndarray,
|
|
911
|
+
spot_price: float = 1.0,
|
|
912
|
+
title: str = 'Probability Density Function') -> go.Figure:
|
|
913
|
+
"""
|
|
914
|
+
Plot the probability density function (PDF) from RND values.
|
|
915
|
+
|
|
916
|
+
Parameters:
|
|
917
|
+
- moneyness_array: Grid of log-moneyness values
|
|
918
|
+
- rnd_values: RND values
|
|
919
|
+
- spot_price: Spot price for converting to absolute prices
|
|
920
|
+
- title: Plot title
|
|
921
|
+
|
|
922
|
+
Returns:
|
|
923
|
+
- Plotly figure
|
|
924
|
+
"""
|
|
925
|
+
# This is essentially the same as plot_rnd but with a different title
|
|
926
|
+
return plot_rnd(moneyness_array, rnd_values, spot_price, title)
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
@catch_exception
|
|
930
|
+
def plot_interpolated_surface(
|
|
931
|
+
interp_results: Dict[str, Any],
|
|
932
|
+
title: str = 'Interpolated Implied Volatility Surface'
|
|
933
|
+
) -> go.Figure:
|
|
934
|
+
"""
|
|
935
|
+
Plot interpolated implied volatility surface.
|
|
936
|
+
|
|
937
|
+
Parameters:
|
|
938
|
+
- interp_results: Dictionary with interpolation results
|
|
939
|
+
- title: Plot title
|
|
940
|
+
|
|
941
|
+
Returns:
|
|
942
|
+
- Plotly figure
|
|
943
|
+
"""
|
|
944
|
+
# Extract data from interpolation results
|
|
945
|
+
moneyness_array = interp_results['moneyness_array']
|
|
946
|
+
target_expiries_years = interp_results['target_expiries_years']
|
|
947
|
+
iv_surface = interp_results['iv_surface']
|
|
948
|
+
|
|
949
|
+
# Create a 3D surface plot
|
|
950
|
+
fig = plot_3d_surface(
|
|
951
|
+
moneyness=moneyness_array,
|
|
952
|
+
expiries=target_expiries_years,
|
|
953
|
+
iv_surface=iv_surface,
|
|
954
|
+
title=title
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
return fig
|
|
958
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|