Spaces:
Sleeping
Sleeping
| """ | |
| Chart generation for equity analysis visualization | |
| """ | |
| import plotly.graph_objects as go | |
| from plotly.subplots import make_subplots | |
| from typing import List, Optional | |
| from models import ScenarioResult, CapTable, EquityCalculator | |
| class EquityCharts: | |
| """Handles all chart generation for equity analysis""" | |
| def create_multi_scenario_comparison(results: List[ScenarioResult]) -> Optional[go.Figure]: | |
| """Create comparison chart showing option values and exit valuations""" | |
| if not results: | |
| return None | |
| scenario_names = [r.scenario_name for r in results] | |
| option_values = [r.option_value for r in results] | |
| exit_values = [r.exit_valuation for r in results] | |
| fig = make_subplots( | |
| rows=2, cols=1, | |
| subplot_titles=("Your Option Value by Scenario", "Exit Valuation by Scenario"), | |
| vertical_spacing=0.20, | |
| specs=[[{"secondary_y": False}], [{"secondary_y": False}]] | |
| ) | |
| # Option values bar chart | |
| fig.add_trace( | |
| go.Bar( | |
| x=scenario_names, | |
| y=option_values, | |
| name="Option Value", | |
| marker_color='#2E86AB', | |
| text=[f"${val:,.0f}" for val in option_values], | |
| textposition='outside' | |
| ), | |
| row=1, col=1 | |
| ) | |
| # Exit valuations bar chart | |
| fig.add_trace( | |
| go.Bar( | |
| x=scenario_names, | |
| y=exit_values, | |
| name="Exit Valuation", | |
| marker_color='#F18F01', | |
| text=[f"${val:,.0f}" for val in exit_values], | |
| textposition='outside', | |
| showlegend=False | |
| ), | |
| row=2, col=1 | |
| ) | |
| fig.update_layout( | |
| title="Multi-Scenario Equity Analysis", | |
| height=650, | |
| showlegend=True, | |
| margin=dict(t=80, b=50, l=80, r=50) | |
| ) | |
| # Add extra space for text labels above bars | |
| if option_values: | |
| fig.update_yaxes(title_text="Your Option Value ($)", row=1, col=1, range=[0, max(option_values) * 1.15]) | |
| if exit_values: | |
| fig.update_yaxes(title_text="Company Valuation ($)", row=2, col=1, range=[0, max(exit_values) * 1.15]) | |
| return fig | |
| def create_liquidation_waterfall( | |
| cap_table: CapTable, | |
| exit_valuation: float, | |
| scenario_name: str = "Best Scenario" | |
| ) -> go.Figure: | |
| """Create detailed liquidation waterfall chart for a specific exit value""" | |
| calculator = EquityCalculator(cap_table) | |
| remaining_proceeds = exit_valuation | |
| waterfall_data = [] | |
| participating_shareholders = [] | |
| # Sort funding rounds (newest first for liquidation preferences) | |
| sorted_rounds = sorted(cap_table.funding_rounds, | |
| key=lambda x: ['Seed', 'Series A', 'Series B', 'Series C'].index(x.name) | |
| if x.name in ['Seed', 'Series A', 'Series B', 'Series C'] else 999, | |
| reverse=True) | |
| # Phase 1: Liquidation preferences | |
| for round in sorted_rounds: | |
| if round.shares_issued > 0 and round.capital_raised > 0: | |
| preference_payout = min(remaining_proceeds, round.liquidation_preference) | |
| remaining_proceeds -= preference_payout | |
| if round.is_participating: | |
| participating_shareholders.append({ | |
| 'round': round.name, | |
| 'shares': round.shares_issued | |
| }) | |
| waterfall_data.append({ | |
| 'Round': f'{round.name} (Pref)', | |
| 'Payout': preference_payout, | |
| 'Type': 'Preference' | |
| }) | |
| # Phase 2: Participating preferred and common distribution | |
| participating_preferred_shares = sum(p['shares'] for p in participating_shareholders) | |
| total_participating_shares = cap_table.common_shares + participating_preferred_shares | |
| if total_participating_shares > 0: | |
| price_per_share = remaining_proceeds / total_participating_shares | |
| common_proceeds = price_per_share * cap_table.common_shares | |
| # Add participating preferred distributions | |
| for participant in participating_shareholders: | |
| participating_payout = price_per_share * participant['shares'] | |
| waterfall_data.append({ | |
| 'Round': f"{participant['round']} (Part.)", | |
| 'Payout': participating_payout, | |
| 'Type': 'Participation' | |
| }) | |
| else: | |
| common_proceeds = remaining_proceeds | |
| # Add common stock | |
| waterfall_data.append({ | |
| 'Round': 'Common Stock', | |
| 'Payout': common_proceeds, | |
| 'Type': 'Common' | |
| }) | |
| # Create the chart | |
| fig = go.Figure() | |
| color_map = { | |
| 'Preference': '#FF6B6B', # Red for liquidation preferences | |
| 'Participation': '#4ECDC4', # Teal for participating preferred | |
| 'Common': '#F7DC6F' # Yellow for common stock | |
| } | |
| for item in waterfall_data: | |
| if item['Payout'] > 0: | |
| color = color_map.get(item['Type'], '#96CEB4') | |
| fig.add_trace(go.Bar( | |
| x=[item['Round']], | |
| y=[item['Payout']], | |
| name=f"{item['Round']} (${item['Payout']:,.0f})", | |
| marker_color=color, | |
| text=f"${item['Payout']:,.0f}", | |
| textposition='outside' | |
| )) | |
| fig.update_layout( | |
| title=f"Liquidation Waterfall - ${exit_valuation:,.0f} Exit", | |
| xaxis_title="Stakeholder", | |
| yaxis_title="Payout ($)", | |
| height=450, | |
| showlegend=True, | |
| margin=dict(t=60, b=50, l=80, r=50) | |
| ) | |
| return fig | |
| def create_roi_analysis(results: List[ScenarioResult], investment_cost: float) -> Optional[go.Figure]: | |
| """Create ROI analysis chart""" | |
| if not results: | |
| return None | |
| roi_data = [] | |
| for result in results: | |
| roi = result.roi_percentage(investment_cost) | |
| # Cap very high ROI for display purposes | |
| display_roi = roi if roi < 999999 else 999999 | |
| roi_data.append({ | |
| 'scenario': result.scenario_name, | |
| 'roi': display_roi, | |
| 'absolute_gain': result.option_value - investment_cost | |
| }) | |
| fig = go.Figure() | |
| fig.add_trace(go.Bar( | |
| x=[d['scenario'] for d in roi_data], | |
| y=[d['roi'] for d in roi_data], | |
| name="ROI %", | |
| marker_color='#28A745', | |
| text=[f"{d['roi']:.0f}%" if d['roi'] < 999999 else "β%" for d in roi_data], | |
| textposition='outside' | |
| )) | |
| fig.update_layout( | |
| title="Return on Investment (ROI) by Scenario", | |
| xaxis_title="Scenario", | |
| yaxis_title="ROI (%)", | |
| height=450, | |
| margin=dict(t=60, b=50, l=60, r=50) | |
| ) | |
| return fig | |
| def format_results_table(results: List[ScenarioResult]) -> str: | |
| """Format scenario results as a markdown table""" | |
| if not results: | |
| return "No scenarios to display" | |
| table = "## π Exit Scenario Comparison\n\n" | |
| table += "| Scenario | Exit Value | Your Option Value | Value per Option | Common Proceeds |\n" | |
| table += "|----------|------------|-------------------|------------------|------------------|\n" | |
| for result in results: | |
| table += f"| **{result.scenario_name}** | ${result.exit_valuation:,.0f} | " | |
| table += f"${result.option_value:,.2f} | ${result.value_per_option:.4f} | " | |
| table += f"${result.common_proceeds:,.0f} |\n" | |
| return table | |
| def format_equity_summary(summary: dict, results: List[ScenarioResult]) -> str: | |
| """Format complete equity analysis summary""" | |
| results_table = format_results_table(results) | |
| summary_text = f""" | |
| ## π° Your Equity Summary | |
| **Your Option Grant:** {summary['your_options']:,} options | |
| **Strike Price:** ${summary['strike_price']:.4f} per share | |
| **Your Equity Stake:** {summary['your_equity_percentage']:.3f}% | |
| {results_table} | |
| ## ποΈ Cap Table Summary | |
| **Total Shares:** {summary['total_shares']:,} | |
| **Common Shares:** {summary['common_shares']:,} | |
| **Preferred Shares:** {summary['preferred_shares']:,} | |
| **Liquidation Terms:** {' | '.join(summary['participating_status']) if summary['participating_status'] else 'No preferred rounds'} | |
| **Break-even Price per Share:** ${summary['break_even_price']:.4f} | |
| *(Price needed for your options to have positive value)* | |
| """ | |
| return summary_text |