"""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, }