summaryrefslogtreecommitdiff
path: root/backend/tests/test_dcf_math.py
diff options
context:
space:
mode:
Diffstat (limited to 'backend/tests/test_dcf_math.py')
-rw-r--r--backend/tests/test_dcf_math.py205
1 files changed, 205 insertions, 0 deletions
diff --git a/backend/tests/test_dcf_math.py b/backend/tests/test_dcf_math.py
new file mode 100644
index 0000000..0418a02
--- /dev/null
+++ b/backend/tests/test_dcf_math.py
@@ -0,0 +1,205 @@
+"""Exact-math verification tests for the DCF engine.
+
+Each scenario independently recomputes the expected projection, discounting,
+terminal value, enterprise value, equity value, and per-share intrinsic value,
+then asserts the implementation matches. This makes the test itself auditable.
+"""
+
+import pandas as pd
+import pytest
+
+from app.services.data_service import _run_dcf
+
+
+def _fcf_series(prior: float, base: float) -> pd.Series:
+ """Return a two-point historical FCF series: prior -> base (most recent)."""
+ return pd.Series([prior, base], index=pd.to_datetime(["2023-09-30", "2024-09-30"]))
+
+
+def _expected_dcf(
+ base_fcf: float,
+ growth_rate: float,
+ wacc: float,
+ terminal_growth: float,
+ projection_years: int,
+ shares_outstanding: float,
+ total_debt: float = 0.0,
+ cash_and_equivalents: float = 0.0,
+ preferred_equity: float = 0.0,
+ minority_interest: float = 0.0,
+) -> dict:
+ """Independent reference implementation of the DCF math used by `_run_dcf`."""
+ projected = [base_fcf * ((1 + growth_rate) ** yr) for yr in range(1, projection_years + 1)]
+ discounted = [fcf / ((1 + wacc) ** (year - 0.5)) for year, fcf in enumerate(projected, start=1)]
+ fcf_pv_sum = sum(discounted)
+
+ terminal_fcf = projected[-1] * (1 + terminal_growth)
+ terminal_value = terminal_fcf / (wacc - terminal_growth)
+ terminal_value_pv = terminal_value / ((1 + wacc) ** (projection_years - 0.5))
+
+ enterprise_value = fcf_pv_sum + terminal_value_pv
+ 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 {
+ "fcf_pv_sum": fcf_pv_sum,
+ "terminal_value_pv": terminal_value_pv,
+ "enterprise_value": enterprise_value,
+ "net_debt": net_debt,
+ "equity_value": equity_value,
+ "intrinsic_value_per_share": intrinsic_value_per_share,
+ }
+
+
+_APPROX = pytest.approx
+
+
+def test_run_dcf_one_year_with_equity_bridge() -> None:
+ """1-year projection with debt, cash, preferred, and minority claims."""
+ fcf = _fcf_series(100.0, 105.0) # exact 5% growth
+ expected = _expected_dcf(
+ base_fcf=105.0,
+ growth_rate=0.05,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=1,
+ shares_outstanding=10.0,
+ total_debt=20.0,
+ cash_and_equivalents=5.0,
+ preferred_equity=2.0,
+ minority_interest=3.0,
+ )
+
+ result = _run_dcf(
+ fcf_series=fcf,
+ shares_outstanding=10.0,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=1,
+ total_debt=20.0,
+ cash_and_equivalents=5.0,
+ preferred_equity=2.0,
+ minority_interest=3.0,
+ )
+
+ assert result["base_fcf"] == 105.0
+ assert result["growth_rate_used"] == _APPROX(0.05)
+ assert result["fcf_pv_sum"] == _APPROX(expected["fcf_pv_sum"])
+ assert result["terminal_value_pv"] == _APPROX(expected["terminal_value_pv"])
+ assert result["enterprise_value"] == _APPROX(expected["enterprise_value"])
+ assert result["net_debt"] == expected["net_debt"]
+ assert result["equity_value"] == _APPROX(expected["equity_value"])
+ assert result["intrinsic_value_per_share"] == _APPROX(expected["intrinsic_value_per_share"])
+
+
+def test_run_dcf_three_years_no_claims() -> None:
+ """3-year projection with a clean capital structure."""
+ fcf = _fcf_series(100.0, 105.0)
+ expected = _expected_dcf(
+ base_fcf=105.0,
+ growth_rate=0.05,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=3,
+ shares_outstanding=10.0,
+ )
+
+ result = _run_dcf(
+ fcf_series=fcf,
+ shares_outstanding=10.0,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=3,
+ )
+
+ assert result["base_fcf"] == 105.0
+ assert result["fcf_pv_sum"] == _APPROX(expected["fcf_pv_sum"])
+ assert result["terminal_value_pv"] == _APPROX(expected["terminal_value_pv"])
+ assert result["enterprise_value"] == _APPROX(expected["enterprise_value"])
+ assert result["equity_value"] == _APPROX(expected["equity_value"])
+ assert result["intrinsic_value_per_share"] == _APPROX(expected["intrinsic_value_per_share"])
+ assert result["net_debt"] == 0.0
+
+
+def test_run_dcf_five_years_default_horizon() -> None:
+ """5-year projection matching the API default horizon."""
+ fcf = _fcf_series(100.0, 105.0)
+ expected = _expected_dcf(
+ base_fcf=105.0,
+ growth_rate=0.05,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=5,
+ shares_outstanding=10.0,
+ )
+
+ result = _run_dcf(
+ fcf_series=fcf,
+ shares_outstanding=10.0,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=5,
+ )
+
+ assert result["base_fcf"] == 105.0
+ assert result["fcf_pv_sum"] == _APPROX(expected["fcf_pv_sum"])
+ assert result["terminal_value_pv"] == _APPROX(expected["terminal_value_pv"])
+ assert result["enterprise_value"] == _APPROX(expected["enterprise_value"])
+ assert result["equity_value"] == _APPROX(expected["equity_value"])
+ assert result["intrinsic_value_per_share"] == _APPROX(expected["intrinsic_value_per_share"])
+
+
+def test_run_dcf_zero_growth_flat_fcf() -> None:
+ """Edge of the model: zero growth projects a flat FCF stream."""
+ fcf = _fcf_series(100.0, 100.0) # exact 0% growth
+ expected = _expected_dcf(
+ base_fcf=100.0,
+ growth_rate=0.0,
+ wacc=0.10,
+ terminal_growth=0.02,
+ projection_years=5,
+ shares_outstanding=10.0,
+ )
+
+ result = _run_dcf(
+ fcf_series=fcf,
+ shares_outstanding=10.0,
+ wacc=0.10,
+ terminal_growth=0.02,
+ projection_years=5,
+ )
+
+ assert result["growth_rate_used"] == _APPROX(0.0)
+ assert result["fcf_pv_sum"] == _APPROX(expected["fcf_pv_sum"])
+ assert result["terminal_value_pv"] == _APPROX(expected["terminal_value_pv"])
+ assert result["enterprise_value"] == _APPROX(expected["enterprise_value"])
+ assert result["intrinsic_value_per_share"] == _APPROX(expected["intrinsic_value_per_share"])
+
+
+def test_run_dcf_uses_historical_growth_rate() -> None:
+ """End-to-end check that the growth-rate estimator feeds the projection.
+
+ A series rising exactly 5% per year should produce the same 5-year result as
+ the hand-computed 5-year scenario (within float tolerance).
+ """
+ fcf = _fcf_series(100.0, 105.0)
+ expected = _expected_dcf(
+ base_fcf=105.0,
+ growth_rate=0.05,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=5,
+ shares_outstanding=10.0,
+ )
+
+ result = _run_dcf(
+ fcf_series=fcf,
+ shares_outstanding=10.0,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=5,
+ )
+
+ assert result["growth_rate_used"] == _APPROX(0.05)
+ assert result["intrinsic_value_per_share"] == _APPROX(expected["intrinsic_value_per_share"])