File size: 5,544 Bytes
36dada9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b54f5bb
36dada9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from datetime import date

import TerraFin.analytics.analysis.fundamental.dcf.reverse as reverse_module
from TerraFin.analytics.analysis.fundamental.dcf.models import DCFInputTemplate, RateCurvePoint, RateCurveSnapshot
from TerraFin.analytics.analysis.fundamental.dcf.reverse import build_stock_reverse_dcf_payload


class _FakeCurve:
    def yield_at(self, maturity_years: float) -> float:
        if maturity_years >= 30:
            return 4.6
        return 4.0 + (0.08 * float(maturity_years))


def _curve_snapshot() -> RateCurveSnapshot:
    points = [
        RateCurvePoint(maturity_years=0.25, yield_pct=4.8, label="13W"),
        RateCurvePoint(maturity_years=2.0, yield_pct=4.2, label="2Y"),
        RateCurvePoint(maturity_years=5.0, yield_pct=4.4, label="5Y"),
        RateCurvePoint(maturity_years=10.0, yield_pct=4.8, label="10Y"),
        RateCurvePoint(maturity_years=30.0, yield_pct=4.6, label="30Y"),
    ]
    return RateCurveSnapshot(
        as_of="2026-04-05",
        source="test",
        points=points,
        fitted_points=list(points),
        fallback_yield_pct=4.4,
        curve=_FakeCurve(),
    )


def _stock_template() -> DCFInputTemplate:
    return DCFInputTemplate(
        status="ready",
        entity_type="stock",
        symbol="AAPL",
        as_of=date(2026, 4, 5),
        current_price=None,
        base_cash_flow_per_share=8.0,
        base_growth_pct=10.0,
        terminal_growth_pct=3.0,
        yearly_risk_free_rates_pct=[4.08, 4.16, 4.24, 4.32, 4.40],
        terminal_risk_free_rate_pct=4.6,
        discount_spread_pct=6.0,
        rate_curve=_curve_snapshot(),
        assumptions={
            "beta": 1.2,
            "equityRiskPremiumPct": 5.0,
            "cashflowSource": "3yr_avg",
            "growthSource": "eps",
        },
        data_quality={"mode": "live", "sources": ["test"]},
        warnings=[],
    )


def test_reverse_dcf_solves_for_market_implied_growth(monkeypatch) -> None:
    template = _stock_template()
    expected_growth_pct = 12.75
    expected_result = reverse_module._result_for_initial_growth(
        template,
        years=10,
        profile_key="early_maturity",
        initial_growth_pct=expected_growth_pct,
    )
    template.current_price = expected_result.intrinsic_value

    monkeypatch.setattr(reverse_module, "build_stock_template", lambda ticker, overrides=None: template)

    payload = build_stock_reverse_dcf_payload("AAPL", projection_years=10, growth_profile="early_maturity")

    assert payload["status"] == "ready"
    assert abs(payload["impliedGrowthPct"] - expected_growth_pct) < 0.05
    assert abs(payload["modelPrice"] - template.current_price) < 0.05
    assert payload["projectionYears"] == 10
    assert payload["growthProfile"]["key"] == "early_maturity"
    assert len(payload["projectedCashFlows"]) == 10


def test_reverse_dcf_returns_insufficient_data_when_template_is_not_ready(monkeypatch) -> None:
    template = _stock_template()
    template.status = "insufficient_data"
    template.current_price = None
    template.warnings = ["Current price unavailable."]

    monkeypatch.setattr(reverse_module, "build_stock_template", lambda ticker, overrides=None: template)

    payload = build_stock_reverse_dcf_payload("AAPL")

    assert payload["status"] == "insufficient_data"
    assert payload["impliedGrowthPct"] is None
    assert payload["projectedCashFlows"] == []
    assert payload["warnings"] == ["Current price unavailable."]


def test_reverse_dcf_marks_payload_insufficient_when_price_is_below_model_floor(monkeypatch) -> None:
    template = _stock_template()
    floor_result = reverse_module._result_for_initial_growth(
        template,
        years=5,
        profile_key="fully_mature",
        initial_growth_pct=-99.0,
    )
    template.current_price = floor_result.intrinsic_value / 2.0

    monkeypatch.setattr(reverse_module, "build_stock_template", lambda ticker, overrides=None: template)

    payload = build_stock_reverse_dcf_payload("AAPL", projection_years=5, growth_profile="fully_mature")

    assert payload["status"] == "insufficient_data"
    assert payload["impliedGrowthPct"] is None
    assert any("model floor" in warning for warning in payload["warnings"])


def test_reverse_dcf_preserves_computed_beta_assumptions_from_stock_template(monkeypatch) -> None:
    template = _stock_template()
    expected_growth_pct = 11.5
    expected_result = reverse_module._result_for_initial_growth(
        template,
        years=5,
        profile_key="early_maturity",
        initial_growth_pct=expected_growth_pct,
    )
    template.current_price = expected_result.intrinsic_value
    template.assumptions = {
        **template.assumptions,
        "beta": 1.37,
        "betaSource": "computed",
        "betaMethodId": "beta_5y_monthly",
        "betaBenchmarkSymbol": "^SPX",
    }
    template.warnings = [
        "Ticker beta was unavailable from provider metadata; using computed beta_5y_monthly vs ^SPX."
    ]

    monkeypatch.setattr(reverse_module, "build_stock_template", lambda ticker, overrides=None: template)

    payload = build_stock_reverse_dcf_payload("AAPL", projection_years=5, growth_profile="early_maturity")

    assert payload["status"] == "ready"
    assert payload["assumptions"]["betaSource"] == "computed"
    assert payload["assumptions"]["betaMethodId"] == "beta_5y_monthly"
    assert payload["assumptions"]["betaBenchmarkSymbol"] == "^SPX"
    assert any("using computed beta_5y_monthly" in warning for warning in payload["warnings"])