From 547997cbd069e9b958b12a8da38b3a4a257e29e5 Mon Sep 17 00:00:00 2001 From: Openclaw Date: Sun, 29 Mar 2026 01:12:24 -0700 Subject: Fix valuation methodology and documentation --- services/valuation_service.py | 123 +++++++++++++++++++++++------------------- 1 file changed, 69 insertions(+), 54 deletions(-) (limited to 'services/valuation_service.py') diff --git a/services/valuation_service.py b/services/valuation_service.py index c874493..6db4053 100644 --- a/services/valuation_service.py +++ b/services/valuation_service.py @@ -1,25 +1,46 @@ -"""DCF valuation engine — Gordon Growth Model + EV/EBITDA.""" +"""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 the median YoY FCF growth rate from historical data, capped at [-0.5, 0.5]. - Returns None if there is insufficient data. + 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().values + historical = fcf_series.sort_index().dropna().astype(float).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) + 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 - raw = float(np.median(growth_rates)) - return max(-0.50, min(0.50, raw)) + + return _cap_growth(float(np.median(growth_rates))) def run_dcf( @@ -29,67 +50,71 @@ def run_dcf( 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 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 + 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().values + 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 = max(-0.50, min(0.50, growth_rate_override)) + growth_rate = _cap_growth(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)) + 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): - fcf = base_fcf * ((1 + growth_rate) ** year) - projected_fcfs.append(fcf) + projected_fcfs.append(base_fcf * ((1 + growth_rate) ** year)) discounted_fcfs = [] for i, fcf in enumerate(projected_fcfs, start=1): - pv = fcf / ((1 + wacc) ** i) - discounted_fcfs.append(pv) + discounted_fcfs.append(fcf / ((1 + wacc) ** i)) - fcf_pv_sum = sum(discounted_fcfs) + fcf_pv_sum = float(sum(discounted_fcfs)) - terminal_fcf = projected_fcfs[-1] * (1 + terminal_growth) + 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) - total_pv = fcf_pv_sum + terminal_value_pv - intrinsic_value_per_share = total_pv / shares_outstanding + 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, - "total_pv": total_pv, + "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)), @@ -107,17 +132,7 @@ def run_ev_ebitda( 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. - """ + """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: -- cgit v1.3-2-g0d8e