diff options
Diffstat (limited to 'services/valuation_service.py')
| -rw-r--r-- | services/valuation_service.py | 82 |
1 files changed, 82 insertions, 0 deletions
diff --git a/services/valuation_service.py b/services/valuation_service.py new file mode 100644 index 0000000..f876f78 --- /dev/null +++ b/services/valuation_service.py @@ -0,0 +1,82 @@ +"""DCF valuation engine — Gordon Growth Model.""" +import numpy as np +import pandas as pd + + +def run_dcf( + fcf_series: pd.Series, + shares_outstanding: float, + wacc: float = 0.10, + terminal_growth: float = 0.03, + projection_years: int = 5, +) -> dict: + """ + Run a DCF model and return per-year breakdown plus intrinsic value per share. + + Args: + fcf_series: Annual FCF values, most recent first (yfinance order). + 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. + + 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 {} + + # Use last N years of FCF (sorted oldest → newest) + historical = fcf_series.sort_index().dropna().values + if len(historical) < 2: + return {} + + # Compute average YoY growth rate from historical FCF + 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) + + # Cap growth rate to reasonable bounds [-0.5, 0.5] + 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]) # most recent FCF + + # Project FCFs + projected_fcfs = [] + for year in range(1, projection_years + 1): + fcf = base_fcf * ((1 + growth_rate) ** year) + projected_fcfs.append(fcf) + + # Discount projected FCFs + 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 value (Gordon Growth Model) + 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, + } |
