Spaces:
Sleeping
Sleeping
File size: 9,095 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 |
"""
Data models and core equity calculation logic
"""
from dataclasses import dataclass
from typing import List, Optional, Dict, Any
@dataclass
class FundingRound:
"""Represents a funding round (Seed, Series A, etc.)"""
name: str
shares_issued: int
capital_raised: float
liquidation_multiple: float
is_participating: bool
@property
def liquidation_preference(self) -> float:
"""Total liquidation preference for this round"""
return self.capital_raised * self.liquidation_multiple
@dataclass
class CapTable:
"""Represents the company's capitalization table"""
total_shares: int
your_options: int
strike_price: float
funding_rounds: List[FundingRound]
@property
def total_preferred_shares(self) -> int:
"""Total preferred shares across all rounds"""
return sum(round.shares_issued for round in self.funding_rounds)
@property
def common_shares(self) -> int:
"""Total common shares available"""
return self.total_shares - self.total_preferred_shares
@property
def your_equity_percentage(self) -> float:
"""Your equity percentage of total company"""
return (self.your_options / self.total_shares) * 100 if self.total_shares > 0 else 0
@dataclass
class ExitScenario:
"""Represents an exit scenario with name and valuation"""
name: str
exit_valuation: float
@dataclass
class ScenarioResult:
"""Result of calculating equity value for one exit scenario"""
scenario_name: str
exit_valuation: float
option_value: float
price_per_share: float
common_proceeds: float
error: Optional[str] = None
@property
def value_per_option(self) -> float:
"""Value per individual option"""
return self.price_per_share
def roi_percentage(self, investment_cost: float) -> float:
"""Calculate ROI percentage"""
if investment_cost <= 0:
return float('inf') if self.option_value > 0 else 0
return ((self.option_value - investment_cost) / investment_cost) * 100
class EquityCalculator:
"""Core equity calculation engine"""
def __init__(self, cap_table: CapTable):
self.cap_table = cap_table
def calculate_scenario(self, exit_scenario: ExitScenario) -> ScenarioResult:
"""Calculate equity value for a single exit scenario"""
if self.cap_table.common_shares <= 0:
return ScenarioResult(
scenario_name=exit_scenario.name,
exit_valuation=exit_scenario.exit_valuation,
option_value=0,
price_per_share=0,
common_proceeds=0,
error='Preferred shares exceed total shares'
)
# Phase 1: Pay liquidation preferences (newest rounds first)
remaining_proceeds = exit_scenario.exit_valuation
participating_shareholders = []
# Sort funding rounds by reverse order (newest first)
sorted_rounds = sorted(self.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)
preference_payouts = {}
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
preference_payouts[round.name] = preference_payout
if round.is_participating:
participating_shareholders.append({
'round': round.name,
'shares': round.shares_issued
})
# Phase 2: Handle non-participating conversions
participating_preferred_shares = sum(p['shares'] for p in participating_shareholders)
total_participating_shares = self.cap_table.common_shares + participating_preferred_shares
# Check if non-participating preferred should convert
for round in sorted_rounds:
if (round.shares_issued > 0 and round.capital_raised > 0
and not round.is_participating):
# Calculate conversion value vs preference value
conversion_value = (round.shares_issued / self.cap_table.total_shares) * exit_scenario.exit_valuation
preference_value = preference_payouts.get(round.name, 0)
if conversion_value > preference_value:
# They convert - add back their preference and include in common distribution
remaining_proceeds += preference_value
total_participating_shares += round.shares_issued
# Phase 3: Final distribution to common + participating preferred
if total_participating_shares > 0:
price_per_participating_share = remaining_proceeds / total_participating_shares
common_proceeds = price_per_participating_share * self.cap_table.common_shares
else:
common_proceeds = remaining_proceeds
# Calculate option value
price_per_common_share = common_proceeds / self.cap_table.common_shares if self.cap_table.common_shares > 0 else 0
option_value_per_share = max(0, price_per_common_share - self.cap_table.strike_price)
total_option_value = option_value_per_share * self.cap_table.your_options
return ScenarioResult(
scenario_name=exit_scenario.name,
exit_valuation=exit_scenario.exit_valuation,
option_value=total_option_value,
price_per_share=price_per_common_share,
common_proceeds=common_proceeds
)
def calculate_multiple_scenarios(self, scenarios: List[ExitScenario]) -> List[ScenarioResult]:
"""Calculate equity value for multiple exit scenarios"""
results = []
for scenario in scenarios:
if scenario.exit_valuation > 0: # Only calculate positive exit values
result = self.calculate_scenario(scenario)
results.append(result)
return results
def get_liquidation_summary(self) -> Dict[str, Any]:
"""Get summary of liquidation terms"""
participating_status = []
for round in self.cap_table.funding_rounds:
if round.shares_issued > 0:
status = 'Participating' if round.is_participating else 'Non-Participating'
participating_status.append(f"{round.name}: {status}")
return {
'total_shares': self.cap_table.total_shares,
'common_shares': self.cap_table.common_shares,
'preferred_shares': self.cap_table.total_preferred_shares,
'your_options': self.cap_table.your_options,
'your_equity_percentage': self.cap_table.your_equity_percentage,
'strike_price': self.cap_table.strike_price,
'participating_status': participating_status,
'break_even_price': self.cap_table.strike_price
}
def create_cap_table(
total_shares: int, your_options: int, strike_price: float,
seed_shares: int = 0, seed_capital: float = 0, seed_multiple: float = 1.0, seed_participating: bool = False,
series_a_shares: int = 0, series_a_capital: float = 0, series_a_multiple: float = 1.0, series_a_participating: bool = False,
series_b_shares: int = 0, series_b_capital: float = 0, series_b_multiple: float = 1.0, series_b_participating: bool = False
) -> CapTable:
"""Factory function to create a CapTable from individual parameters"""
funding_rounds = []
if seed_shares > 0 or seed_capital > 0:
funding_rounds.append(FundingRound(
name='Seed',
shares_issued=seed_shares,
capital_raised=seed_capital,
liquidation_multiple=seed_multiple,
is_participating=seed_participating
))
if series_a_shares > 0 or series_a_capital > 0:
funding_rounds.append(FundingRound(
name='Series A',
shares_issued=series_a_shares,
capital_raised=series_a_capital,
liquidation_multiple=series_a_multiple,
is_participating=series_a_participating
))
if series_b_shares > 0 or series_b_capital > 0:
funding_rounds.append(FundingRound(
name='Series B',
shares_issued=series_b_shares,
capital_raised=series_b_capital,
liquidation_multiple=series_b_multiple,
is_participating=series_b_participating
))
return CapTable(
total_shares=total_shares,
your_options=your_options,
strike_price=strike_price,
funding_rounds=funding_rounds
) |