from fastapi.testclient import TestClient import TerraFin.interface.market_insights.data_routes as market_insights_routes import TerraFin.interface.stock.data_routes as stock_routes from TerraFin.analytics.analysis.risk.models import BetaEstimate from TerraFin.data.cache.registry import reset_cache_manager from TerraFin.interface.server import create_app from TerraFin.interface.watchlist_service import reset_watchlist_service def _client() -> TestClient: reset_watchlist_service() reset_cache_manager() return TestClient(create_app()) def _ready_payload(symbol: str, entity_type: str) -> dict: return { "status": "ready", "entityType": entity_type, "symbol": symbol, "asOf": "2026-04-05", "currentPrice": 100.0, "currentIntrinsicValue": 112.0, "upsidePct": 12.0, "scenarios": { "base": { "key": "base", "label": "Base", "status": "ready", "growthShiftPct": 0.0, "discountRateShiftBps": 0, "terminalGrowthShiftBps": 0, "intrinsicValue": 112.0, "upsidePct": 12.0, "terminalValue": 80.0, "terminalGrowthPct": 3.0, "terminalDiscountRatePct": 9.0, "projectedCashFlows": [], } }, "assumptions": {"baseGrowthPct": 6.0}, "sensitivity": { "discountRateShiftBps": [-100, 0, 100], "terminalGrowthShiftBps": [-100, 0, 100], "cells": [], }, "rateCurve": { "source": "test", "asOf": "2026-04-05", "fitRmse": 0.01, "fallbackUsed": False, "points": [], "fittedPoints": [], }, "dataQuality": {"mode": "live", "sources": ["test"]}, "warnings": [], "methods": None, } def test_market_insights_sp500_dcf_endpoint_contract(monkeypatch) -> None: monkeypatch.setattr( market_insights_routes, "build_sp500_dcf_payload", lambda *args, **kwargs: { **_ready_payload("S&P 500", "index"), "methods": [ { "key": "shareholder_yield", "label": "Shareholder Yield", "description": "Case A", "weight": 0.5, "currentIntrinsicValue": 105.0, "upsidePct": 5.0, }, { "key": "earnings_power", "label": "Earnings Power", "description": "Case B", "weight": 0.5, "currentIntrinsicValue": 119.0, "upsidePct": 19.0, }, ], }, ) client = _client() response = client.get("/market-insights/api/dcf/sp500") assert response.status_code == 200 body = response.json() assert body["entityType"] == "index" assert body["symbol"] == "S&P 500" assert body["currentIntrinsicValue"] == 112.0 assert len(body["methods"]) == 2 def test_market_insights_sp500_dcf_post_accepts_overrides(monkeypatch) -> None: captured = {} def _build(*args, **kwargs): captured["overrides"] = kwargs.get("overrides") return _ready_payload("S&P 500", "index") monkeypatch.setattr(market_insights_routes, "build_sp500_dcf_payload", _build) client = _client() response = client.post( "/market-insights/api/dcf/sp500", json={ "baseYearEps": 240.0, "terminalGrowthPct": 3.8, "terminalEquityRiskPremiumPct": 4.4, "terminalRoePct": 19.5, "yearlyAssumptions": [ { "yearOffset": 1, "growthPct": 12.0, "payoutRatioPct": 30.0, "buybackRatioPct": 45.0, "equityRiskPremiumPct": 5.5, } ], }, ) assert response.status_code == 200 assert captured["overrides"].base_year_eps == 240.0 assert captured["overrides"].terminal_growth_pct == 3.8 assert len(captured["overrides"].yearly_assumptions) == 1 def test_stock_dcf_endpoint_contract(monkeypatch) -> None: monkeypatch.setattr( stock_routes, "build_stock_dcf_payload", lambda ticker, *args, **kwargs: _ready_payload(ticker.upper(), "stock"), ) client = _client() response = client.get("/stock/api/dcf?ticker=aapl") assert response.status_code == 200 body = response.json() assert body["entityType"] == "stock" assert body["symbol"] == "AAPL" assert "base" in body["scenarios"] def test_stock_dcf_endpoint_allows_insufficient_data(monkeypatch) -> None: monkeypatch.setattr( stock_routes, "build_stock_dcf_payload", lambda ticker, *args, **kwargs: { **_ready_payload(ticker.upper(), "stock"), "status": "insufficient_data", "currentIntrinsicValue": None, "upsidePct": None, "scenarios": {}, "warnings": ["Free cash flow per share is not positive."], }, ) client = _client() response = client.get("/stock/api/dcf?ticker=loss") assert response.status_code == 200 body = response.json() assert body["status"] == "insufficient_data" assert body["warnings"] == ["Free cash flow per share is not positive."] def test_stock_beta_estimate_endpoint_contract(monkeypatch) -> None: monkeypatch.setattr( stock_routes, "estimate_beta_5y_monthly", lambda ticker: BetaEstimate( symbol=ticker.upper(), benchmark_symbol="^SPX", benchmark_label="S&P 500", method_id="beta_5y_monthly", lookback_years=5, frequency="monthly", beta=1.11, observations=60, r_squared=0.42, status="ready", warnings=[], ), ) monkeypatch.setattr( stock_routes, "estimate_beta_5y_monthly_adjusted", lambda ticker: BetaEstimate( symbol=ticker.upper(), benchmark_symbol="^SPX", benchmark_label="S&P 500", method_id="beta_5y_monthly_adjusted", lookback_years=5, frequency="monthly", beta=1.07, observations=60, r_squared=0.42, status="ready", warnings=[], ), ) client = _client() response = client.get("/stock/api/beta-estimate?ticker=googl") assert response.status_code == 200 body = response.json() assert body["symbol"] == "GOOGL" assert body["benchmarkSymbol"] == "^SPX" assert body["methodId"] == "beta_5y_monthly" assert body["adjustedMethodId"] == "beta_5y_monthly_adjusted" assert body["beta"] == 1.11 assert body["adjustedBeta"] == 1.07 assert body["observations"] == 60 assert body["rSquared"] == 0.42 def test_stock_dcf_post_accepts_overrides(monkeypatch) -> None: captured = {} def _build(ticker, *args, **kwargs): captured["ticker"] = ticker captured["overrides"] = kwargs.get("overrides") return _ready_payload(ticker.upper(), "stock") monkeypatch.setattr(stock_routes, "build_stock_dcf_payload", _build) client = _client() response = client.post( "/stock/api/dcf?ticker=nvda", json={ "baseCashFlowPerShare": 4.2, "baseGrowthPct": 18.0, "terminalGrowthPct": 3.4, "beta": 1.3, "equityRiskPremiumPct": 4.9, "fcfBaseSource": "ttm", }, ) assert response.status_code == 200 assert captured["ticker"] == "nvda" assert captured["overrides"].base_cash_flow_per_share == 4.2 assert captured["overrides"].beta == 1.3 assert captured["overrides"].fcf_base_source == "ttm" def test_fcf_history_endpoint_contract(monkeypatch) -> None: monkeypatch.setattr( stock_routes, "build_fcf_history_payload", lambda ticker, **kwargs: { "ticker": ticker.upper(), "sharesOutstanding": 100.0, "ttmFcfPerShare": 0.85, "ttmSource": "quarterly_ttm", "candidates": { "threeYearAvg": 0.43, "latestAnnual": -0.50, "ttm": 0.85, }, "autoSelectedSource": "3yr_avg", "sharesNote": "Per-year FCF/share is computed using current sharesOutstanding.", "history": [ {"year": "2022", "fcf": 80.0, "fcfPerShare": 0.80}, {"year": "2023", "fcf": 100.0, "fcfPerShare": 1.00}, {"year": "2024", "fcf": -50.0, "fcfPerShare": -0.50}, ], }, ) client = _client() response = client.get("/stock/api/fcf-history?ticker=moh&years=5") assert response.status_code == 200 body = response.json() assert body["ticker"] == "MOH" assert body["ttmFcfPerShare"] == 0.85 assert body["candidates"]["threeYearAvg"] == 0.43 assert body["candidates"]["latestAnnual"] == -0.50 assert body["candidates"]["ttm"] == 0.85 assert body["autoSelectedSource"] == "3yr_avg" assert len(body["history"]) == 3 assert body["history"][2]["year"] == "2024" assert body["history"][2]["fcfPerShare"] == -0.50 def test_stock_dcf_post_accepts_turnaround_and_horizon(monkeypatch) -> None: captured = {} def _build(ticker, *args, **kwargs): captured["ticker"] = ticker captured["overrides"] = kwargs.get("overrides") captured["projection_years"] = kwargs.get("projection_years") return _ready_payload(ticker.upper(), "stock") monkeypatch.setattr(stock_routes, "build_stock_dcf_payload", _build) client = _client() response = client.post( "/stock/api/dcf?ticker=moh", json={ "projectionYears": 10, "breakevenYear": 3, "breakevenCashFlowPerShare": 2.5, "postBreakevenGrowthPct": 12.0, }, ) assert response.status_code == 200 assert captured["ticker"] == "moh" assert captured["projection_years"] == 10 assert captured["overrides"].breakeven_year == 3 assert captured["overrides"].breakeven_cash_flow_per_share == 2.5 assert captured["overrides"].post_breakeven_growth_pct == 12.0