startup-valuation 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ """Startup Valuation — Comprehensive valuation library for startups."""
2
+
3
+ from startup_valuation.types import (
4
+ ValuationResult,
5
+ Distribution,
6
+ Scenario,
7
+ SensitivityResult,
8
+ )
9
+
10
+ __version__ = "0.1.0"
11
+ __all__ = [
12
+ "ValuationResult",
13
+ "Distribution",
14
+ "Scenario",
15
+ "SensitivityResult",
16
+ ]
@@ -0,0 +1,375 @@
1
+ """Advanced valuation techniques: Black-Scholes, Binomial, Monte Carlo, Scenario Analysis.
2
+
3
+ Chapter 4: Advanced Techniques
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import math
9
+ from typing import Any
10
+
11
+ import numpy as np
12
+ from scipy import stats
13
+
14
+ from startup_valuation.types import Distribution, Scenario, ValuationResult
15
+
16
+
17
+ def black_scholes(
18
+ underlying: float,
19
+ strike: float,
20
+ risk_free_rate: float,
21
+ volatility: float,
22
+ time_to_maturity: float,
23
+ ) -> ValuationResult:
24
+ """Calculate call option value using Black-Scholes model.
25
+
26
+ Formula: C = N(d₁)S - N(d₂)Ke^(-rT)
27
+ where:
28
+ d₁ = [ln(S/K) + (r + σ²/2)T] / [σ√T]
29
+ d₂ = d₁ - σ√T
30
+
31
+ Args:
32
+ underlying: Current price of underlying asset (S).
33
+ strike: Strike price (K).
34
+ risk_free_rate: Risk-free rate (r).
35
+ volatility: Volatility of underlying (σ).
36
+ time_to_maturity: Time to maturity in years (T).
37
+
38
+ Returns:
39
+ ValuationResult with call option value.
40
+
41
+ Example:
42
+ >>> result = black_scholes(20_000_000, 5_000_000, 0.05, 0.40, 1.0)
43
+ >>> round(result.value / 1_000_000, 2)
44
+ 15.24
45
+ """
46
+ if underlying <= 0 or strike <= 0:
47
+ raise ValueError("underlying and strike must be positive")
48
+ if volatility <= 0:
49
+ raise ValueError("volatility must be positive")
50
+ if time_to_maturity <= 0:
51
+ raise ValueError("time_to_maturity must be positive")
52
+
53
+ d1 = (math.log(underlying / strike) + (risk_free_rate + volatility**2 / 2) * time_to_maturity) / (
54
+ volatility * math.sqrt(time_to_maturity)
55
+ )
56
+ d2 = d1 - volatility * math.sqrt(time_to_maturity)
57
+
58
+ call_value = underlying * stats.norm.cdf(d1) - strike * math.exp(-risk_free_rate * time_to_maturity) * stats.norm.cdf(d2)
59
+
60
+ return ValuationResult(
61
+ value=call_value,
62
+ method="Black-Scholes",
63
+ inputs={
64
+ "underlying": underlying,
65
+ "strike": strike,
66
+ "risk_free_rate": risk_free_rate,
67
+ "volatility": volatility,
68
+ "time_to_maturity": time_to_maturity,
69
+ },
70
+ assumptions=[
71
+ "European-style option (exercise only at maturity)",
72
+ "Volatility is constant",
73
+ "No dividends",
74
+ "Continuous trading",
75
+ ],
76
+ chapter="4",
77
+ )
78
+
79
+
80
+ def binomial_tree(
81
+ underlying: float,
82
+ strike: float,
83
+ risk_free_rate: float,
84
+ volatility: float,
85
+ time_to_maturity: float,
86
+ steps: int = 3,
87
+ ) -> ValuationResult:
88
+ """Value an option using a binomial tree model.
89
+
90
+ Formulas:
91
+ u = e^(σ√Δt)
92
+ d = e^(-σ√Δt)
93
+ p = (e^(rΔt) - d) / (u - d)
94
+
95
+ Args:
96
+ underlying: Current price of underlying asset.
97
+ strike: Strike price.
98
+ risk_free_rate: Risk-free rate.
99
+ volatility: Volatility.
100
+ time_to_maturity: Time to maturity in years.
101
+ steps: Number of time steps.
102
+
103
+ Returns:
104
+ ValuationResult with option value.
105
+ """
106
+ dt = time_to_maturity / steps
107
+ u = math.exp(volatility * math.sqrt(dt))
108
+ d = math.exp(-volatility * math.sqrt(dt))
109
+ p = (math.exp(risk_free_rate * dt) - d) / (u - d)
110
+
111
+ # Build terminal node values
112
+ node_values = [max(underlying * (u ** i) * (d ** (steps - i)) - strike, 0) for i in range(steps + 1)]
113
+
114
+ # Backward induction
115
+ for step in range(steps - 1, -1, -1):
116
+ node_values = [
117
+ math.exp(-risk_free_rate * dt) * (p * node_values[i + 1] + (1 - p) * node_values[i])
118
+ for i in range(step + 1)
119
+ ]
120
+
121
+ option_value = node_values[0]
122
+
123
+ return ValuationResult(
124
+ value=option_value,
125
+ method="Binomial Tree",
126
+ inputs={
127
+ "underlying": underlying,
128
+ "strike": strike,
129
+ "risk_free_rate": risk_free_rate,
130
+ "volatility": volatility,
131
+ "time_to_maturity": time_to_maturity,
132
+ "steps": steps,
133
+ },
134
+ assumptions=[
135
+ "American-style option (can exercise at any node)",
136
+ "Binomial tree converges to Black-Scholes as steps → ∞",
137
+ ],
138
+ chapter="4",
139
+ )
140
+
141
+
142
+ def monte_carlo_valuation(
143
+ market_size_dist: Distribution,
144
+ market_share_dist: Distribution,
145
+ margin_dist: Distribution,
146
+ exit_multiple: float,
147
+ discount_rate: float,
148
+ years: float,
149
+ num_simulations: int = 10_000,
150
+ seed: int | None = None,
151
+ ) -> ValuationResult:
152
+ """Monte Carlo simulation for startup valuation.
153
+
154
+ Formula: PV = (Market Size × Market Share × Margin × Multiple) / (1 + r)^n
155
+
156
+ Args:
157
+ market_size_dist: Distribution for market size.
158
+ market_share_dist: Distribution for market share.
159
+ margin_dist: Distribution for profit margin.
160
+ exit_multiple: Exit multiple.
161
+ discount_rate: Discount rate.
162
+ years: Years to exit.
163
+ num_simulations: Number of simulations.
164
+ seed: Random seed for reproducibility.
165
+
166
+ Returns:
167
+ ValuationResult with mean, percentiles, and full distribution.
168
+ """
169
+ rng = np.random.default_rng(seed)
170
+
171
+ def sample(dist: Distribution, n: int) -> Any:
172
+ if dist.dist_type == "normal":
173
+ return rng.normal(dist.params["mean"], dist.params["std"], n)
174
+ elif dist.dist_type == "uniform":
175
+ return rng.uniform(dist.params["min"], dist.params["max"], n)
176
+ elif dist.dist_type == "triangular":
177
+ return rng.triangular(dist.params["min"], dist.params["mode"], dist.params["max"], n)
178
+ else:
179
+ raise ValueError(f"Unknown distribution type: {dist.dist_type}")
180
+
181
+ market_sizes = sample(market_size_dist, num_simulations)
182
+ market_shares = sample(market_share_dist, num_simulations)
183
+ margins = sample(margin_dist, num_simulations)
184
+
185
+ terminal_values = market_sizes * market_shares * margins * exit_multiple
186
+ present_values = terminal_values / ((1 + discount_rate) ** years)
187
+
188
+ present_values = present_values[present_values > 0] # Filter negative values
189
+
190
+ mean_val = float(np.mean(present_values))
191
+ p10 = float(np.percentile(present_values, 10))
192
+ p50 = float(np.percentile(present_values, 50))
193
+ p90 = float(np.percentile(present_values, 90))
194
+ std_val = float(np.std(present_values))
195
+
196
+ return ValuationResult(
197
+ value=mean_val,
198
+ method="Monte Carlo Valuation",
199
+ inputs={
200
+ "market_size_dist": f"{market_size_dist.dist_type}({market_size_dist.params})",
201
+ "market_share_dist": f"{market_share_dist.dist_type}({market_share_dist.params})",
202
+ "margin_dist": f"{margin_dist.dist_type}({margin_dist.params})",
203
+ "exit_multiple": exit_multiple,
204
+ "discount_rate": discount_rate,
205
+ "years": years,
206
+ "num_simulations": num_simulations,
207
+ },
208
+ assumptions=[
209
+ f"Results based on {num_simulations:,} simulations",
210
+ "Distributions are independent",
211
+ ],
212
+ chapter="4",
213
+ )
214
+
215
+
216
+ def scenario_analysis(scenarios: list[Scenario]) -> ValuationResult:
217
+ """Calculate expected value across scenarios.
218
+
219
+ Formula: E[V] = Σ pᵢ × Vᵢ
220
+
221
+ Args:
222
+ scenarios: List of scenarios with name, probability, and value.
223
+
224
+ Returns:
225
+ ValuationResult with expected value.
226
+
227
+ Example:
228
+ >>> scenarios = [
229
+ ... Scenario("bull", 0.20, 10_000_000),
230
+ ... Scenario("base", 0.60, 5_000_000),
231
+ ... Scenario("bear", 0.20, 1_000_000),
232
+ ... ]
233
+ >>> result = scenario_analysis(scenarios)
234
+ >>> result.value
235
+ 5200000.0
236
+ """
237
+ if not scenarios:
238
+ raise ValueError("at least one scenario required")
239
+
240
+ total_prob = sum(s.probability for s in scenarios)
241
+ if abs(total_prob - 1.0) > 0.01:
242
+ raise ValueError(f"scenario probabilities must sum to 1.0, got {total_prob}")
243
+
244
+ ev = sum(s.probability * s.value for s in scenarios)
245
+
246
+ return ValuationResult(
247
+ value=ev,
248
+ method="Scenario Analysis",
249
+ inputs={"scenarios": [(s.name, s.probability, s.value) for s in scenarios]},
250
+ assumptions=["Scenarios are mutually exclusive and exhaustive"],
251
+ chapter="4",
252
+ )
253
+
254
+
255
+ def ltv_cac_valuation(
256
+ ltv_dist: Distribution,
257
+ cac_dist: Distribution,
258
+ market_size_dist: Distribution,
259
+ num_simulations: int = 10_000,
260
+ seed: int | None = None,
261
+ ) -> ValuationResult:
262
+ """Valuation using LTV/CAC ratio with Monte Carlo.
263
+
264
+ Formula: Valuation = (LTV/CAC) × Market Size × 0.1
265
+
266
+ Args:
267
+ ltv_dist: Distribution for LTV.
268
+ cac_dist: Distribution for CAC.
269
+ market_size_dist: Distribution for market size.
270
+ num_simulations: Number of simulations.
271
+ seed: Random seed.
272
+
273
+ Returns:
274
+ ValuationResult with mean valuation.
275
+ """
276
+ rng = np.random.default_rng(seed)
277
+
278
+ def sample(dist: Distribution, n: int) -> Any:
279
+ if dist.dist_type == "normal":
280
+ return rng.normal(dist.params["mean"], dist.params["std"], n)
281
+ elif dist.dist_type == "uniform":
282
+ return rng.uniform(dist.params["min"], dist.params["max"], n)
283
+ elif dist.dist_type == "triangular":
284
+ return rng.triangular(dist.params["min"], dist.params["mode"], dist.params["max"], n)
285
+ else:
286
+ raise ValueError(f"Unknown distribution type: {dist.dist_type}")
287
+
288
+ ltvs = sample(ltv_dist, num_simulations)
289
+ cacs = sample(cac_dist, num_simulations)
290
+ market_sizes = sample(market_size_dist, num_simulations)
291
+
292
+ cacs = np.maximum(cacs, 1e-6) # Avoid division by zero
293
+ valuations = (ltvs / cacs) * market_sizes * 0.1
294
+ valuations = valuations[valuations > 0]
295
+
296
+ mean_val = float(np.mean(valuations))
297
+ std_val = float(np.std(valuations))
298
+ min_val = float(np.min(valuations))
299
+ max_val = float(np.max(valuations))
300
+
301
+ return ValuationResult(
302
+ value=mean_val,
303
+ method="LTV/CAC Valuation",
304
+ inputs={
305
+ "ltv_dist": f"{ltv_dist.dist_type}({ltv_dist.params})",
306
+ "cac_dist": f"{cac_dist.dist_type}({cac_dist.params})",
307
+ "market_size_dist": f"{market_size_dist.dist_type}({market_size_dist.params})",
308
+ },
309
+ assumptions=["LTV/CAC > 1 for viable business model"],
310
+ chapter="4",
311
+ )
312
+
313
+
314
+ def binomial_valuation(
315
+ underlying: float,
316
+ strike: float,
317
+ risk_free_rate: float,
318
+ volatility: float,
319
+ time_to_maturity: float,
320
+ steps: int = 50,
321
+ ) -> ValuationResult:
322
+ """Value a startup option using binomial tree with high resolution.
323
+
324
+ Formula: Same as binomial_tree with steps=50 for convergence to Black-Scholes.
325
+
326
+ Args:
327
+ underlying: Current value of startup.
328
+ strike: Liquidation preference or strike price.
329
+ risk_free_rate: Risk-free rate.
330
+ volatility: Volatility of startup value.
331
+ time_to_maturity: Time to exit in years.
332
+ steps: Number of time steps (default 50 for convergence).
333
+
334
+ Returns:
335
+ ValuationResult with option value.
336
+
337
+ Example:
338
+ >>> result = binomial_valuation(20_000_000, 5_000_000, 0.05, 0.40, 1.0, 50)
339
+ >>> round(result.value / 1_000_000, 2)
340
+ 15.24
341
+ """
342
+ dt = time_to_maturity / steps
343
+ u = math.exp(volatility * math.sqrt(dt))
344
+ d = math.exp(-volatility * math.sqrt(dt))
345
+ p = (math.exp(risk_free_rate * dt) - d) / (u - d)
346
+
347
+ # Build terminal node values
348
+ node_values = [max(underlying * (u ** i) * (d ** (steps - i)) - strike, 0) for i in range(steps + 1)]
349
+
350
+ # Backward induction
351
+ for step in range(steps - 1, -1, -1):
352
+ node_values = [
353
+ math.exp(-risk_free_rate * dt) * (p * node_values[i + 1] + (1 - p) * node_values[i])
354
+ for i in range(step + 1)
355
+ ]
356
+
357
+ option_value = node_values[0]
358
+
359
+ return ValuationResult(
360
+ value=option_value,
361
+ method="Binomial Valuation (High Resolution)",
362
+ inputs={
363
+ "underlying": underlying,
364
+ "strike": strike,
365
+ "risk_free_rate": risk_free_rate,
366
+ "volatility": volatility,
367
+ "time_to_maturity": time_to_maturity,
368
+ "steps": steps,
369
+ },
370
+ assumptions=[
371
+ f"Binomial tree with {steps} steps converges to Black-Scholes",
372
+ "European-style option",
373
+ ],
374
+ chapter="4",
375
+ )
@@ -0,0 +1,161 @@
1
+ """Biotech and pharmaceutical valuation methods.
2
+
3
+ Chapter 11: Industry-Specific Valuation Frameworks — Biotech
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import math
9
+
10
+ from startup_valuation.types import ValuationResult
11
+
12
+
13
+ def rnPV(
14
+ cash_flows: list[float],
15
+ probabilities: list[float],
16
+ discount_rate: float,
17
+ development_costs: float = 0,
18
+ ) -> ValuationResult:
19
+ """Calculate risk-adjusted NPV for biotech.
20
+
21
+ Formula: rNPV = Σ[P_success × CFₜ / (1+r)ᵗ] - Development Costs
22
+
23
+ Args:
24
+ cash_flows: Cash flows at each period.
25
+ probabilities: Cumulative probability of success at each period.
26
+ discount_rate: Discount rate.
27
+ development_costs: Remaining development costs.
28
+
29
+ Returns:
30
+ ValuationResult with rNPV.
31
+ """
32
+ if len(cash_flows) != len(probabilities):
33
+ raise ValueError("cash_flows and probabilities must have same length")
34
+
35
+ rnpv = sum(
36
+ p * cf / ((1 + discount_rate) ** t)
37
+ for t, (cf, p) in enumerate(zip(cash_flows, probabilities))
38
+ ) - development_costs
39
+
40
+ return ValuationResult(
41
+ value=rnpv,
42
+ method="Risk-Adjusted NPV (rNPV)",
43
+ inputs={
44
+ "cash_flows": cash_flows,
45
+ "probabilities": probabilities,
46
+ "discount_rate": discount_rate,
47
+ "development_costs": development_costs,
48
+ },
49
+ assumptions=["Probabilities are cumulative success probabilities"],
50
+ chapter="11",
51
+ )
52
+
53
+
54
+ def decision_tree_ev(
55
+ probabilities: list[float],
56
+ terminal_value: float,
57
+ ) -> ValuationResult:
58
+ """Calculate expected value from decision tree.
59
+
60
+ Formula: EV = Π pᵢ × Terminal Value
61
+
62
+ Example:
63
+ >>> result = decision_tree_ev([0.35, 0.60, 0.90], 500_000_000)
64
+ >>> result.value
65
+ 94500000.0
66
+ """
67
+ joint_prob = math.prod(probabilities)
68
+ return ValuationResult(
69
+ value=joint_prob * terminal_value,
70
+ method="Decision Tree Expected Value",
71
+ inputs={"probabilities": probabilities, "terminal_value": terminal_value},
72
+ assumptions=["All stages must succeed sequentially"],
73
+ chapter="11",
74
+ )
75
+
76
+
77
+ def peak_sales(
78
+ patient_population: float,
79
+ penetration_rate: float,
80
+ price: float,
81
+ compliance_rate: float = 1.0,
82
+ ) -> ValuationResult:
83
+ """Calculate peak sales for a drug.
84
+
85
+ Formula: Peak Sales = Population × Penetration × Price × Compliance
86
+
87
+ Example:
88
+ >>> result = peak_sales(50_000, 0.40, 150_000, 0.90)
89
+ >>> result.value
90
+ 2700000000.0
91
+ """
92
+ return ValuationResult(
93
+ value=patient_population * penetration_rate * price * compliance_rate,
94
+ method="Peak Sales",
95
+ inputs={
96
+ "patient_population": patient_population,
97
+ "penetration_rate": penetration_rate,
98
+ "price": price,
99
+ "compliance_rate": compliance_rate,
100
+ },
101
+ chapter="11",
102
+ )
103
+
104
+
105
+ def pipeline_valuation(
106
+ drugs: list[dict[str, float]],
107
+ discount_rate: float,
108
+ ) -> ValuationResult:
109
+ """Value a biotech pipeline.
110
+
111
+ Formula: V = Σ(Peak Sales × Multiple × P_success) / (1+r)ⁿ
112
+
113
+ Args:
114
+ drugs: List of dicts with keys: peak_sales, multiple, p_success, years_to_peak.
115
+ discount_rate: Discount rate.
116
+
117
+ Returns:
118
+ ValuationResult with pipeline value.
119
+
120
+ Example:
121
+ >>> drugs = [{"peak_sales": 2_000_000_000, "multiple": 5, "p_success": 0.60, "years_to_peak": 2}]
122
+ >>> result = pipeline_valuation(drugs, 0.12)
123
+ >>> round(result.value / 1_000_000_000, 2)
124
+ 4.78
125
+ """
126
+ total_value = 0
127
+ for drug in drugs:
128
+ ps = drug["peak_sales"]
129
+ m = drug["multiple"]
130
+ p = drug["p_success"]
131
+ n = drug["years_to_peak"]
132
+ total_value += (ps * m * p) / ((1 + discount_rate) ** n)
133
+
134
+ return ValuationResult(
135
+ value=total_value,
136
+ method="Biotech Pipeline Valuation",
137
+ inputs={"num_drugs": len(drugs), "discount_rate": discount_rate},
138
+ assumptions=["Multiple is 3-5x peak sales for approved drugs"],
139
+ chapter="11",
140
+ )
141
+
142
+
143
+ def overall_success_probability(
144
+ stage_probabilities: list[float],
145
+ ) -> ValuationResult:
146
+ """Calculate overall probability of success across all stages.
147
+
148
+ Formula: P_total = Π P_stage
149
+
150
+ Example:
151
+ >>> result = overall_success_probability([0.35, 0.60, 0.90])
152
+ >>> round(result.value, 4)
153
+ 0.189
154
+ """
155
+ return ValuationResult(
156
+ value=math.prod(stage_probabilities),
157
+ method="Overall Success Probability",
158
+ inputs={"stage_probabilities": stage_probabilities},
159
+ assumptions=["Stages are sequential and independent"],
160
+ chapter="11",
161
+ )