aboutsummaryrefslogtreecommitdiff
path: root/services/valuation_service.py
diff options
context:
space:
mode:
authorTyler <tyler@tylerhoang.xyz>2026-03-28 23:01:14 -0700
committerTyler <tyler@tylerhoang.xyz>2026-03-28 23:01:14 -0700
commit23675b39b8055a8568cdcf71f66482b9d0cf90a9 (patch)
tree14e42cf710b47072e904b1c21d7322352ae1823c /services/valuation_service.py
Initial commit — Prism financial analysis dashboard
Streamlit app with market bar, price chart, financial statements, DCF valuation engine, comparable companies, and news feed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'services/valuation_service.py')
-rw-r--r--services/valuation_service.py82
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,
+ }