Spaces:
Sleeping
Sleeping
add: broken single code file into logical modules
Browse files- charts.py +248 -0
- equity_calculator.py +0 -563
- interface.py +235 -0
- main.py +95 -0
- models.py +228 -0
charts.py
ADDED
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Chart generation for equity analysis visualization
|
3 |
+
"""
|
4 |
+
import plotly.graph_objects as go
|
5 |
+
from plotly.subplots import make_subplots
|
6 |
+
from typing import List, Optional
|
7 |
+
from models import ScenarioResult, CapTable, EquityCalculator
|
8 |
+
|
9 |
+
|
10 |
+
class EquityCharts:
|
11 |
+
"""Handles all chart generation for equity analysis"""
|
12 |
+
|
13 |
+
@staticmethod
|
14 |
+
def create_multi_scenario_comparison(results: List[ScenarioResult]) -> Optional[go.Figure]:
|
15 |
+
"""Create comparison chart showing option values and exit valuations"""
|
16 |
+
if not results:
|
17 |
+
return None
|
18 |
+
|
19 |
+
scenario_names = [r.scenario_name for r in results]
|
20 |
+
option_values = [r.option_value for r in results]
|
21 |
+
exit_values = [r.exit_valuation for r in results]
|
22 |
+
|
23 |
+
fig = make_subplots(
|
24 |
+
rows=2, cols=1,
|
25 |
+
subplot_titles=("Your Option Value by Scenario", "Exit Valuation by Scenario"),
|
26 |
+
vertical_spacing=0.20,
|
27 |
+
specs=[[{"secondary_y": False}], [{"secondary_y": False}]]
|
28 |
+
)
|
29 |
+
|
30 |
+
# Option values bar chart
|
31 |
+
fig.add_trace(
|
32 |
+
go.Bar(
|
33 |
+
x=scenario_names,
|
34 |
+
y=option_values,
|
35 |
+
name="Option Value",
|
36 |
+
marker_color='#2E86AB',
|
37 |
+
text=[f"${val:,.0f}" for val in option_values],
|
38 |
+
textposition='outside'
|
39 |
+
),
|
40 |
+
row=1, col=1
|
41 |
+
)
|
42 |
+
|
43 |
+
# Exit valuations bar chart
|
44 |
+
fig.add_trace(
|
45 |
+
go.Bar(
|
46 |
+
x=scenario_names,
|
47 |
+
y=exit_values,
|
48 |
+
name="Exit Valuation",
|
49 |
+
marker_color='#F18F01',
|
50 |
+
text=[f"${val:,.0f}" for val in exit_values],
|
51 |
+
textposition='outside',
|
52 |
+
showlegend=False
|
53 |
+
),
|
54 |
+
row=2, col=1
|
55 |
+
)
|
56 |
+
|
57 |
+
fig.update_layout(
|
58 |
+
title="Multi-Scenario Equity Analysis",
|
59 |
+
height=650,
|
60 |
+
showlegend=True,
|
61 |
+
margin=dict(t=80, b=50, l=80, r=50)
|
62 |
+
)
|
63 |
+
|
64 |
+
# Add extra space for text labels above bars
|
65 |
+
if option_values:
|
66 |
+
fig.update_yaxes(title_text="Your Option Value ($)", row=1, col=1, range=[0, max(option_values) * 1.15])
|
67 |
+
if exit_values:
|
68 |
+
fig.update_yaxes(title_text="Company Valuation ($)", row=2, col=1, range=[0, max(exit_values) * 1.15])
|
69 |
+
|
70 |
+
return fig
|
71 |
+
|
72 |
+
@staticmethod
|
73 |
+
def create_liquidation_waterfall(
|
74 |
+
cap_table: CapTable,
|
75 |
+
exit_valuation: float,
|
76 |
+
scenario_name: str = "Best Scenario"
|
77 |
+
) -> go.Figure:
|
78 |
+
"""Create detailed liquidation waterfall chart for a specific exit value"""
|
79 |
+
|
80 |
+
calculator = EquityCalculator(cap_table)
|
81 |
+
remaining_proceeds = exit_valuation
|
82 |
+
waterfall_data = []
|
83 |
+
participating_shareholders = []
|
84 |
+
|
85 |
+
# Sort funding rounds (newest first for liquidation preferences)
|
86 |
+
sorted_rounds = sorted(cap_table.funding_rounds,
|
87 |
+
key=lambda x: ['Seed', 'Series A', 'Series B', 'Series C'].index(x.name)
|
88 |
+
if x.name in ['Seed', 'Series A', 'Series B', 'Series C'] else 999,
|
89 |
+
reverse=True)
|
90 |
+
|
91 |
+
# Phase 1: Liquidation preferences
|
92 |
+
for round in sorted_rounds:
|
93 |
+
if round.shares_issued > 0 and round.capital_raised > 0:
|
94 |
+
preference_payout = min(remaining_proceeds, round.liquidation_preference)
|
95 |
+
remaining_proceeds -= preference_payout
|
96 |
+
|
97 |
+
if round.is_participating:
|
98 |
+
participating_shareholders.append({
|
99 |
+
'round': round.name,
|
100 |
+
'shares': round.shares_issued
|
101 |
+
})
|
102 |
+
|
103 |
+
waterfall_data.append({
|
104 |
+
'Round': f'{round.name} (Pref)',
|
105 |
+
'Payout': preference_payout,
|
106 |
+
'Type': 'Preference'
|
107 |
+
})
|
108 |
+
|
109 |
+
# Phase 2: Participating preferred and common distribution
|
110 |
+
participating_preferred_shares = sum(p['shares'] for p in participating_shareholders)
|
111 |
+
total_participating_shares = cap_table.common_shares + participating_preferred_shares
|
112 |
+
|
113 |
+
if total_participating_shares > 0:
|
114 |
+
price_per_share = remaining_proceeds / total_participating_shares
|
115 |
+
common_proceeds = price_per_share * cap_table.common_shares
|
116 |
+
|
117 |
+
# Add participating preferred distributions
|
118 |
+
for participant in participating_shareholders:
|
119 |
+
participating_payout = price_per_share * participant['shares']
|
120 |
+
waterfall_data.append({
|
121 |
+
'Round': f"{participant['round']} (Part.)",
|
122 |
+
'Payout': participating_payout,
|
123 |
+
'Type': 'Participation'
|
124 |
+
})
|
125 |
+
else:
|
126 |
+
common_proceeds = remaining_proceeds
|
127 |
+
|
128 |
+
# Add common stock
|
129 |
+
waterfall_data.append({
|
130 |
+
'Round': 'Common Stock',
|
131 |
+
'Payout': common_proceeds,
|
132 |
+
'Type': 'Common'
|
133 |
+
})
|
134 |
+
|
135 |
+
# Create the chart
|
136 |
+
fig = go.Figure()
|
137 |
+
|
138 |
+
color_map = {
|
139 |
+
'Preference': '#FF6B6B', # Red for liquidation preferences
|
140 |
+
'Participation': '#4ECDC4', # Teal for participating preferred
|
141 |
+
'Common': '#F7DC6F' # Yellow for common stock
|
142 |
+
}
|
143 |
+
|
144 |
+
for item in waterfall_data:
|
145 |
+
if item['Payout'] > 0:
|
146 |
+
color = color_map.get(item['Type'], '#96CEB4')
|
147 |
+
fig.add_trace(go.Bar(
|
148 |
+
x=[item['Round']],
|
149 |
+
y=[item['Payout']],
|
150 |
+
name=f"{item['Round']} (${item['Payout']:,.0f})",
|
151 |
+
marker_color=color,
|
152 |
+
text=f"${item['Payout']:,.0f}",
|
153 |
+
textposition='outside'
|
154 |
+
))
|
155 |
+
|
156 |
+
fig.update_layout(
|
157 |
+
title=f"Liquidation Waterfall - ${exit_valuation:,.0f} Exit",
|
158 |
+
xaxis_title="Stakeholder",
|
159 |
+
yaxis_title="Payout ($)",
|
160 |
+
height=450,
|
161 |
+
showlegend=True,
|
162 |
+
margin=dict(t=60, b=50, l=80, r=50)
|
163 |
+
)
|
164 |
+
|
165 |
+
return fig
|
166 |
+
|
167 |
+
@staticmethod
|
168 |
+
def create_roi_analysis(results: List[ScenarioResult], investment_cost: float) -> Optional[go.Figure]:
|
169 |
+
"""Create ROI analysis chart"""
|
170 |
+
if not results:
|
171 |
+
return None
|
172 |
+
|
173 |
+
roi_data = []
|
174 |
+
for result in results:
|
175 |
+
roi = result.roi_percentage(investment_cost)
|
176 |
+
# Cap very high ROI for display purposes
|
177 |
+
display_roi = roi if roi < 999999 else 999999
|
178 |
+
roi_data.append({
|
179 |
+
'scenario': result.scenario_name,
|
180 |
+
'roi': display_roi,
|
181 |
+
'absolute_gain': result.option_value - investment_cost
|
182 |
+
})
|
183 |
+
|
184 |
+
fig = go.Figure()
|
185 |
+
fig.add_trace(go.Bar(
|
186 |
+
x=[d['scenario'] for d in roi_data],
|
187 |
+
y=[d['roi'] for d in roi_data],
|
188 |
+
name="ROI %",
|
189 |
+
marker_color='#28A745',
|
190 |
+
text=[f"{d['roi']:.0f}%" if d['roi'] < 999999 else "∞%" for d in roi_data],
|
191 |
+
textposition='outside'
|
192 |
+
))
|
193 |
+
|
194 |
+
fig.update_layout(
|
195 |
+
title="Return on Investment (ROI) by Scenario",
|
196 |
+
xaxis_title="Scenario",
|
197 |
+
yaxis_title="ROI (%)",
|
198 |
+
height=450,
|
199 |
+
margin=dict(t=60, b=50, l=60, r=50)
|
200 |
+
)
|
201 |
+
|
202 |
+
return fig
|
203 |
+
|
204 |
+
|
205 |
+
def format_results_table(results: List[ScenarioResult]) -> str:
|
206 |
+
"""Format scenario results as a markdown table"""
|
207 |
+
if not results:
|
208 |
+
return "No scenarios to display"
|
209 |
+
|
210 |
+
table = "## 📊 Exit Scenario Comparison\n\n"
|
211 |
+
table += "| Scenario | Exit Value | Your Option Value | Value per Option | Common Proceeds |\n"
|
212 |
+
table += "|----------|------------|-------------------|------------------|------------------|\n"
|
213 |
+
|
214 |
+
for result in results:
|
215 |
+
table += f"| **{result.scenario_name}** | ${result.exit_valuation:,.0f} | "
|
216 |
+
table += f"${result.option_value:,.2f} | ${result.value_per_option:.4f} | "
|
217 |
+
table += f"${result.common_proceeds:,.0f} |\n"
|
218 |
+
|
219 |
+
return table
|
220 |
+
|
221 |
+
|
222 |
+
def format_equity_summary(summary: dict, results: List[ScenarioResult]) -> str:
|
223 |
+
"""Format complete equity analysis summary"""
|
224 |
+
|
225 |
+
results_table = format_results_table(results)
|
226 |
+
|
227 |
+
summary_text = f"""
|
228 |
+
## 💰 Your Equity Summary
|
229 |
+
|
230 |
+
**Your Option Grant:** {summary['your_options']:,} options
|
231 |
+
**Strike Price:** ${summary['strike_price']:.4f} per share
|
232 |
+
**Your Equity Stake:** {summary['your_equity_percentage']:.3f}%
|
233 |
+
|
234 |
+
{results_table}
|
235 |
+
|
236 |
+
## 🏗️ Cap Table Summary
|
237 |
+
|
238 |
+
**Total Shares:** {summary['total_shares']:,}
|
239 |
+
**Common Shares:** {summary['common_shares']:,}
|
240 |
+
**Preferred Shares:** {summary['preferred_shares']:,}
|
241 |
+
|
242 |
+
**Liquidation Terms:** {' | '.join(summary['participating_status']) if summary['participating_status'] else 'No preferred rounds'}
|
243 |
+
|
244 |
+
**Break-even Price per Share:** ${summary['break_even_price']:.4f}
|
245 |
+
*(Price needed for your options to have positive value)*
|
246 |
+
"""
|
247 |
+
|
248 |
+
return summary_text
|
equity_calculator.py
DELETED
@@ -1,563 +0,0 @@
|
|
1 |
-
import gradio as gr
|
2 |
-
import pandas as pd
|
3 |
-
import plotly.graph_objects as go
|
4 |
-
import plotly.express as px
|
5 |
-
from plotly.subplots import make_subplots
|
6 |
-
|
7 |
-
def calculate_single_scenario(
|
8 |
-
exit_valuation, total_shares, your_options, strike_price,
|
9 |
-
seed_shares, seed_capital, seed_multiple, seed_participating,
|
10 |
-
series_a_shares, series_a_capital, series_a_multiple, series_a_participating,
|
11 |
-
series_b_shares, series_b_capital, series_b_multiple, series_b_participating
|
12 |
-
):
|
13 |
-
"""Calculate equity value for a single exit scenario"""
|
14 |
-
|
15 |
-
# Calculate common shares
|
16 |
-
total_preferred_shares = seed_shares + series_a_shares + series_b_shares
|
17 |
-
common_shares = total_shares - total_preferred_shares
|
18 |
-
|
19 |
-
if common_shares <= 0:
|
20 |
-
return {
|
21 |
-
'exit_valuation': exit_valuation,
|
22 |
-
'option_value': 0,
|
23 |
-
'price_per_share': 0,
|
24 |
-
'common_proceeds': 0,
|
25 |
-
'error': 'Preferred shares exceed total shares'
|
26 |
-
}
|
27 |
-
|
28 |
-
# Phase 1: Pay liquidation preferences
|
29 |
-
remaining_proceeds = exit_valuation
|
30 |
-
participating_shareholders = []
|
31 |
-
|
32 |
-
# Series B (most recent)
|
33 |
-
series_b_preference_payout = 0
|
34 |
-
if series_b_shares > 0 and series_b_capital > 0:
|
35 |
-
series_b_preference = series_b_capital * series_b_multiple
|
36 |
-
series_b_preference_payout = min(remaining_proceeds, series_b_preference)
|
37 |
-
remaining_proceeds -= series_b_preference_payout
|
38 |
-
if series_b_participating:
|
39 |
-
participating_shareholders.append({'shares': series_b_shares})
|
40 |
-
|
41 |
-
# Series A
|
42 |
-
series_a_preference_payout = 0
|
43 |
-
if series_a_shares > 0 and series_a_capital > 0:
|
44 |
-
series_a_preference = series_a_capital * series_a_multiple
|
45 |
-
series_a_preference_payout = min(remaining_proceeds, series_a_preference)
|
46 |
-
remaining_proceeds -= series_a_preference_payout
|
47 |
-
if series_a_participating:
|
48 |
-
participating_shareholders.append({'shares': series_a_shares})
|
49 |
-
|
50 |
-
# Seed
|
51 |
-
seed_preference_payout = 0
|
52 |
-
if seed_shares > 0 and seed_capital > 0:
|
53 |
-
seed_preference = seed_capital * seed_multiple
|
54 |
-
seed_preference_payout = min(remaining_proceeds, seed_preference)
|
55 |
-
remaining_proceeds -= seed_preference_payout
|
56 |
-
if seed_participating:
|
57 |
-
participating_shareholders.append({'shares': seed_shares})
|
58 |
-
|
59 |
-
# Phase 2: Handle non-participating conversions
|
60 |
-
participating_preferred_shares = sum([p['shares'] for p in participating_shareholders])
|
61 |
-
total_participating_shares = common_shares + participating_preferred_shares
|
62 |
-
|
63 |
-
# Check conversions for non-participating preferred
|
64 |
-
if series_b_shares > 0 and series_b_capital > 0 and not series_b_participating:
|
65 |
-
conversion_value = (series_b_shares / total_shares) * exit_valuation
|
66 |
-
if conversion_value > series_b_preference_payout:
|
67 |
-
remaining_proceeds += series_b_preference_payout
|
68 |
-
total_participating_shares += series_b_shares
|
69 |
-
|
70 |
-
if series_a_shares > 0 and series_a_capital > 0 and not series_a_participating:
|
71 |
-
conversion_value = (series_a_shares / total_shares) * exit_valuation
|
72 |
-
if conversion_value > series_a_preference_payout:
|
73 |
-
remaining_proceeds += series_a_preference_payout
|
74 |
-
total_participating_shares += series_a_shares
|
75 |
-
|
76 |
-
if seed_shares > 0 and seed_capital > 0 and not seed_participating:
|
77 |
-
conversion_value = (seed_shares / total_shares) * exit_valuation
|
78 |
-
if conversion_value > seed_preference_payout:
|
79 |
-
remaining_proceeds += seed_preference_payout
|
80 |
-
total_participating_shares += seed_shares
|
81 |
-
|
82 |
-
# Final distribution
|
83 |
-
if total_participating_shares > 0:
|
84 |
-
price_per_participating_share = remaining_proceeds / total_participating_shares
|
85 |
-
common_proceeds = price_per_participating_share * common_shares
|
86 |
-
else:
|
87 |
-
common_proceeds = remaining_proceeds
|
88 |
-
|
89 |
-
price_per_common_share = common_proceeds / common_shares if common_shares > 0 else 0
|
90 |
-
option_value_per_share = max(0, price_per_common_share - strike_price)
|
91 |
-
total_option_value = option_value_per_share * your_options
|
92 |
-
|
93 |
-
return {
|
94 |
-
'exit_valuation': exit_valuation,
|
95 |
-
'option_value': total_option_value,
|
96 |
-
'price_per_share': price_per_common_share,
|
97 |
-
'common_proceeds': common_proceeds,
|
98 |
-
'error': None
|
99 |
-
}
|
100 |
-
|
101 |
-
def calculate_equity_value(
|
102 |
-
# Cap table inputs
|
103 |
-
total_shares, your_options, strike_price,
|
104 |
-
# Seed round
|
105 |
-
seed_shares, seed_capital, seed_multiple, seed_participating,
|
106 |
-
# Series A
|
107 |
-
series_a_shares, series_a_capital, series_a_multiple, series_a_participating,
|
108 |
-
# Series B
|
109 |
-
series_b_shares, series_b_capital, series_b_multiple, series_b_participating,
|
110 |
-
# Multiple exit scenarios
|
111 |
-
exit_scenario_1, scenario_1_name,
|
112 |
-
exit_scenario_2, scenario_2_name,
|
113 |
-
exit_scenario_3, scenario_3_name,
|
114 |
-
exit_scenario_4, scenario_4_name,
|
115 |
-
exit_scenario_5, scenario_5_name
|
116 |
-
):
|
117 |
-
"""Calculate startup equity value with liquidation preferences for multiple exit scenarios"""
|
118 |
-
|
119 |
-
# Handle None values and provide defaults
|
120 |
-
total_shares = total_shares or 10000000
|
121 |
-
your_options = your_options or 0
|
122 |
-
strike_price = strike_price or 0
|
123 |
-
seed_shares = seed_shares or 0
|
124 |
-
seed_capital = seed_capital or 0
|
125 |
-
seed_multiple = seed_multiple or 1.0
|
126 |
-
seed_participating = seed_participating or False
|
127 |
-
series_a_shares = series_a_shares or 0
|
128 |
-
series_a_capital = series_a_capital or 0
|
129 |
-
series_a_multiple = series_a_multiple or 1.0
|
130 |
-
series_a_participating = series_a_participating or False
|
131 |
-
series_b_shares = series_b_shares or 0
|
132 |
-
series_b_capital = series_b_capital or 0
|
133 |
-
series_b_multiple = series_b_multiple or 1.0
|
134 |
-
series_b_participating = series_b_participating or False
|
135 |
-
|
136 |
-
# Handle exit scenarios
|
137 |
-
exit_scenario_1 = exit_scenario_1 or 0
|
138 |
-
exit_scenario_2 = exit_scenario_2 or 0
|
139 |
-
exit_scenario_3 = exit_scenario_3 or 0
|
140 |
-
exit_scenario_4 = exit_scenario_4 or 0
|
141 |
-
exit_scenario_5 = exit_scenario_5 or 0
|
142 |
-
scenario_1_name = scenario_1_name or "Scenario 1"
|
143 |
-
scenario_2_name = scenario_2_name or "Scenario 2"
|
144 |
-
scenario_3_name = scenario_3_name or "Scenario 3"
|
145 |
-
scenario_4_name = scenario_4_name or "Scenario 4"
|
146 |
-
scenario_5_name = scenario_5_name or "Scenario 5"
|
147 |
-
|
148 |
-
# Input validation
|
149 |
-
if total_shares <= 0:
|
150 |
-
return "Invalid inputs - please check your values", None, None, None
|
151 |
-
|
152 |
-
# Calculate scenarios
|
153 |
-
scenarios = []
|
154 |
-
exit_values = [exit_scenario_1, exit_scenario_2, exit_scenario_3, exit_scenario_4, exit_scenario_5]
|
155 |
-
scenario_names = [scenario_1_name, scenario_2_name, scenario_3_name, scenario_4_name, scenario_5_name]
|
156 |
-
|
157 |
-
for exit_val, name in zip(exit_values, scenario_names):
|
158 |
-
if exit_val > 0: # Only calculate scenarios with positive exit values
|
159 |
-
scenario_result = calculate_single_scenario(
|
160 |
-
exit_val, total_shares, your_options, strike_price,
|
161 |
-
seed_shares, seed_capital, seed_multiple, seed_participating,
|
162 |
-
series_a_shares, series_a_capital, series_a_multiple, series_a_participating,
|
163 |
-
series_b_shares, series_b_capital, series_b_multiple, series_b_participating
|
164 |
-
)
|
165 |
-
scenario_result['name'] = name
|
166 |
-
scenarios.append(scenario_result)
|
167 |
-
|
168 |
-
if not scenarios:
|
169 |
-
return "Please enter at least one exit scenario with a positive value", None, None, None
|
170 |
-
|
171 |
-
# Calculate common shares and basic info
|
172 |
-
total_preferred_shares = seed_shares + series_a_shares + series_b_shares
|
173 |
-
common_shares = total_shares - total_preferred_shares
|
174 |
-
your_equity_percentage = (your_options / total_shares) * 100 if total_shares > 0 else 0
|
175 |
-
|
176 |
-
# Build results summary
|
177 |
-
participating_status = []
|
178 |
-
if seed_shares > 0: participating_status.append(f"Seed: {'Participating' if seed_participating else 'Non-Participating'}")
|
179 |
-
if series_a_shares > 0: participating_status.append(f"Series A: {'Participating' if series_a_participating else 'Non-Participating'}")
|
180 |
-
if series_b_shares > 0: participating_status.append(f"Series B: {'Participating' if series_b_participating else 'Non-Participating'}")
|
181 |
-
|
182 |
-
# Create scenario comparison table
|
183 |
-
scenario_table = "## 📊 Exit Scenario Comparison\n\n"
|
184 |
-
scenario_table += "| Scenario | Exit Value | Your Option Value | Value per Option | Common Proceeds |\n"
|
185 |
-
scenario_table += "|----------|------------|-------------------|------------------|------------------|\n"
|
186 |
-
|
187 |
-
for scenario in scenarios:
|
188 |
-
scenario_table += f"| **{scenario['name']}** | ${scenario['exit_valuation']:,.0f} | "
|
189 |
-
scenario_table += f"${scenario['option_value']:,.2f} | ${scenario['option_value']/your_options if your_options > 0 else 0:.4f} | "
|
190 |
-
scenario_table += f"${scenario['common_proceeds']:,.0f} |\n"
|
191 |
-
|
192 |
-
results = f"""
|
193 |
-
## 💰 Your Equity Summary
|
194 |
-
|
195 |
-
**Your Option Grant:** {your_options:,} options
|
196 |
-
**Strike Price:** ${strike_price:.4f} per share
|
197 |
-
**Your Equity Stake:** {your_equity_percentage:.3f}%
|
198 |
-
|
199 |
-
{scenario_table}
|
200 |
-
|
201 |
-
## 🏗️ Cap Table Summary
|
202 |
-
|
203 |
-
**Total Shares:** {total_shares:,}
|
204 |
-
**Common Shares:** {common_shares:,}
|
205 |
-
**Preferred Shares:** {total_preferred_shares:,}
|
206 |
-
|
207 |
-
**Liquidation Terms:** {' | '.join(participating_status) if participating_status else 'No preferred rounds'}
|
208 |
-
|
209 |
-
**Break-even Price per Share:** ${strike_price:.4f}
|
210 |
-
*(Price needed for your options to have positive value)*
|
211 |
-
"""
|
212 |
-
|
213 |
-
# Create comparison bar chart
|
214 |
-
if scenarios:
|
215 |
-
scenario_names = [s['name'] for s in scenarios]
|
216 |
-
option_values = [s['option_value'] for s in scenarios]
|
217 |
-
exit_values = [s['exit_valuation'] for s in scenarios]
|
218 |
-
|
219 |
-
fig = make_subplots(
|
220 |
-
rows=2, cols=1,
|
221 |
-
subplot_titles=("Your Option Value by Scenario", "Exit Valuation by Scenario"),
|
222 |
-
vertical_spacing=0.20,
|
223 |
-
specs=[[{"secondary_y": False}], [{"secondary_y": False}]]
|
224 |
-
)
|
225 |
-
|
226 |
-
# Option values bar chart
|
227 |
-
fig.add_trace(
|
228 |
-
go.Bar(
|
229 |
-
x=scenario_names,
|
230 |
-
y=option_values,
|
231 |
-
name="Option Value",
|
232 |
-
marker_color='#2E86AB',
|
233 |
-
text=[f"${val:,.0f}" for val in option_values],
|
234 |
-
textposition='outside'
|
235 |
-
),
|
236 |
-
row=1, col=1
|
237 |
-
)
|
238 |
-
|
239 |
-
# Exit valuations bar chart
|
240 |
-
fig.add_trace(
|
241 |
-
go.Bar(
|
242 |
-
x=scenario_names,
|
243 |
-
y=exit_values,
|
244 |
-
name="Exit Valuation",
|
245 |
-
marker_color='#F18F01',
|
246 |
-
text=[f"${val:,.0f}" for val in exit_values],
|
247 |
-
textposition='outside',
|
248 |
-
showlegend=False
|
249 |
-
),
|
250 |
-
row=2, col=1
|
251 |
-
)
|
252 |
-
|
253 |
-
fig.update_layout(
|
254 |
-
title="Multi-Scenario Equity Analysis",
|
255 |
-
height=650,
|
256 |
-
showlegend=True,
|
257 |
-
margin=dict(t=80, b=50, l=80, r=50)
|
258 |
-
)
|
259 |
-
|
260 |
-
# Add extra space for text labels above bars
|
261 |
-
fig.update_yaxes(title_text="Your Option Value ($)", row=1, col=1, range=[0, max(option_values) * 1.15])
|
262 |
-
fig.update_yaxes(title_text="Company Valuation ($)", row=2, col=1, range=[0, max(exit_values) * 1.15])
|
263 |
-
|
264 |
-
comparison_chart = fig
|
265 |
-
else:
|
266 |
-
comparison_chart = None
|
267 |
-
|
268 |
-
# Create detailed breakdown chart for the highest scenario
|
269 |
-
if scenarios:
|
270 |
-
# Find the scenario with highest option value for detailed analysis
|
271 |
-
best_scenario = max(scenarios, key=lambda x: x['option_value'])
|
272 |
-
|
273 |
-
# Create a detailed waterfall for this scenario
|
274 |
-
detailed_fig = calculate_single_scenario_waterfall(
|
275 |
-
best_scenario['exit_valuation'], total_shares, your_options, strike_price,
|
276 |
-
seed_shares, seed_capital, seed_multiple, seed_participating,
|
277 |
-
series_a_shares, series_a_capital, series_a_multiple, series_a_participating,
|
278 |
-
series_b_shares, series_b_capital, series_b_multiple, series_b_participating
|
279 |
-
)
|
280 |
-
|
281 |
-
detailed_chart = detailed_fig
|
282 |
-
else:
|
283 |
-
detailed_chart = None
|
284 |
-
|
285 |
-
# Create ROI comparison
|
286 |
-
if scenarios and your_options > 0:
|
287 |
-
roi_data = []
|
288 |
-
investment_cost = your_options * strike_price
|
289 |
-
|
290 |
-
for scenario in scenarios:
|
291 |
-
if investment_cost > 0:
|
292 |
-
roi = ((scenario['option_value'] - investment_cost) / investment_cost) * 100
|
293 |
-
else:
|
294 |
-
roi = float('inf') if scenario['option_value'] > 0 else 0
|
295 |
-
|
296 |
-
roi_data.append({
|
297 |
-
'scenario': scenario['name'],
|
298 |
-
'roi': roi if roi != float('inf') else 999999, # Cap at very high number for display
|
299 |
-
'absolute_gain': scenario['option_value'] - investment_cost
|
300 |
-
})
|
301 |
-
|
302 |
-
roi_fig = go.Figure()
|
303 |
-
roi_fig.add_trace(go.Bar(
|
304 |
-
x=[d['scenario'] for d in roi_data],
|
305 |
-
y=[d['roi'] for d in roi_data],
|
306 |
-
name="ROI %",
|
307 |
-
marker_color='#28A745',
|
308 |
-
text=[f"{d['roi']:.0f}%" if d['roi'] < 999999 else "∞%" for d in roi_data],
|
309 |
-
textposition='outside'
|
310 |
-
))
|
311 |
-
|
312 |
-
roi_fig.update_layout(
|
313 |
-
title="Return on Investment (ROI) by Scenario",
|
314 |
-
xaxis_title="Scenario",
|
315 |
-
yaxis_title="ROI (%)",
|
316 |
-
height=450,
|
317 |
-
margin=dict(t=60, b=50, l=60, r=50)
|
318 |
-
)
|
319 |
-
|
320 |
-
roi_chart = roi_fig
|
321 |
-
else:
|
322 |
-
roi_chart = None
|
323 |
-
|
324 |
-
return results, comparison_chart, detailed_chart, roi_chart
|
325 |
-
|
326 |
-
def calculate_single_scenario_waterfall(
|
327 |
-
exit_valuation, total_shares, your_options, strike_price,
|
328 |
-
seed_shares, seed_capital, seed_multiple, seed_participating,
|
329 |
-
series_a_shares, series_a_capital, series_a_multiple, series_a_participating,
|
330 |
-
series_b_shares, series_b_capital, series_b_multiple, series_b_participating
|
331 |
-
):
|
332 |
-
"""Create detailed waterfall chart for a single scenario"""
|
333 |
-
|
334 |
-
common_shares = total_shares - (seed_shares + series_a_shares + series_b_shares)
|
335 |
-
remaining_proceeds = exit_valuation
|
336 |
-
waterfall_data = []
|
337 |
-
participating_shareholders = []
|
338 |
-
|
339 |
-
# Phase 1: Liquidation preferences
|
340 |
-
if series_b_shares > 0 and series_b_capital > 0:
|
341 |
-
series_b_preference = series_b_capital * series_b_multiple
|
342 |
-
series_b_payout = min(remaining_proceeds, series_b_preference)
|
343 |
-
remaining_proceeds -= series_b_payout
|
344 |
-
|
345 |
-
if series_b_participating:
|
346 |
-
participating_shareholders.append({'round': 'Series B', 'shares': series_b_shares})
|
347 |
-
|
348 |
-
waterfall_data.append({
|
349 |
-
'Round': 'Series B (Pref)',
|
350 |
-
'Payout': series_b_payout,
|
351 |
-
'Type': 'Preference'
|
352 |
-
})
|
353 |
-
|
354 |
-
if series_a_shares > 0 and series_a_capital > 0:
|
355 |
-
series_a_preference = series_a_capital * series_a_multiple
|
356 |
-
series_a_payout = min(remaining_proceeds, series_a_preference)
|
357 |
-
remaining_proceeds -= series_a_payout
|
358 |
-
|
359 |
-
if series_a_participating:
|
360 |
-
participating_shareholders.append({'round': 'Series A', 'shares': series_a_shares})
|
361 |
-
|
362 |
-
waterfall_data.append({
|
363 |
-
'Round': 'Series A (Pref)',
|
364 |
-
'Payout': series_a_payout,
|
365 |
-
'Type': 'Preference'
|
366 |
-
})
|
367 |
-
|
368 |
-
if seed_shares > 0 and seed_capital > 0:
|
369 |
-
seed_preference = seed_capital * seed_multiple
|
370 |
-
seed_payout = min(remaining_proceeds, seed_preference)
|
371 |
-
remaining_proceeds -= seed_payout
|
372 |
-
|
373 |
-
if seed_participating:
|
374 |
-
participating_shareholders.append({'round': 'Seed', 'shares': seed_shares})
|
375 |
-
|
376 |
-
waterfall_data.append({
|
377 |
-
'Round': 'Seed (Pref)',
|
378 |
-
'Payout': seed_payout,
|
379 |
-
'Type': 'Preference'
|
380 |
-
})
|
381 |
-
|
382 |
-
# Phase 2: Check conversions and final distribution
|
383 |
-
participating_preferred_shares = sum([p['shares'] for p in participating_shareholders])
|
384 |
-
total_participating_shares = common_shares + participating_preferred_shares
|
385 |
-
|
386 |
-
# Simplified conversion logic for visualization
|
387 |
-
if total_participating_shares > 0:
|
388 |
-
price_per_share = remaining_proceeds / total_participating_shares
|
389 |
-
common_proceeds = price_per_share * common_shares
|
390 |
-
|
391 |
-
# Add participating preferred distributions
|
392 |
-
for participant in participating_shareholders:
|
393 |
-
participating_payout = price_per_share * participant['shares']
|
394 |
-
waterfall_data.append({
|
395 |
-
'Round': f"{participant['round']} (Part.)",
|
396 |
-
'Payout': participating_payout,
|
397 |
-
'Type': 'Participation'
|
398 |
-
})
|
399 |
-
else:
|
400 |
-
common_proceeds = remaining_proceeds
|
401 |
-
|
402 |
-
# Add common stock
|
403 |
-
waterfall_data.append({
|
404 |
-
'Round': 'Common Stock',
|
405 |
-
'Payout': common_proceeds,
|
406 |
-
'Type': 'Common'
|
407 |
-
})
|
408 |
-
|
409 |
-
# Create the chart
|
410 |
-
fig = go.Figure()
|
411 |
-
|
412 |
-
color_map = {
|
413 |
-
'Preference': '#FF6B6B',
|
414 |
-
'Participation': '#4ECDC4',
|
415 |
-
'Common': '#F7DC6F'
|
416 |
-
}
|
417 |
-
|
418 |
-
for item in waterfall_data:
|
419 |
-
if item['Payout'] > 0:
|
420 |
-
color = color_map.get(item['Type'], '#96CEB4')
|
421 |
-
fig.add_trace(go.Bar(
|
422 |
-
x=[item['Round']],
|
423 |
-
y=[item['Payout']],
|
424 |
-
name=f"{item['Round']} (${item['Payout']:,.0f})",
|
425 |
-
marker_color=color,
|
426 |
-
text=f"${item['Payout']:,.0f}",
|
427 |
-
textposition='outside'
|
428 |
-
))
|
429 |
-
|
430 |
-
fig.update_layout(
|
431 |
-
title=f"Liquidation Waterfall - ${exit_valuation:,.0f} Exit",
|
432 |
-
xaxis_title="Stakeholder",
|
433 |
-
yaxis_title="Payout ($)",
|
434 |
-
height=450,
|
435 |
-
showlegend=True,
|
436 |
-
margin=dict(t=60, b=50, l=80, r=50)
|
437 |
-
)
|
438 |
-
|
439 |
-
return fig
|
440 |
-
|
441 |
-
# Create Gradio interface
|
442 |
-
with gr.Blocks(title="Startup Equity Calculator", theme=gr.themes.Soft()) as app:
|
443 |
-
gr.Markdown("# 🚀 Startup Equity Calculator")
|
444 |
-
gr.Markdown("Calculate the value of your stock options based on cap table structure and liquidation preferences")
|
445 |
-
|
446 |
-
with gr.Row():
|
447 |
-
with gr.Column():
|
448 |
-
gr.Markdown("## Cap Table Structure")
|
449 |
-
total_shares = gr.Number(label="Total Fully Diluted Shares", value=10000000, precision=0)
|
450 |
-
your_options = gr.Number(label="Your Option Grant", value=0, precision=0)
|
451 |
-
strike_price = gr.Number(label="Strike Price per Share ($)", value=0.10, precision=4)
|
452 |
-
|
453 |
-
gr.Markdown("## Funding Rounds")
|
454 |
-
|
455 |
-
with gr.Accordion("Seed Round", open=False):
|
456 |
-
seed_shares = gr.Number(label="Seed Shares Issued", value=0, precision=0)
|
457 |
-
seed_capital = gr.Number(label="Seed Capital Raised ($)", value=0, precision=0)
|
458 |
-
seed_multiple = gr.Number(label="Liquidation Multiple", value=1.0, precision=1)
|
459 |
-
seed_participating = gr.Checkbox(label="Participating Preferred", value=False)
|
460 |
-
|
461 |
-
with gr.Accordion("Series A", open=False):
|
462 |
-
series_a_shares = gr.Number(label="Series A Shares Issued", value=0, precision=0)
|
463 |
-
series_a_capital = gr.Number(label="Series A Capital Raised ($)", value=0, precision=0)
|
464 |
-
series_a_multiple = gr.Number(label="Liquidation Multiple", value=1.0, precision=1)
|
465 |
-
series_a_participating = gr.Checkbox(label="Participating Preferred", value=False)
|
466 |
-
|
467 |
-
with gr.Accordion("Series B", open=False):
|
468 |
-
series_b_shares = gr.Number(label="Series B Shares Issued", value=0, precision=0)
|
469 |
-
series_b_capital = gr.Number(label="Series B Capital Raised ($)", value=0, precision=0)
|
470 |
-
series_b_multiple = gr.Number(label="Liquidation Multiple", value=1.0, precision=1)
|
471 |
-
series_b_participating = gr.Checkbox(label="Participating Preferred", value=False)
|
472 |
-
|
473 |
-
with gr.Column():
|
474 |
-
gr.Markdown("## Exit Scenarios")
|
475 |
-
gr.Markdown("*Define multiple exit scenarios to compare side-by-side*")
|
476 |
-
|
477 |
-
with gr.Row():
|
478 |
-
with gr.Column():
|
479 |
-
scenario_1_name = gr.Textbox(label="Scenario 1 Name", value="Conservative", placeholder="e.g., Conservative")
|
480 |
-
exit_scenario_1 = gr.Number(label="Exit Valuation ($)", value=25000000, precision=0)
|
481 |
-
|
482 |
-
with gr.Column():
|
483 |
-
scenario_2_name = gr.Textbox(label="Scenario 2 Name", value="Base Case", placeholder="e.g., Base Case")
|
484 |
-
exit_scenario_2 = gr.Number(label="Exit Valuation ($)", value=50000000, precision=0)
|
485 |
-
|
486 |
-
with gr.Row():
|
487 |
-
with gr.Column():
|
488 |
-
scenario_3_name = gr.Textbox(label="Scenario 3 Name", value="Optimistic", placeholder="e.g., Optimistic")
|
489 |
-
exit_scenario_3 = gr.Number(label="Exit Valuation ($)", value=100000000, precision=0)
|
490 |
-
|
491 |
-
with gr.Column():
|
492 |
-
scenario_4_name = gr.Textbox(label="Scenario 4 Name", value="", placeholder="e.g., Moon Shot")
|
493 |
-
exit_scenario_4 = gr.Number(label="Exit Valuation ($)", value=0, precision=0)
|
494 |
-
|
495 |
-
with gr.Row():
|
496 |
-
with gr.Column():
|
497 |
-
scenario_5_name = gr.Textbox(label="Scenario 5 Name", value="", placeholder="e.g., IPO")
|
498 |
-
exit_scenario_5 = gr.Number(label="Exit Valuation ($)", value=0, precision=0)
|
499 |
-
|
500 |
-
calculate_btn = gr.Button("🚀 Calculate All Scenarios", variant="primary", size="lg")
|
501 |
-
|
502 |
-
results_text = gr.Markdown()
|
503 |
-
|
504 |
-
with gr.Row():
|
505 |
-
comparison_plot = gr.Plot(label="Multi-Scenario Comparison")
|
506 |
-
|
507 |
-
with gr.Row():
|
508 |
-
waterfall_plot = gr.Plot(label="Detailed Waterfall (Best Scenario)")
|
509 |
-
roi_plot = gr.Plot(label="Return on Investment")
|
510 |
-
|
511 |
-
# Set up the calculation trigger
|
512 |
-
inputs = [
|
513 |
-
total_shares, your_options, strike_price,
|
514 |
-
seed_shares, seed_capital, seed_multiple, seed_participating,
|
515 |
-
series_a_shares, series_a_capital, series_a_multiple, series_a_participating,
|
516 |
-
series_b_shares, series_b_capital, series_b_multiple, series_b_participating,
|
517 |
-
exit_scenario_1, scenario_1_name,
|
518 |
-
exit_scenario_2, scenario_2_name,
|
519 |
-
exit_scenario_3, scenario_3_name,
|
520 |
-
exit_scenario_4, scenario_4_name,
|
521 |
-
exit_scenario_5, scenario_5_name
|
522 |
-
]
|
523 |
-
|
524 |
-
outputs = [results_text, comparison_plot, waterfall_plot, roi_plot]
|
525 |
-
|
526 |
-
calculate_btn.click(calculate_equity_value, inputs=inputs, outputs=outputs)
|
527 |
-
|
528 |
-
# Auto-calculate on input changes
|
529 |
-
for input_component in inputs:
|
530 |
-
input_component.change(calculate_equity_value, inputs=inputs, outputs=outputs)
|
531 |
-
|
532 |
-
gr.Markdown("""
|
533 |
-
## 📚 How to Use This Calculator
|
534 |
-
|
535 |
-
### 🎯 Multi-Scenario Analysis
|
536 |
-
**This is where the real value lies!** Instead of guessing one exit value, define multiple realistic scenarios:
|
537 |
-
- **Conservative**: What if growth is slower than expected?
|
538 |
-
- **Base Case**: Most likely scenario based on current trajectory
|
539 |
-
- **Optimistic**: If everything goes right
|
540 |
-
- **Moon Shot**: Best case scenario (10x+ returns)
|
541 |
-
|
542 |
-
### 📊 Key Outputs
|
543 |
-
1. **Comparison Table**: Side-by-side option values across all scenarios
|
544 |
-
2. **Visual Charts**: See how your returns scale with different exits
|
545 |
-
3. **ROI Analysis**: Understand your return on investment potential
|
546 |
-
4. **Detailed Waterfall**: How liquidation preferences affect distributions
|
547 |
-
|
548 |
-
### 💡 Decision Framework
|
549 |
-
Use this to evaluate:
|
550 |
-
- **Risk vs Reward**: How much upside vs downside?
|
551 |
-
- **Opportunity Cost**: Compare to other job offers or investments
|
552 |
-
- **Negotiation Power**: Understanding your equity's potential value range
|
553 |
-
|
554 |
-
### 🔧 Liquidation Preferences
|
555 |
-
- **Non-Participating**: Investors choose preference OR convert to common (better for employees)
|
556 |
-
- **Participating**: Investors get preference AND share upside (worse for employees)
|
557 |
-
- **Multiples**: How many times their investment investors get back first
|
558 |
-
|
559 |
-
**Pro Tip**: Try toggling participating preferred on/off to see the dramatic impact on your equity value!
|
560 |
-
""")
|
561 |
-
|
562 |
-
if __name__ == "__main__":
|
563 |
-
app.launch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
interface.py
ADDED
@@ -0,0 +1,235 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Gradio interface components for the equity calculator
|
3 |
+
"""
|
4 |
+
import gradio as gr
|
5 |
+
from typing import Tuple, List, Optional
|
6 |
+
from models import create_cap_table, EquityCalculator, ExitScenario
|
7 |
+
from charts import EquityCharts, format_equity_summary
|
8 |
+
|
9 |
+
|
10 |
+
def create_cap_table_inputs():
|
11 |
+
"""Create the cap table input components"""
|
12 |
+
with gr.Column():
|
13 |
+
gr.Markdown("## Cap Table Structure")
|
14 |
+
total_shares = gr.Number(label="Total Fully Diluted Shares", value=10000000, precision=0)
|
15 |
+
your_options = gr.Number(label="Your Option Grant", value=0, precision=0)
|
16 |
+
strike_price = gr.Number(label="Strike Price per Share ($)", value=0.10, precision=4)
|
17 |
+
|
18 |
+
gr.Markdown("## Funding Rounds")
|
19 |
+
|
20 |
+
with gr.Accordion("Seed Round", open=False):
|
21 |
+
seed_shares = gr.Number(label="Seed Shares Issued", value=0, precision=0)
|
22 |
+
seed_capital = gr.Number(label="Seed Capital Raised ($)", value=0, precision=0)
|
23 |
+
seed_multiple = gr.Number(label="Liquidation Multiple", value=1.0, precision=1)
|
24 |
+
seed_participating = gr.Checkbox(label="Participating Preferred", value=False)
|
25 |
+
|
26 |
+
with gr.Accordion("Series A", open=False):
|
27 |
+
series_a_shares = gr.Number(label="Series A Shares Issued", value=0, precision=0)
|
28 |
+
series_a_capital = gr.Number(label="Series A Capital Raised ($)", value=0, precision=0)
|
29 |
+
series_a_multiple = gr.Number(label="Liquidation Multiple", value=1.0, precision=1)
|
30 |
+
series_a_participating = gr.Checkbox(label="Participating Preferred", value=False)
|
31 |
+
|
32 |
+
with gr.Accordion("Series B", open=False):
|
33 |
+
series_b_shares = gr.Number(label="Series B Shares Issued", value=0, precision=0)
|
34 |
+
series_b_capital = gr.Number(label="Series B Capital Raised ($)", value=0, precision=0)
|
35 |
+
series_b_multiple = gr.Number(label="Liquidation Multiple", value=1.0, precision=1)
|
36 |
+
series_b_participating = gr.Checkbox(label="Participating Preferred", value=False)
|
37 |
+
|
38 |
+
return [
|
39 |
+
total_shares, your_options, strike_price,
|
40 |
+
seed_shares, seed_capital, seed_multiple, seed_participating,
|
41 |
+
series_a_shares, series_a_capital, series_a_multiple, series_a_participating,
|
42 |
+
series_b_shares, series_b_capital, series_b_multiple, series_b_participating
|
43 |
+
]
|
44 |
+
|
45 |
+
|
46 |
+
def create_scenario_inputs():
|
47 |
+
"""Create the exit scenario input components"""
|
48 |
+
with gr.Column():
|
49 |
+
gr.Markdown("## Exit Scenarios")
|
50 |
+
gr.Markdown("*Define multiple exit scenarios to compare side-by-side*")
|
51 |
+
|
52 |
+
with gr.Row():
|
53 |
+
with gr.Column():
|
54 |
+
scenario_1_name = gr.Textbox(label="Scenario 1 Name", value="Conservative", placeholder="e.g., Conservative")
|
55 |
+
exit_scenario_1 = gr.Number(label="Exit Valuation ($)", value=25000000, precision=0)
|
56 |
+
|
57 |
+
with gr.Column():
|
58 |
+
scenario_2_name = gr.Textbox(label="Scenario 2 Name", value="Base Case", placeholder="e.g., Base Case")
|
59 |
+
exit_scenario_2 = gr.Number(label="Exit Valuation ($)", value=50000000, precision=0)
|
60 |
+
|
61 |
+
with gr.Row():
|
62 |
+
with gr.Column():
|
63 |
+
scenario_3_name = gr.Textbox(label="Scenario 3 Name", value="Optimistic", placeholder="e.g., Optimistic")
|
64 |
+
exit_scenario_3 = gr.Number(label="Exit Valuation ($)", value=100000000, precision=0)
|
65 |
+
|
66 |
+
with gr.Column():
|
67 |
+
scenario_4_name = gr.Textbox(label="Scenario 4 Name", value="", placeholder="e.g., Moon Shot")
|
68 |
+
exit_scenario_4 = gr.Number(label="Exit Valuation ($)", value=0, precision=0)
|
69 |
+
|
70 |
+
with gr.Row():
|
71 |
+
with gr.Column():
|
72 |
+
scenario_5_name = gr.Textbox(label="Scenario 5 Name", value="", placeholder="e.g., IPO")
|
73 |
+
exit_scenario_5 = gr.Number(label="Exit Valuation ($)", value=0, precision=0)
|
74 |
+
|
75 |
+
calculate_btn = gr.Button("🚀 Calculate All Scenarios", variant="primary", size="lg")
|
76 |
+
results_text = gr.Markdown()
|
77 |
+
|
78 |
+
return [
|
79 |
+
exit_scenario_1, scenario_1_name,
|
80 |
+
exit_scenario_2, scenario_2_name,
|
81 |
+
exit_scenario_3, scenario_3_name,
|
82 |
+
exit_scenario_4, scenario_4_name,
|
83 |
+
exit_scenario_5, scenario_5_name,
|
84 |
+
calculate_btn, results_text
|
85 |
+
]
|
86 |
+
|
87 |
+
|
88 |
+
def create_output_components():
|
89 |
+
"""Create the output chart components"""
|
90 |
+
with gr.Row():
|
91 |
+
comparison_plot = gr.Plot(label="Multi-Scenario Comparison")
|
92 |
+
|
93 |
+
with gr.Row():
|
94 |
+
waterfall_plot = gr.Plot(label="Detailed Waterfall (Best Scenario)")
|
95 |
+
roi_plot = gr.Plot(label="Return on Investment")
|
96 |
+
|
97 |
+
return [comparison_plot, waterfall_plot, roi_plot]
|
98 |
+
|
99 |
+
|
100 |
+
def process_inputs(
|
101 |
+
# Cap table inputs
|
102 |
+
total_shares, your_options, strike_price,
|
103 |
+
seed_shares, seed_capital, seed_multiple, seed_participating,
|
104 |
+
series_a_shares, series_a_capital, series_a_multiple, series_a_participating,
|
105 |
+
series_b_shares, series_b_capital, series_b_multiple, series_b_participating,
|
106 |
+
# Scenario inputs
|
107 |
+
exit_scenario_1, scenario_1_name,
|
108 |
+
exit_scenario_2, scenario_2_name,
|
109 |
+
exit_scenario_3, scenario_3_name,
|
110 |
+
exit_scenario_4, scenario_4_name,
|
111 |
+
exit_scenario_5, scenario_5_name
|
112 |
+
) -> Tuple[str, Optional[gr.Plot], Optional[gr.Plot], Optional[gr.Plot]]:
|
113 |
+
"""Process all inputs and return formatted results and charts"""
|
114 |
+
|
115 |
+
# Handle None values with defaults
|
116 |
+
total_shares = total_shares or 10000000
|
117 |
+
your_options = your_options or 0
|
118 |
+
strike_price = strike_price or 0.10
|
119 |
+
|
120 |
+
# Validate inputs
|
121 |
+
if total_shares <= 0:
|
122 |
+
return "Invalid inputs - please check your values", None, None, None
|
123 |
+
|
124 |
+
# Create cap table
|
125 |
+
try:
|
126 |
+
cap_table = create_cap_table(
|
127 |
+
total_shares=total_shares,
|
128 |
+
your_options=your_options,
|
129 |
+
strike_price=strike_price,
|
130 |
+
seed_shares=seed_shares or 0,
|
131 |
+
seed_capital=seed_capital or 0,
|
132 |
+
seed_multiple=seed_multiple or 1.0,
|
133 |
+
seed_participating=seed_participating or False,
|
134 |
+
series_a_shares=series_a_shares or 0,
|
135 |
+
series_a_capital=series_a_capital or 0,
|
136 |
+
series_a_multiple=series_a_multiple or 1.0,
|
137 |
+
series_a_participating=series_a_participating or False,
|
138 |
+
series_b_shares=series_b_shares or 0,
|
139 |
+
series_b_capital=series_b_capital or 0,
|
140 |
+
series_b_multiple=series_b_multiple or 1.0,
|
141 |
+
series_b_participating=series_b_participating or False
|
142 |
+
)
|
143 |
+
except Exception as e:
|
144 |
+
return f"Error creating cap table: {str(e)}", None, None, None
|
145 |
+
|
146 |
+
# Create scenarios
|
147 |
+
scenarios = []
|
148 |
+
scenario_data = [
|
149 |
+
(exit_scenario_1 or 0, scenario_1_name or "Scenario 1"),
|
150 |
+
(exit_scenario_2 or 0, scenario_2_name or "Scenario 2"),
|
151 |
+
(exit_scenario_3 or 0, scenario_3_name or "Scenario 3"),
|
152 |
+
(exit_scenario_4 or 0, scenario_4_name or "Scenario 4"),
|
153 |
+
(exit_scenario_5 or 0, scenario_5_name or "Scenario 5")
|
154 |
+
]
|
155 |
+
|
156 |
+
for exit_val, name in scenario_data:
|
157 |
+
if exit_val > 0:
|
158 |
+
scenarios.append(ExitScenario(name=name, exit_valuation=exit_val))
|
159 |
+
|
160 |
+
if not scenarios:
|
161 |
+
return "Please enter at least one exit scenario with a positive value", None, None, None
|
162 |
+
|
163 |
+
# Calculate results
|
164 |
+
calculator = EquityCalculator(cap_table)
|
165 |
+
try:
|
166 |
+
results = calculator.calculate_multiple_scenarios(scenarios)
|
167 |
+
summary = calculator.get_liquidation_summary()
|
168 |
+
except Exception as e:
|
169 |
+
return f"Error calculating results: {str(e)}", None, None, None
|
170 |
+
|
171 |
+
if not results:
|
172 |
+
return "No valid scenarios to calculate", None, None, None
|
173 |
+
|
174 |
+
# Generate charts
|
175 |
+
charts = EquityCharts()
|
176 |
+
|
177 |
+
try:
|
178 |
+
# Multi-scenario comparison
|
179 |
+
comparison_chart = charts.create_multi_scenario_comparison(results)
|
180 |
+
|
181 |
+
# Detailed waterfall for best scenario
|
182 |
+
best_result = max(results, key=lambda x: x.option_value)
|
183 |
+
waterfall_chart = charts.create_liquidation_waterfall(
|
184 |
+
cap_table,
|
185 |
+
best_result.exit_valuation,
|
186 |
+
best_result.scenario_name
|
187 |
+
)
|
188 |
+
|
189 |
+
# ROI analysis
|
190 |
+
investment_cost = cap_table.your_options * cap_table.strike_price
|
191 |
+
roi_chart = charts.create_roi_analysis(results, investment_cost)
|
192 |
+
|
193 |
+
except Exception as e:
|
194 |
+
return f"Error generating charts: {str(e)}", None, None, None
|
195 |
+
|
196 |
+
# Format summary
|
197 |
+
try:
|
198 |
+
summary_text = format_equity_summary(summary, results)
|
199 |
+
except Exception as e:
|
200 |
+
return f"Error formatting summary: {str(e)}", comparison_chart, waterfall_chart, roi_chart
|
201 |
+
|
202 |
+
return summary_text, comparison_chart, waterfall_chart, roi_chart
|
203 |
+
|
204 |
+
|
205 |
+
def create_help_section():
|
206 |
+
"""Create the help/documentation section"""
|
207 |
+
gr.Markdown("""
|
208 |
+
## 📚 How to Use This Calculator
|
209 |
+
|
210 |
+
### 🎯 Multi-Scenario Analysis
|
211 |
+
**This is where the real value lies!** Instead of guessing one exit value, define multiple realistic scenarios:
|
212 |
+
- **Conservative**: What if growth is slower than expected?
|
213 |
+
- **Base Case**: Most likely scenario based on current trajectory
|
214 |
+
- **Optimistic**: If everything goes right
|
215 |
+
- **Moon Shot**: Best case scenario (10x+ returns)
|
216 |
+
|
217 |
+
### 📊 Key Outputs
|
218 |
+
1. **Comparison Table**: Side-by-side option values across all scenarios
|
219 |
+
2. **Visual Charts**: See how your returns scale with different exits
|
220 |
+
3. **ROI Analysis**: Understand your return on investment potential
|
221 |
+
4. **Detailed Waterfall**: How liquidation preferences affect distributions
|
222 |
+
|
223 |
+
### 💡 Decision Framework
|
224 |
+
Use this to evaluate:
|
225 |
+
- **Risk vs Reward**: How much upside vs downside?
|
226 |
+
- **Opportunity Cost**: Compare to other job offers or investments
|
227 |
+
- **Negotiation Power**: Understanding your equity's potential value range
|
228 |
+
|
229 |
+
### 🔧 Liquidation Preferences
|
230 |
+
- **Non-Participating**: Investors choose preference OR convert to common (better for employees)
|
231 |
+
- **Participating**: Investors get preference AND share upside (worse for employees)
|
232 |
+
- **Multiples**: How many times their investment investors get back first
|
233 |
+
|
234 |
+
**Pro Tip**: Try toggling participating preferred on/off to see the dramatic impact on your equity value!
|
235 |
+
""")
|
main.py
ADDED
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Main application file - Startup Equity Calculator
|
3 |
+
Entry point for the Gradio web application
|
4 |
+
"""
|
5 |
+
import gradio as gr
|
6 |
+
from interface import (
|
7 |
+
create_cap_table_inputs,
|
8 |
+
create_scenario_inputs,
|
9 |
+
create_output_components,
|
10 |
+
process_inputs,
|
11 |
+
create_help_section
|
12 |
+
)
|
13 |
+
|
14 |
+
|
15 |
+
def create_app():
|
16 |
+
"""Create and configure the Gradio application"""
|
17 |
+
|
18 |
+
with gr.Blocks(title="Startup Equity Calculator", theme=gr.themes.Soft()) as app:
|
19 |
+
|
20 |
+
# Header
|
21 |
+
gr.Markdown("# 🚀 Startup Equity Calculator")
|
22 |
+
gr.Markdown("Calculate the value of your stock options based on cap table structure and liquidation preferences")
|
23 |
+
|
24 |
+
# Main interface
|
25 |
+
with gr.Row():
|
26 |
+
# Left column: Cap table inputs
|
27 |
+
cap_table_components = create_cap_table_inputs()
|
28 |
+
|
29 |
+
# Right column: Scenario inputs and results
|
30 |
+
scenario_components = create_scenario_inputs()
|
31 |
+
|
32 |
+
# Output charts
|
33 |
+
output_components = create_output_components()
|
34 |
+
|
35 |
+
# Extract components for event handling
|
36 |
+
calculate_btn = scenario_components[-2] # Second to last component
|
37 |
+
results_text = scenario_components[-1] # Last component
|
38 |
+
|
39 |
+
# All input components
|
40 |
+
all_inputs = cap_table_components + scenario_components[:-2] # Exclude button and results
|
41 |
+
|
42 |
+
# All output components
|
43 |
+
all_outputs = [results_text] + output_components
|
44 |
+
|
45 |
+
# Set up event handlers
|
46 |
+
calculate_btn.click(
|
47 |
+
process_inputs,
|
48 |
+
inputs=all_inputs,
|
49 |
+
outputs=all_outputs
|
50 |
+
)
|
51 |
+
|
52 |
+
# Auto-calculate on input changes (optional - can be enabled/disabled)
|
53 |
+
for input_component in cap_table_components:
|
54 |
+
input_component.change(
|
55 |
+
process_inputs,
|
56 |
+
inputs=all_inputs,
|
57 |
+
outputs=all_outputs
|
58 |
+
)
|
59 |
+
|
60 |
+
# Help section
|
61 |
+
create_help_section()
|
62 |
+
|
63 |
+
return app
|
64 |
+
|
65 |
+
|
66 |
+
def main():
|
67 |
+
"""Main function to launch the application"""
|
68 |
+
print("🚀 Starting Startup Equity Calculator...")
|
69 |
+
print("📊 Loading models and interface...")
|
70 |
+
|
71 |
+
try:
|
72 |
+
app = create_app()
|
73 |
+
print("✅ Application created successfully!")
|
74 |
+
print("🌐 Launching web interface...")
|
75 |
+
|
76 |
+
# Launch with custom settings
|
77 |
+
app.launch(
|
78 |
+
server_name="0.0.0.0", # Allow external access
|
79 |
+
server_port=7860, # Default Gradio port
|
80 |
+
share=False, # Set to True to create public link
|
81 |
+
debug=False, # Set to True for development
|
82 |
+
show_error=True # Show detailed errors
|
83 |
+
)
|
84 |
+
|
85 |
+
except ImportError as e:
|
86 |
+
print(f"❌ Import Error: {e}")
|
87 |
+
print("Make sure all required modules (models.py, charts.py, interface.py) are in the same directory")
|
88 |
+
|
89 |
+
except Exception as e:
|
90 |
+
print(f"❌ Error launching application: {e}")
|
91 |
+
print("Check that all dependencies are installed: gradio, plotly, pandas")
|
92 |
+
|
93 |
+
|
94 |
+
if __name__ == "__main__":
|
95 |
+
main()
|
models.py
ADDED
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Data models and core equity calculation logic
|
3 |
+
"""
|
4 |
+
from dataclasses import dataclass
|
5 |
+
from typing import List, Optional, Dict, Any
|
6 |
+
|
7 |
+
|
8 |
+
@dataclass
|
9 |
+
class FundingRound:
|
10 |
+
"""Represents a funding round (Seed, Series A, etc.)"""
|
11 |
+
name: str
|
12 |
+
shares_issued: int
|
13 |
+
capital_raised: float
|
14 |
+
liquidation_multiple: float
|
15 |
+
is_participating: bool
|
16 |
+
|
17 |
+
@property
|
18 |
+
def liquidation_preference(self) -> float:
|
19 |
+
"""Total liquidation preference for this round"""
|
20 |
+
return self.capital_raised * self.liquidation_multiple
|
21 |
+
|
22 |
+
|
23 |
+
@dataclass
|
24 |
+
class CapTable:
|
25 |
+
"""Represents the company's capitalization table"""
|
26 |
+
total_shares: int
|
27 |
+
your_options: int
|
28 |
+
strike_price: float
|
29 |
+
funding_rounds: List[FundingRound]
|
30 |
+
|
31 |
+
@property
|
32 |
+
def total_preferred_shares(self) -> int:
|
33 |
+
"""Total preferred shares across all rounds"""
|
34 |
+
return sum(round.shares_issued for round in self.funding_rounds)
|
35 |
+
|
36 |
+
@property
|
37 |
+
def common_shares(self) -> int:
|
38 |
+
"""Total common shares available"""
|
39 |
+
return self.total_shares - self.total_preferred_shares
|
40 |
+
|
41 |
+
@property
|
42 |
+
def your_equity_percentage(self) -> float:
|
43 |
+
"""Your equity percentage of total company"""
|
44 |
+
return (self.your_options / self.total_shares) * 100 if self.total_shares > 0 else 0
|
45 |
+
|
46 |
+
|
47 |
+
@dataclass
|
48 |
+
class ExitScenario:
|
49 |
+
"""Represents an exit scenario with name and valuation"""
|
50 |
+
name: str
|
51 |
+
exit_valuation: float
|
52 |
+
|
53 |
+
|
54 |
+
@dataclass
|
55 |
+
class ScenarioResult:
|
56 |
+
"""Result of calculating equity value for one exit scenario"""
|
57 |
+
scenario_name: str
|
58 |
+
exit_valuation: float
|
59 |
+
option_value: float
|
60 |
+
price_per_share: float
|
61 |
+
common_proceeds: float
|
62 |
+
error: Optional[str] = None
|
63 |
+
|
64 |
+
@property
|
65 |
+
def value_per_option(self) -> float:
|
66 |
+
"""Value per individual option"""
|
67 |
+
return self.price_per_share
|
68 |
+
|
69 |
+
def roi_percentage(self, investment_cost: float) -> float:
|
70 |
+
"""Calculate ROI percentage"""
|
71 |
+
if investment_cost <= 0:
|
72 |
+
return float('inf') if self.option_value > 0 else 0
|
73 |
+
return ((self.option_value - investment_cost) / investment_cost) * 100
|
74 |
+
|
75 |
+
|
76 |
+
class EquityCalculator:
|
77 |
+
"""Core equity calculation engine"""
|
78 |
+
|
79 |
+
def __init__(self, cap_table: CapTable):
|
80 |
+
self.cap_table = cap_table
|
81 |
+
|
82 |
+
def calculate_scenario(self, exit_scenario: ExitScenario) -> ScenarioResult:
|
83 |
+
"""Calculate equity value for a single exit scenario"""
|
84 |
+
|
85 |
+
if self.cap_table.common_shares <= 0:
|
86 |
+
return ScenarioResult(
|
87 |
+
scenario_name=exit_scenario.name,
|
88 |
+
exit_valuation=exit_scenario.exit_valuation,
|
89 |
+
option_value=0,
|
90 |
+
price_per_share=0,
|
91 |
+
common_proceeds=0,
|
92 |
+
error='Preferred shares exceed total shares'
|
93 |
+
)
|
94 |
+
|
95 |
+
# Phase 1: Pay liquidation preferences (newest rounds first)
|
96 |
+
remaining_proceeds = exit_scenario.exit_valuation
|
97 |
+
participating_shareholders = []
|
98 |
+
|
99 |
+
# Sort funding rounds by reverse order (newest first)
|
100 |
+
sorted_rounds = sorted(self.cap_table.funding_rounds,
|
101 |
+
key=lambda x: ['Seed', 'Series A', 'Series B', 'Series C'].index(x.name)
|
102 |
+
if x.name in ['Seed', 'Series A', 'Series B', 'Series C'] else 999,
|
103 |
+
reverse=True)
|
104 |
+
|
105 |
+
preference_payouts = {}
|
106 |
+
|
107 |
+
for round in sorted_rounds:
|
108 |
+
if round.shares_issued > 0 and round.capital_raised > 0:
|
109 |
+
preference_payout = min(remaining_proceeds, round.liquidation_preference)
|
110 |
+
remaining_proceeds -= preference_payout
|
111 |
+
preference_payouts[round.name] = preference_payout
|
112 |
+
|
113 |
+
if round.is_participating:
|
114 |
+
participating_shareholders.append({
|
115 |
+
'round': round.name,
|
116 |
+
'shares': round.shares_issued
|
117 |
+
})
|
118 |
+
|
119 |
+
# Phase 2: Handle non-participating conversions
|
120 |
+
participating_preferred_shares = sum(p['shares'] for p in participating_shareholders)
|
121 |
+
total_participating_shares = self.cap_table.common_shares + participating_preferred_shares
|
122 |
+
|
123 |
+
# Check if non-participating preferred should convert
|
124 |
+
for round in sorted_rounds:
|
125 |
+
if (round.shares_issued > 0 and round.capital_raised > 0
|
126 |
+
and not round.is_participating):
|
127 |
+
|
128 |
+
# Calculate conversion value vs preference value
|
129 |
+
conversion_value = (round.shares_issued / self.cap_table.total_shares) * exit_scenario.exit_valuation
|
130 |
+
preference_value = preference_payouts.get(round.name, 0)
|
131 |
+
|
132 |
+
if conversion_value > preference_value:
|
133 |
+
# They convert - add back their preference and include in common distribution
|
134 |
+
remaining_proceeds += preference_value
|
135 |
+
total_participating_shares += round.shares_issued
|
136 |
+
|
137 |
+
# Phase 3: Final distribution to common + participating preferred
|
138 |
+
if total_participating_shares > 0:
|
139 |
+
price_per_participating_share = remaining_proceeds / total_participating_shares
|
140 |
+
common_proceeds = price_per_participating_share * self.cap_table.common_shares
|
141 |
+
else:
|
142 |
+
common_proceeds = remaining_proceeds
|
143 |
+
|
144 |
+
# Calculate option value
|
145 |
+
price_per_common_share = common_proceeds / self.cap_table.common_shares if self.cap_table.common_shares > 0 else 0
|
146 |
+
option_value_per_share = max(0, price_per_common_share - self.cap_table.strike_price)
|
147 |
+
total_option_value = option_value_per_share * self.cap_table.your_options
|
148 |
+
|
149 |
+
return ScenarioResult(
|
150 |
+
scenario_name=exit_scenario.name,
|
151 |
+
exit_valuation=exit_scenario.exit_valuation,
|
152 |
+
option_value=total_option_value,
|
153 |
+
price_per_share=price_per_common_share,
|
154 |
+
common_proceeds=common_proceeds
|
155 |
+
)
|
156 |
+
|
157 |
+
def calculate_multiple_scenarios(self, scenarios: List[ExitScenario]) -> List[ScenarioResult]:
|
158 |
+
"""Calculate equity value for multiple exit scenarios"""
|
159 |
+
results = []
|
160 |
+
for scenario in scenarios:
|
161 |
+
if scenario.exit_valuation > 0: # Only calculate positive exit values
|
162 |
+
result = self.calculate_scenario(scenario)
|
163 |
+
results.append(result)
|
164 |
+
return results
|
165 |
+
|
166 |
+
def get_liquidation_summary(self) -> Dict[str, Any]:
|
167 |
+
"""Get summary of liquidation terms"""
|
168 |
+
participating_status = []
|
169 |
+
for round in self.cap_table.funding_rounds:
|
170 |
+
if round.shares_issued > 0:
|
171 |
+
status = 'Participating' if round.is_participating else 'Non-Participating'
|
172 |
+
participating_status.append(f"{round.name}: {status}")
|
173 |
+
|
174 |
+
return {
|
175 |
+
'total_shares': self.cap_table.total_shares,
|
176 |
+
'common_shares': self.cap_table.common_shares,
|
177 |
+
'preferred_shares': self.cap_table.total_preferred_shares,
|
178 |
+
'your_options': self.cap_table.your_options,
|
179 |
+
'your_equity_percentage': self.cap_table.your_equity_percentage,
|
180 |
+
'strike_price': self.cap_table.strike_price,
|
181 |
+
'participating_status': participating_status,
|
182 |
+
'break_even_price': self.cap_table.strike_price
|
183 |
+
}
|
184 |
+
|
185 |
+
|
186 |
+
def create_cap_table(
|
187 |
+
total_shares: int, your_options: int, strike_price: float,
|
188 |
+
seed_shares: int = 0, seed_capital: float = 0, seed_multiple: float = 1.0, seed_participating: bool = False,
|
189 |
+
series_a_shares: int = 0, series_a_capital: float = 0, series_a_multiple: float = 1.0, series_a_participating: bool = False,
|
190 |
+
series_b_shares: int = 0, series_b_capital: float = 0, series_b_multiple: float = 1.0, series_b_participating: bool = False
|
191 |
+
) -> CapTable:
|
192 |
+
"""Factory function to create a CapTable from individual parameters"""
|
193 |
+
|
194 |
+
funding_rounds = []
|
195 |
+
|
196 |
+
if seed_shares > 0 or seed_capital > 0:
|
197 |
+
funding_rounds.append(FundingRound(
|
198 |
+
name='Seed',
|
199 |
+
shares_issued=seed_shares,
|
200 |
+
capital_raised=seed_capital,
|
201 |
+
liquidation_multiple=seed_multiple,
|
202 |
+
is_participating=seed_participating
|
203 |
+
))
|
204 |
+
|
205 |
+
if series_a_shares > 0 or series_a_capital > 0:
|
206 |
+
funding_rounds.append(FundingRound(
|
207 |
+
name='Series A',
|
208 |
+
shares_issued=series_a_shares,
|
209 |
+
capital_raised=series_a_capital,
|
210 |
+
liquidation_multiple=series_a_multiple,
|
211 |
+
is_participating=series_a_participating
|
212 |
+
))
|
213 |
+
|
214 |
+
if series_b_shares > 0 or series_b_capital > 0:
|
215 |
+
funding_rounds.append(FundingRound(
|
216 |
+
name='Series B',
|
217 |
+
shares_issued=series_b_shares,
|
218 |
+
capital_raised=series_b_capital,
|
219 |
+
liquidation_multiple=series_b_multiple,
|
220 |
+
is_participating=series_b_participating
|
221 |
+
))
|
222 |
+
|
223 |
+
return CapTable(
|
224 |
+
total_shares=total_shares,
|
225 |
+
your_options=your_options,
|
226 |
+
strike_price=strike_price,
|
227 |
+
funding_rounds=funding_rounds
|
228 |
+
)
|