"""Valuation engines for DCF and EV/EBITDA.""" import numpy as np import pandas as pd GROWTH_FLOOR = -0.50 GROWTH_CAP = 0.50 MIN_BASE_MAGNITUDE = 1e-9 def _cap_growth(value: float) -> float: return max(GROWTH_FLOOR, min(GROWTH_CAP, float(value))) def compute_historical_growth_rate(fcf_series: pd.Series) -> float | None: """ Return a capped median YoY FCF growth rate from historical data. Notes: - skips periods with near-zero prior FCF - skips sign-flip periods (negative to positive or vice versa), since the implied "growth rate" is usually not economically meaningful """ historical = fcf_series.sort_index().dropna().astype(float).values if len(historical) < 2: return None growth_rates = [] for i in range(1, len(historical)): previous = float(historical[i - 1]) current = float(historical[i]) if abs(previous) < MIN_BASE_MAGNITUDE: continue if previous <= 0 or current <= 0: continue growth_rates.append((current - previous) / previous) if not growth_rates: return None return _cap_growth(float(np.median(growth_rates))) 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, total_debt: float = 0.0, cash_and_equivalents: float = 0.0, preferred_equity: float = 0.0, minority_interest: float = 0.0, ) -> dict: """ Run a simple FCFF-style DCF and bridge enterprise value to equity value. Returns an error payload when inputs are mathematically invalid. """ if fcf_series.empty or shares_outstanding <= 0: return {} historical = fcf_series.sort_index().dropna().astype(float).values if len(historical) < 2: return {} if wacc <= 0: return {"error": "WACC must be greater than 0%."} if terminal_growth >= wacc: return {"error": "Terminal growth must be lower than WACC."} if growth_rate_override is not None: growth_rate = _cap_growth(growth_rate_override) else: historical_growth = compute_historical_growth_rate(fcf_series) growth_rate = historical_growth if historical_growth is not None else 0.05 base_fcf = float(historical[-1]) projected_fcfs = [] for year in range(1, projection_years + 1): projected_fcfs.append(base_fcf * ((1 + growth_rate) ** year)) discounted_fcfs = [] for i, fcf in enumerate(projected_fcfs, start=1): discounted_fcfs.append(fcf / ((1 + wacc) ** i)) fcf_pv_sum = float(sum(discounted_fcfs)) terminal_fcf = float(projected_fcfs[-1]) * (1 + terminal_growth) terminal_value = terminal_fcf / (wacc - terminal_growth) terminal_value_pv = terminal_value / ((1 + wacc) ** projection_years) enterprise_value = fcf_pv_sum + terminal_value_pv total_debt = float(total_debt or 0.0) cash_and_equivalents = float(cash_and_equivalents or 0.0) preferred_equity = float(preferred_equity or 0.0) minority_interest = float(minority_interest or 0.0) net_debt = total_debt - cash_and_equivalents equity_value = enterprise_value - net_debt - preferred_equity - minority_interest intrinsic_value_per_share = equity_value / shares_outstanding return { "intrinsic_value_per_share": intrinsic_value_per_share, "enterprise_value": enterprise_value, "equity_value": equity_value, "net_debt": net_debt, "cash_and_equivalents": cash_and_equivalents, "total_debt": total_debt, "preferred_equity": preferred_equity, "minority_interest": minority_interest, "terminal_value": terminal_value, "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.""" 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, }