import pandas as pd import pytest from fastapi.testclient import TestClient from app import main from app.services import data_service def annual_frame(rows: dict[str, list[float]]) -> pd.DataFrame: columns = pd.to_datetime(["2024-09-30", "2023-09-30", "2022-09-30", "2021-09-30"]) return pd.DataFrame(rows, index=columns).T def quarterly_frame(rows: dict[str, list[float]]) -> pd.DataFrame: columns = pd.to_datetime(["2025-12-31", "2025-09-30", "2025-06-30", "2025-03-31"]) return pd.DataFrame(rows, index=columns).T def clear_valuation_cache() -> None: data_service.VALUATION_CACHE.clear() def advanced_payload() -> dict[str, float | int | list[float]]: return { "base_revenue": 1_000.0, "revenue_growth": [0.10, 0.05], "ebitda_margin": [0.30, 0.32], "dna_pct_revenue": [0.05, 0.05], "capex_pct_revenue": [0.07, 0.07], "nwc_chg_pct_delta_rev": [0.02, 0.02], "tax_rate": [0.25, 0.25], "wacc": 0.10, "terminal_growth": 0.03, "projection_years": 2, } def test_simple_valuation_includes_five_by_five_sensitivity_grid(monkeypatch: pytest.MonkeyPatch) -> None: # Given: historical free cash flow data for the simple DCF endpoint. clear_valuation_cache() cash_flow = annual_frame({ "Operating Cash Flow": [100.0, 90.0, 80.0, 70.0], "Capital Expenditure": [-10.0, -9.0, -8.0, -7.0], }) monkeypatch.setattr(data_service, "get_cash_flow", lambda symbol, quarterly=False: pd.DataFrame() if quarterly else cash_flow) monkeypatch.setattr(data_service, "get_income_statement", lambda symbol, quarterly=False: pd.DataFrame()) monkeypatch.setattr(data_service, "get_balance_sheet", lambda symbol, quarterly=False: pd.DataFrame()) monkeypatch.setattr(data_service, "get_company_info", lambda symbol: {"currentPrice": 150.0}) monkeypatch.setattr(data_service, "get_shares_outstanding", lambda symbol: 1_000_000_000.0) # When: the simple valuation service is called. result = data_service.get_valuation("ref") # Then: the DCF result includes the mandated WACC x terminal-growth matrix. dcf = result["dcf"] sensitivity = dcf["sensitivity"] assert result["currency"] == "USD" assert result["currency_warning"] is None assert sensitivity["wacc"] == [0.08, 0.09, 0.10, 0.11, 0.12] assert sensitivity["terminal_growth"] == [0.015, 0.02, 0.03, 0.04, 0.045] assert len(sensitivity["implied_prices"]) == 5 assert all(len(row) == 5 for row in sensitivity["implied_prices"]) assert sensitivity["implied_prices"][2][2] == pytest.approx(dcf["intrinsic_value_per_share"]) def test_advanced_valuation_endpoint_returns_inputs_and_sensitivity(monkeypatch: pytest.MonkeyPatch) -> None: # Given: symbol-level balance sheet inputs and a base valuation shell. clear_valuation_cache() monkeypatch.setattr( data_service, "get_valuation", lambda symbol: { "symbol": "REF", "current_price": 42.0, "shares_outstanding": 100.0, "dcf": {"available": False, "wacc": 0.10, "terminal_growth": 0.03, "projection_years": 5}, "ev_ebitda": {"available": False}, "ev_revenue": {"available": False}, "price_to_book": {"available": False}, }, ) monkeypatch.setattr(data_service, "get_shares_outstanding", lambda symbol: 100.0) monkeypatch.setattr( data_service, "get_balance_sheet", lambda symbol, quarterly=False: quarterly_frame({ "Total Debt": [50.0, 0.0, 0.0, 0.0], "Cash And Cash Equivalents": [10.0, 0.0, 0.0, 0.0], "Preferred Stock": [5.0, 0.0, 0.0, 0.0], "Minority Interest": [3.0, 0.0, 0.0, 0.0], }), ) client = TestClient(main.app) # When: the advanced valuation endpoint receives an explicit build payload. response = client.post("/api/tickers/REF/valuation/advanced", json=advanced_payload()) # Then: it returns a full valuation response with base-case-centered sensitivity. assert response.status_code == 200 body = response.json() dcf = body["dcf"] sensitivity = dcf["sensitivity"] assert body["symbol"] == "REF" assert body["currency"] == "USD" assert body["currency_warning"] is None assert dcf["advanced_inputs"] == advanced_payload() assert dcf["intrinsic_value_per_share"] == pytest.approx(29.838067203890013) assert sensitivity["wacc"] == [0.08, 0.09, 0.10, 0.11, 0.12] assert sensitivity["terminal_growth"] == [0.015, 0.02, 0.03, 0.04, 0.045] assert len(sensitivity["implied_prices"]) == 5 assert all(len(row) == 5 for row in sensitivity["implied_prices"]) assert sensitivity["implied_prices"][2][2] == pytest.approx(dcf["intrinsic_value_per_share"]) def test_advanced_valuation_endpoint_rejects_short_projection_arrays() -> None: # Given: a payload whose per-year arrays do not cover projection_years. payload = advanced_payload() payload["revenue_growth"] = [0.10] client = TestClient(main.app) # When: the invalid payload is submitted. response = client.post("/api/tickers/REF/valuation/advanced", json=payload) # Then: FastAPI returns a validation response instead of an unhandled exception. assert response.status_code == 422 assert "projection_years" in response.text