From ccdc3572c802a29f54ceb85d020bb9ab8ffe040c Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Mon, 29 Jun 2026 01:37:18 -0700 Subject: test: add DCF and valuation backend tests --- backend/tests/test_dcf_math.py | 205 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 backend/tests/test_dcf_math.py (limited to 'backend/tests/test_dcf_math.py') 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"]) -- cgit v1.3-2-g0d8e