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"])
|