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_edge_cases.py | 155 +++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 backend/tests/test_dcf_edge_cases.py (limited to 'backend/tests/test_dcf_edge_cases.py') diff --git a/backend/tests/test_dcf_edge_cases.py b/backend/tests/test_dcf_edge_cases.py new file mode 100644 index 0000000..94a960b --- /dev/null +++ b/backend/tests/test_dcf_edge_cases.py @@ -0,0 +1,155 @@ +"""Boundary and invalid-input tests for the DCF engine.""" + +import pandas as pd +import pytest + +from app.services.data_service import _run_dcf + + +def _fcf_series(base: float, growth: float) -> pd.Series: + """Return a two-point historical FCF series whose median YoY growth equals `growth`.""" + prior = base / (1 + growth) + return pd.Series([prior, base], index=pd.to_datetime(["2023-09-30", "2024-09-30"])) + + +def _expected_mid_year_enterprise_value( + base_fcf: float, + growth: float, + wacc: float, + terminal_growth: float, + projection_years: int, +) -> float: + projected = [base_fcf * ((1 + growth) ** year) for year in range(1, projection_years + 1)] + discounted = [fcf / ((1 + wacc) ** (year - 0.5)) for year, fcf in enumerate(projected, start=1)] + 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)) + return sum(discounted) + terminal_value_pv + + +def test_run_dcf_two_year_horizon_uses_mid_year_discounting() -> None: + fcf = _fcf_series(100.0, 0.05) + expected_enterprise_value = _expected_mid_year_enterprise_value( + base_fcf=100.0, + growth=0.05, + wacc=0.10, + terminal_growth=0.03, + projection_years=2, + ) + + result = _run_dcf( + fcf_series=fcf, + shares_outstanding=10.0, + wacc=0.10, + terminal_growth=0.03, + projection_years=2, + ) + + assert result["enterprise_value"] == pytest.approx(expected_enterprise_value) + + +def test_run_dcf_terminal_growth_equals_wacc_is_error() -> None: + fcf = _fcf_series(100.0, 0.05) + result = _run_dcf( + fcf_series=fcf, + shares_outstanding=10.0, + wacc=0.10, + terminal_growth=0.10, + projection_years=5, + ) + assert "error" in result + assert "Terminal growth" in result["error"] + assert "intrinsic_value_per_share" not in result + + +def test_run_dcf_terminal_growth_above_wacc_is_error() -> None: + fcf = _fcf_series(100.0, 0.05) + result = _run_dcf( + fcf_series=fcf, + shares_outstanding=10.0, + wacc=0.10, + terminal_growth=0.11, + projection_years=5, + ) + assert "error" in result + assert "Terminal growth" in result["error"] + assert "intrinsic_value_per_share" not in result + + +def test_run_dcf_zero_wacc_is_error() -> None: + fcf = _fcf_series(100.0, 0.05) + result = _run_dcf( + fcf_series=fcf, + shares_outstanding=10.0, + wacc=0.0, + terminal_growth=0.03, + projection_years=5, + ) + assert "error" in result + assert "WACC" in result["error"] + assert "intrinsic_value_per_share" not in result + + +def test_run_dcf_negative_base_fcf_is_error() -> None: + """The most recent FCF is negative, making the DCF not meaningful.""" + fcf = pd.Series([100.0, -50.0], index=pd.to_datetime(["2023-09-30", "2024-09-30"])) + result = _run_dcf( + fcf_series=fcf, + shares_outstanding=10.0, + wacc=0.10, + terminal_growth=0.03, + projection_years=5, + ) + assert "error" in result + assert "negative" in result["error"].lower() or "zero" in result["error"].lower() + assert "intrinsic_value_per_share" not in result + + +def test_run_dcf_zero_base_fcf_is_error() -> None: + fcf = pd.Series([100.0, 0.0], index=pd.to_datetime(["2023-09-30", "2024-09-30"])) + result = _run_dcf( + fcf_series=fcf, + shares_outstanding=10.0, + wacc=0.10, + terminal_growth=0.03, + projection_years=5, + ) + assert "error" in result + assert "zero" in result["error"].lower() + assert "intrinsic_value_per_share" not in result + + +def test_run_dcf_zero_shares_returns_empty() -> None: + fcf = _fcf_series(100.0, 0.05) + result = _run_dcf( + fcf_series=fcf, + shares_outstanding=0.0, + wacc=0.10, + terminal_growth=0.03, + projection_years=5, + ) + assert result == {} + + +def test_run_dcf_negative_shares_returns_empty() -> None: + fcf = _fcf_series(100.0, 0.05) + result = _run_dcf( + fcf_series=fcf, + shares_outstanding=-1.0, + wacc=0.10, + terminal_growth=0.03, + projection_years=5, + ) + assert result == {} + + +def test_run_dcf_insufficient_history_returns_empty() -> None: + fcf = pd.Series([100.0], index=pd.to_datetime(["2024-09-30"])) + result = _run_dcf( + fcf_series=fcf, + shares_outstanding=10.0, + wacc=0.10, + terminal_growth=0.03, + projection_years=5, + ) + assert result == {} -- cgit v1.3-2-g0d8e