aboutsummaryrefslogtreecommitdiff
path: root/services/valuation_service.py
diff options
context:
space:
mode:
Diffstat (limited to 'services/valuation_service.py')
-rw-r--r--services/valuation_service.py90
1 files changed, 73 insertions, 17 deletions
diff --git a/services/valuation_service.py b/services/valuation_service.py
index f876f78..c874493 100644
--- a/services/valuation_service.py
+++ b/services/valuation_service.py
@@ -1,24 +1,46 @@
-"""DCF valuation engine — Gordon Growth Model."""
+"""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, most recent first (yfinance order).
+ 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:
@@ -29,31 +51,28 @@ def run_dcf(
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))
+ 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]) # most recent FCF
+ base_fcf = float(historical[-1])
- # 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)
@@ -61,7 +80,6 @@ def run_dcf(
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)
@@ -80,3 +98,41 @@ def run_dcf(
"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,
+ }