voly 0.0.70__tar.gz → 0.0.71__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: voly
3
- Version: 0.0.70
3
+ Version: 0.0.71
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.70"
7
+ version = "0.0.71"
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.70"
63
+ python_version = "0.0.71"
64
64
  warn_return_any = true
65
65
  warn_unused_configs = true
66
66
  disallow_untyped_defs = true
@@ -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(x_domain: np.ndarray,
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
- - x_domain: Grid of domain values
460
+ - moneyness_array: Grid of log-moneyness values
462
461
  - rnd_values: RND values
463
- - spot_price: Spot price for reference
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 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)
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 = x_domain[1] - x_domain[0]
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=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
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: voly
3
- Version: 0.0.70
3
+ Version: 0.0.71
4
4
  Summary: Options & volatility research package
5
5
  Author-email: Manu de Cara <manu.de.cara@gmail.com>
6
6
  License: MIT
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