summaryrefslogtreecommitdiff
path: root/backend/tests/test_dcf_edge_cases.py
blob: 94a960b8f93ea0f9ca3758a0085f4f51d32e65bb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
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 == {}