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