Spaces:
Sleeping
Sleeping
File size: 9,160 Bytes
1e4f9fd |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 |
"""
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"""
@staticmethod
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
@staticmethod
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
@staticmethod
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 |