diff options
| author | Tyler Hoang <tyler@tylerhoang.xyz> | 2026-06-29 01:37:18 -0700 |
|---|---|---|
| committer | Tyler Hoang <tyler@tylerhoang.xyz> | 2026-06-29 01:37:18 -0700 |
| commit | ccdc3572c802a29f54ceb85d020bb9ab8ffe040c (patch) | |
| tree | c4e6a49999019a1f3e9722d3d0865216a60312e5 /backend/tests/test_valuation_advanced_endpoint.py | |
| parent | 74fcf05bf9a8d2bccb0016896ffa49886bc81300 (diff) | |
test: add DCF and valuation backend tests
Diffstat (limited to 'backend/tests/test_valuation_advanced_endpoint.py')
| -rw-r--r-- | backend/tests/test_valuation_advanced_endpoint.py | 126 |
1 files changed, 126 insertions, 0 deletions
diff --git a/backend/tests/test_valuation_advanced_endpoint.py b/backend/tests/test_valuation_advanced_endpoint.py new file mode 100644 index 0000000..3cdb857 --- /dev/null +++ b/backend/tests/test_valuation_advanced_endpoint.py @@ -0,0 +1,126 @@ +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 |
