Spaces:
Sleeping
Sleeping
| """ | |
| 2D PDF visualization components. | |
| Create single and comparison plots for probability density functions. | |
| """ | |
| import numpy as np | |
| import plotly.graph_objects as go | |
| from typing import Dict, List, Optional, Tuple | |
| from src.visualization.themes import ( | |
| create_base_layout, | |
| get_line_style, | |
| format_hover_template, | |
| DARK_THEME | |
| ) | |
| def plot_pdf_2d( | |
| strikes: np.ndarray, | |
| pdf: np.ndarray, | |
| spot_price: float, | |
| title: str = "Option-Implied Probability Density", | |
| show_spot: bool = True, | |
| show_ci: bool = True, | |
| ci_levels: Tuple[float, float] = (0.16, 0.84) | |
| ) -> go.Figure: | |
| """ | |
| Create 2D plot of probability density function. | |
| Args: | |
| strikes: Strike prices | |
| pdf: PDF values | |
| spot_price: Current spot price | |
| title: Plot title | |
| show_spot: Whether to show vertical line at spot price | |
| show_ci: Whether to show confidence interval shading | |
| ci_levels: Confidence interval levels (default: 68% CI) | |
| Returns: | |
| Plotly figure | |
| """ | |
| fig = go.Figure() | |
| # Main PDF line | |
| fig.add_trace(go.Scatter( | |
| x=strikes, | |
| y=pdf, | |
| mode='lines', | |
| name='PDF', | |
| line=dict( | |
| color=DARK_THEME['primary'], | |
| width=3 | |
| ), | |
| fill='tozeroy', | |
| fillcolor=f"rgba(0, 217, 255, 0.2)", # Semi-transparent cyan | |
| hovertemplate=format_hover_template("Strike", "Probability Density") | |
| )) | |
| # Add spot price indicator | |
| if show_spot: | |
| fig.add_vline( | |
| x=spot_price, | |
| line_dash="dash", | |
| line_color=DARK_THEME['success'], | |
| line_width=2, | |
| annotation_text=f"Spot: ${spot_price:.2f}", | |
| annotation_position="top" | |
| ) | |
| # Add confidence interval shading | |
| if show_ci and ci_levels: | |
| try: | |
| from scipy.integrate import cumulative_trapezoid | |
| except ImportError: | |
| from scipy.integrate import cumtrapz as cumulative_trapezoid | |
| # Calculate CDF | |
| cdf = cumulative_trapezoid(pdf, strikes, initial=0) | |
| cdf = cdf / cdf[-1] # Normalize | |
| # Find strikes at CI levels | |
| lower_strike = np.interp(ci_levels[0], cdf, strikes) | |
| upper_strike = np.interp(ci_levels[1], cdf, strikes) | |
| # Add shaded region | |
| ci_mask = (strikes >= lower_strike) & (strikes <= upper_strike) | |
| fig.add_trace(go.Scatter( | |
| x=strikes[ci_mask], | |
| y=pdf[ci_mask], | |
| mode='lines', | |
| name=f'{int((ci_levels[1]-ci_levels[0])*100)}% CI', | |
| line=dict(width=0), | |
| fill='tozeroy', | |
| fillcolor='rgba(0, 255, 136, 0.15)', # Semi-transparent green | |
| showlegend=True, | |
| hoverinfo='skip' | |
| )) | |
| # Add vertical lines for CI bounds | |
| fig.add_vline( | |
| x=lower_strike, | |
| line_dash="dot", | |
| line_color=DARK_THEME['neutral'], | |
| line_width=1, | |
| annotation_text=f"${lower_strike:.2f}", | |
| annotation_position="bottom" | |
| ) | |
| fig.add_vline( | |
| x=upper_strike, | |
| line_dash="dot", | |
| line_color=DARK_THEME['neutral'], | |
| line_width=1, | |
| annotation_text=f"${upper_strike:.2f}", | |
| annotation_position="bottom" | |
| ) | |
| # Layout | |
| layout = create_base_layout( | |
| title=title, | |
| xaxis_title="Strike Price ($)", | |
| yaxis_title="Probability Density", | |
| showlegend=True, | |
| legend=dict( | |
| x=0.02, | |
| y=0.98, | |
| bgcolor='rgba(20,20,20,0.8)', | |
| bordercolor=DARK_THEME['grid'], | |
| borderwidth=1 | |
| ) | |
| ) | |
| fig.update_layout(**layout) | |
| return fig | |
| def plot_pdf_comparison( | |
| pdf_data: Dict[str, Dict[str, np.ndarray]], | |
| spot_price: float, | |
| title: str = "PDF Comparison Across Expirations" | |
| ) -> go.Figure: | |
| """ | |
| Create comparison plot of multiple PDFs. | |
| Args: | |
| pdf_data: Dictionary with format: | |
| { | |
| 'expiration_date': { | |
| 'strikes': np.ndarray, | |
| 'pdf': np.ndarray, | |
| 'days_to_expiry': int | |
| } | |
| } | |
| spot_price: Current spot price | |
| title: Plot title | |
| Returns: | |
| Plotly figure | |
| """ | |
| fig = go.Figure() | |
| # Sort by days to expiry | |
| sorted_data = sorted( | |
| pdf_data.items(), | |
| key=lambda x: x[1]['days_to_expiry'] | |
| ) | |
| # Add each PDF as a line | |
| for idx, (exp_date, data) in enumerate(sorted_data): | |
| strikes = data['strikes'] | |
| pdf = data['pdf'] | |
| days = data['days_to_expiry'] | |
| style = get_line_style(idx) | |
| fig.add_trace(go.Scatter( | |
| x=strikes, | |
| y=pdf, | |
| mode='lines', | |
| name=f"{days}D ({exp_date})", | |
| line=style, | |
| hovertemplate=format_hover_template( | |
| "Strike", | |
| "Probability", | |
| {'Expiration': exp_date, 'DTE': f'{days} days'} | |
| ) | |
| )) | |
| # Add spot price indicator | |
| fig.add_vline( | |
| x=spot_price, | |
| line_dash="dash", | |
| line_color=DARK_THEME['success'], | |
| line_width=2, | |
| annotation_text=f"Spot: ${spot_price:.2f}", | |
| annotation_position="top" | |
| ) | |
| # Layout | |
| layout = create_base_layout( | |
| title=title, | |
| xaxis_title="Strike Price ($)", | |
| yaxis_title="Probability Density", | |
| showlegend=True, | |
| legend=dict( | |
| x=0.02, | |
| y=0.98, | |
| bgcolor='rgba(20,20,20,0.8)', | |
| bordercolor=DARK_THEME['grid'], | |
| borderwidth=1 | |
| ) | |
| ) | |
| fig.update_layout(**layout) | |
| return fig | |
| def plot_cdf( | |
| strikes: np.ndarray, | |
| cdf: np.ndarray, | |
| spot_price: float, | |
| title: str = "Cumulative Distribution Function", | |
| show_percentiles: bool = True | |
| ) -> go.Figure: | |
| """ | |
| Plot cumulative distribution function. | |
| Args: | |
| strikes: Strike prices | |
| cdf: CDF values (0 to 1) | |
| spot_price: Current spot price | |
| title: Plot title | |
| show_percentiles: Whether to show key percentile lines | |
| Returns: | |
| Plotly figure | |
| """ | |
| fig = go.Figure() | |
| # Main CDF line | |
| fig.add_trace(go.Scatter( | |
| x=strikes, | |
| y=cdf * 100, # Convert to percentage | |
| mode='lines', | |
| name='CDF', | |
| line=dict( | |
| color=DARK_THEME['secondary'], | |
| width=3 | |
| ), | |
| hovertemplate=format_hover_template("Strike", "Cumulative Probability (%)") | |
| )) | |
| # Add spot price indicator | |
| fig.add_vline( | |
| x=spot_price, | |
| line_dash="dash", | |
| line_color=DARK_THEME['success'], | |
| line_width=2, | |
| annotation_text=f"Spot: ${spot_price:.2f}", | |
| annotation_position="top" | |
| ) | |
| # Add percentile lines | |
| if show_percentiles: | |
| percentiles = [25, 50, 75] | |
| for p in percentiles: | |
| strike_at_p = np.interp(p / 100, cdf, strikes) | |
| fig.add_hline( | |
| y=p, | |
| line_dash="dot", | |
| line_color=DARK_THEME['neutral'], | |
| line_width=1, | |
| annotation_text=f"P{p}", | |
| annotation_position="right" | |
| ) | |
| fig.add_vline( | |
| x=strike_at_p, | |
| line_dash="dot", | |
| line_color=DARK_THEME['neutral'], | |
| line_width=1, | |
| annotation_text=f"${strike_at_p:.0f}", | |
| annotation_position="top" | |
| ) | |
| # Layout | |
| layout = create_base_layout( | |
| title=title, | |
| xaxis_title="Strike Price ($)", | |
| yaxis_title="Cumulative Probability (%)", | |
| showlegend=False | |
| ) | |
| # Set y-axis range to 0-100% | |
| layout['yaxis']['range'] = [0, 100] | |
| fig.update_layout(**layout) | |
| return fig | |
| def plot_pdf_vs_normal( | |
| strikes: np.ndarray, | |
| pdf: np.ndarray, | |
| mean: float, | |
| std: float, | |
| spot_price: float, | |
| title: str = "PDF vs Normal Distribution" | |
| ) -> go.Figure: | |
| """ | |
| Compare PDF to normal distribution with same mean and std. | |
| Args: | |
| strikes: Strike prices | |
| pdf: Actual PDF values | |
| mean: PDF mean | |
| std: PDF standard deviation | |
| spot_price: Current spot price | |
| title: Plot title | |
| Returns: | |
| Plotly figure | |
| """ | |
| from scipy.stats import norm | |
| fig = go.Figure() | |
| # Actual PDF | |
| fig.add_trace(go.Scatter( | |
| x=strikes, | |
| y=pdf, | |
| mode='lines', | |
| name='Market PDF', | |
| line=dict( | |
| color=DARK_THEME['primary'], | |
| width=3 | |
| ), | |
| hovertemplate=format_hover_template("Strike", "Probability Density") | |
| )) | |
| # Normal distribution | |
| normal_pdf = norm.pdf(strikes, loc=mean, scale=std) | |
| fig.add_trace(go.Scatter( | |
| x=strikes, | |
| y=normal_pdf, | |
| mode='lines', | |
| name='Normal Distribution', | |
| line=dict( | |
| color=DARK_THEME['warning'], | |
| width=2, | |
| dash='dash' | |
| ), | |
| hovertemplate=format_hover_template("Strike", "Normal PDF") | |
| )) | |
| # Add spot price indicator | |
| fig.add_vline( | |
| x=spot_price, | |
| line_dash="dot", | |
| line_color=DARK_THEME['success'], | |
| line_width=2, | |
| annotation_text=f"Spot: ${spot_price:.2f}", | |
| annotation_position="top" | |
| ) | |
| # Layout | |
| layout = create_base_layout( | |
| title=title, | |
| xaxis_title="Strike Price ($)", | |
| yaxis_title="Probability Density", | |
| showlegend=True, | |
| legend=dict( | |
| x=0.02, | |
| y=0.98, | |
| bgcolor='rgba(20,20,20,0.8)', | |
| bordercolor=DARK_THEME['grid'], | |
| borderwidth=1 | |
| ) | |
| ) | |
| fig.update_layout(**layout) | |
| return fig | |
| if __name__ == "__main__": | |
| # Test 2D PDF plots | |
| print("Testing 2D PDF plots...") | |
| # Create synthetic PDF data | |
| spot = 450.0 | |
| strikes = np.linspace(400, 500, 200) | |
| mean = spot | |
| std = 15 | |
| # Lognormal-like PDF | |
| from scipy.stats import norm | |
| pdf = norm.pdf(strikes, loc=mean, scale=std) | |
| # Test single PDF plot | |
| fig1 = plot_pdf_2d(strikes, pdf, spot) | |
| fig1.write_html("test_pdf_2d.html") | |
| print("✅ 2D PDF plot saved to test_pdf_2d.html") | |
| # Test CDF plot | |
| try: | |
| from scipy.integrate import cumulative_trapezoid | |
| except ImportError: | |
| from scipy.integrate import cumtrapz as cumulative_trapezoid | |
| cdf = cumulative_trapezoid(pdf, strikes, initial=0) | |
| cdf = cdf / cdf[-1] | |
| fig2 = plot_cdf(strikes, cdf, spot) | |
| fig2.write_html("test_cdf.html") | |
| print("✅ CDF plot saved to test_cdf.html") | |
| # Test comparison plot | |
| pdf_data = { | |
| '2025-01-15': { | |
| 'strikes': strikes, | |
| 'pdf': norm.pdf(strikes, mean, std * 0.8), | |
| 'days_to_expiry': 15 | |
| }, | |
| '2025-02-01': { | |
| 'strikes': strikes, | |
| 'pdf': norm.pdf(strikes, mean, std), | |
| 'days_to_expiry': 30 | |
| }, | |
| '2025-03-01': { | |
| 'strikes': strikes, | |
| 'pdf': norm.pdf(strikes, mean, std * 1.2), | |
| 'days_to_expiry': 60 | |
| } | |
| } | |
| fig3 = plot_pdf_comparison(pdf_data, spot) | |
| fig3.write_html("test_pdf_comparison.html") | |
| print("✅ PDF comparison saved to test_pdf_comparison.html") | |
| # Test PDF vs Normal | |
| fig4 = plot_pdf_vs_normal(strikes, pdf, mean, std, spot) | |
| fig4.write_html("test_pdf_vs_normal.html") | |
| print("✅ PDF vs Normal saved to test_pdf_vs_normal.html") | |
| print("\n✅ All 2D PDF visualization tests passed!") | |