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
|
"""DCF valuation engine — Gordon Growth Model + EV/EBITDA."""
import numpy as np
import pandas as pd
def compute_historical_growth_rate(fcf_series: pd.Series) -> float | None:
"""
Return the median YoY FCF growth rate from historical data, capped at [-0.5, 0.5].
Returns None if there is insufficient data.
"""
historical = fcf_series.sort_index().dropna().values
if len(historical) < 2:
return None
growth_rates = []
for i in range(1, len(historical)):
if historical[i - 1] != 0:
g = (historical[i] - historical[i - 1]) / abs(historical[i - 1])
growth_rates.append(g)
if not growth_rates:
return None
raw = float(np.median(growth_rates))
return max(-0.50, min(0.50, raw))
def run_dcf(
fcf_series: pd.Series,
shares_outstanding: float,
wacc: float = 0.10,
terminal_growth: float = 0.03,
projection_years: int = 5,
growth_rate_override: float | None = None,
) -> dict:
"""
Run a DCF model and return per-year breakdown plus intrinsic value per share.
Args:
fcf_series: Annual FCF values (yfinance order — most recent first).
shares_outstanding: Diluted shares outstanding.
wacc: Weighted average cost of capital (decimal, e.g. 0.10).
terminal_growth: Perpetuity growth rate (decimal, e.g. 0.03).
projection_years: Number of years to project FCFs.
growth_rate_override: If provided, use this growth rate instead of
computing from historical FCF data (decimal, e.g. 0.08).
Returns:
dict with keys:
intrinsic_value_per_share, total_pv, terminal_value_pv,
fcf_pv_sum, years, projected_fcfs, discounted_fcfs,
growth_rate_used
"""
if fcf_series.empty or shares_outstanding <= 0:
return {}
historical = fcf_series.sort_index().dropna().values
if len(historical) < 2:
return {}
if growth_rate_override is not None:
growth_rate = max(-0.50, min(0.50, growth_rate_override))
else:
growth_rates = []
for i in range(1, len(historical)):
if historical[i - 1] != 0:
g = (historical[i] - historical[i - 1]) / abs(historical[i - 1])
growth_rates.append(g)
raw_growth = float(np.median(growth_rates)) if growth_rates else 0.05
growth_rate = max(-0.50, min(0.50, raw_growth))
base_fcf = float(historical[-1])
projected_fcfs = []
for year in range(1, projection_years + 1):
fcf = base_fcf * ((1 + growth_rate) ** year)
projected_fcfs.append(fcf)
discounted_fcfs = []
for i, fcf in enumerate(projected_fcfs, start=1):
pv = fcf / ((1 + wacc) ** i)
discounted_fcfs.append(pv)
fcf_pv_sum = sum(discounted_fcfs)
terminal_fcf = projected_fcfs[-1] * (1 + terminal_growth)
terminal_value = terminal_fcf / (wacc - terminal_growth)
terminal_value_pv = terminal_value / ((1 + wacc) ** projection_years)
total_pv = fcf_pv_sum + terminal_value_pv
intrinsic_value_per_share = total_pv / shares_outstanding
return {
"intrinsic_value_per_share": intrinsic_value_per_share,
"total_pv": total_pv,
"terminal_value_pv": terminal_value_pv,
"fcf_pv_sum": fcf_pv_sum,
"years": list(range(1, projection_years + 1)),
"projected_fcfs": projected_fcfs,
"discounted_fcfs": discounted_fcfs,
"growth_rate_used": growth_rate,
"base_fcf": base_fcf,
}
def run_ev_ebitda(
ebitda: float,
total_debt: float,
total_cash: float,
shares_outstanding: float,
target_multiple: float,
) -> dict:
"""
Derive implied equity value per share from an EV/EBITDA multiple.
Steps:
implied_ev = ebitda * target_multiple
net_debt = total_debt - total_cash
equity_value = implied_ev - net_debt
price_per_share = equity_value / shares_outstanding
Returns {} if EBITDA <= 0 or any required input is missing/invalid.
"""
if not ebitda or ebitda <= 0:
return {}
if not shares_outstanding or shares_outstanding <= 0:
return {}
if not target_multiple or target_multiple <= 0:
return {}
implied_ev = ebitda * target_multiple
net_debt = (total_debt or 0.0) - (total_cash or 0.0)
equity_value = implied_ev - net_debt
return {
"implied_ev": implied_ev,
"net_debt": net_debt,
"equity_value": equity_value,
"implied_price_per_share": equity_value / shares_outstanding,
"target_multiple_used": target_multiple,
}
|