voly 0.0.85__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/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} (DTM: {maturity_data["dtm"].iloc[0]:.1f})'
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} (DTM: {fit_results.loc[m, 'dtm']:.1f}" for m in maturity_names]
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} (DTE: {fit_results.loc[m, 'dtm']:.1f})" for m in maturity_names]
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} (DTE: {fit_results.loc[m, 'dtm']:.1f})" for m in maturity_names]
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 DTM
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, 'dtm'] for name in maturity_names]
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, 100)
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['ytm'] = df['dtm'] / 365.25
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