summaryrefslogtreecommitdiff
path: root/backend/tests/test_api.py
diff options
context:
space:
mode:
authorTyler Hoang <tyler@tylerhoang.xyz>2026-05-18 00:10:00 -0700
committerTyler Hoang <tyler@tylerhoang.xyz>2026-05-18 00:10:00 -0700
commit1df80dc1f868b906c5540b66a211947c6886484d (patch)
tree5f1a71ef6259c0f6892a09e48198033e8062ca6e /backend/tests/test_api.py
parentd3d91f6faca104dcb98487adc0c5ff5d268ed8f7 (diff)
feat: add get_financials() with income/balance/cashflow builders
Implements _build_income, _build_balance, _build_cash_flow, and get_financials() (cached) in data_service.py. Annual mode appends TTM (income/CF) and MRQ (balance) columns; quarterly mode returns up to 8 periods. Adds annual_frame helper and 5 TDD tests covering column labels, TTM sums, MRQ values, FCF computation, and empty-statement graceful returns. Test count: 19 → 24. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'backend/tests/test_api.py')
-rw-r--r--backend/tests/test_api.py140
1 files changed, 140 insertions, 0 deletions
diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py
index 5d76dfd..f98b582 100644
--- a/backend/tests/test_api.py
+++ b/backend/tests/test_api.py
@@ -14,6 +14,7 @@ def clear_service_caches() -> None:
data_service.STATEMENT_CACHE.clear()
data_service.SHARES_CACHE.clear()
data_service.RATIO_CACHE.clear()
+ data_service.FINANCIALS_CACHE.clear()
def quarterly_frame(rows: dict[str, list[float]]) -> pd.DataFrame:
@@ -21,6 +22,11 @@ def quarterly_frame(rows: dict[str, list[float]]) -> pd.DataFrame:
return pd.DataFrame(rows, index=columns).T
+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 test_health() -> None:
assert main.health() == {"status": "ok"}
@@ -403,3 +409,137 @@ def test_overview_uses_computed_sources_and_ratios(monkeypatch) -> None:
assert overview["meta"]["sources"]["ratios.price_to_book"] == "computed"
assert overview["meta"]["field_availability"]["ratios.ev_to_ebitda"] is True
assert any(signal["key"] == "Valuation" and "24.5x" in signal["value"] for signal in overview["signals"])
+
+
+def test_build_income_annual_columns_and_ttm(monkeypatch) -> None:
+ data_service.STATEMENT_CACHE.clear()
+ data_service.FINANCIALS_CACHE.clear()
+
+ inc_annual = annual_frame({
+ "Total Revenue": [391_000.0, 383_300.0, 394_300.0, 365_800.0],
+ "Gross Profit": [180_700.0, 169_100.0, 170_800.0, 152_800.0],
+ "Net Income": [ 93_700.0, 97_000.0, 99_800.0, 94_700.0],
+ })
+ inc_q = quarterly_frame({
+ "Total Revenue": [100_000.0, 95_000.0, 98_000.0, 97_000.0],
+ "Gross Profit": [ 46_000.0, 44_000.0, 45_000.0, 43_000.0],
+ "Net Income": [ 24_000.0, 23_000.0, 24_000.0, 22_000.0],
+ })
+
+ monkeypatch.setattr(data_service, "get_income_statement",
+ lambda sym, quarterly=False: inc_q if quarterly else inc_annual)
+ monkeypatch.setattr(data_service, "get_balance_sheet",
+ lambda sym, quarterly=False: pd.DataFrame())
+ monkeypatch.setattr(data_service, "get_cash_flow",
+ lambda sym, quarterly=False: pd.DataFrame())
+
+ result = data_service.get_financials("AAPL", "annual")
+ income = result["income"]
+
+ assert income["columns"] == ["FY 2024", "FY 2023", "FY 2022", "FY 2021", "TTM"]
+ rev_row = next(r for r in income["rows"] if r["label"] == "Total Revenue")
+ assert rev_row["is_total"] is True
+ assert rev_row["values"][4] == 390_000.0 # sum of 4 quarters
+ margin_row = next(r for r in income["rows"] if r["label"] == "gross margin")
+ assert margin_row["is_margin"] is True
+ assert margin_row["values"][0] is not None # FY 2024 gross margin computed
+
+
+def test_build_income_quarterly_eight_columns(monkeypatch) -> None:
+ data_service.STATEMENT_CACHE.clear()
+ data_service.FINANCIALS_CACHE.clear()
+
+ cols = pd.to_datetime([
+ "2025-12-31","2025-09-30","2025-06-30","2025-03-31",
+ "2024-12-31","2024-09-30","2024-06-30","2024-03-31",
+ ])
+ inc_q8 = pd.DataFrame(
+ {"Total Revenue": [100_000.0]*8, "Net Income": [25_000.0]*8},
+ index=cols,
+ ).T
+
+ monkeypatch.setattr(data_service, "get_income_statement",
+ lambda sym, quarterly=False: inc_q8)
+ monkeypatch.setattr(data_service, "get_balance_sheet",
+ lambda sym, quarterly=False: pd.DataFrame())
+ monkeypatch.setattr(data_service, "get_cash_flow",
+ lambda sym, quarterly=False: pd.DataFrame())
+
+ result = data_service.get_financials("AAPL", "quarterly")
+ income = result["income"]
+ assert len(income["columns"]) == 8
+ assert income["columns"][0] == "Q4 2025"
+ assert "TTM" not in income["columns"]
+
+
+def test_build_balance_mrq_column(monkeypatch) -> None:
+ data_service.STATEMENT_CACHE.clear()
+ data_service.FINANCIALS_CACHE.clear()
+
+ bal_annual = annual_frame({"Total Assets": [364_900.0, 335_000.0, 352_800.0, 351_000.0]})
+ bal_q = quarterly_frame({"Total Assets": [371_900.0, 368_000.0, 360_000.0, 355_000.0]})
+
+ monkeypatch.setattr(data_service, "get_income_statement",
+ lambda sym, quarterly=False: pd.DataFrame())
+ monkeypatch.setattr(data_service, "get_balance_sheet",
+ lambda sym, quarterly=False: bal_q if quarterly else bal_annual)
+ monkeypatch.setattr(data_service, "get_cash_flow",
+ lambda sym, quarterly=False: pd.DataFrame())
+
+ result = data_service.get_financials("AAPL", "annual")
+ balance = result["balance"]
+ assert balance["columns"][-1] == "MRQ"
+ assets_row = next(r for r in balance["rows"] if r["label"] == "Total Assets")
+ assert assets_row["values"][-1] == 371_900.0 # MRQ value
+
+
+def test_build_cash_flow_fcf(monkeypatch) -> None:
+ data_service.STATEMENT_CACHE.clear()
+ data_service.FINANCIALS_CACHE.clear()
+
+ cf_annual = annual_frame({
+ "Operating Cash Flow": [118_300.0, 110_500.0, 122_200.0, 104_000.0],
+ "Capital Expenditure": [ -9_500.0, -10_900.0, -10_700.0, -8_600.0],
+ })
+ cf_q = quarterly_frame({
+ "Operating Cash Flow": [30_000.0, 29_000.0, 31_000.0, 28_000.0],
+ "Capital Expenditure": [-2_500.0, -2_400.0, -2_500.0, -2_400.0],
+ })
+ inc_annual = annual_frame({"Total Revenue": [391_000.0, 383_300.0, 394_300.0, 365_800.0]})
+ inc_q = quarterly_frame({"Total Revenue": [100_000.0, 95_000.0, 98_000.0, 97_000.0]})
+
+ def mock_cf(sym, quarterly=False):
+ return cf_q if quarterly else cf_annual
+
+ def mock_inc(sym, quarterly=False):
+ return inc_q if quarterly else inc_annual
+
+ monkeypatch.setattr(data_service, "get_income_statement", mock_inc)
+ monkeypatch.setattr(data_service, "get_balance_sheet", lambda sym, quarterly=False: pd.DataFrame())
+ monkeypatch.setattr(data_service, "get_cash_flow", mock_cf)
+
+ result = data_service.get_financials("AAPL", "annual")
+ cf = result["cash_flow"]
+
+ fcf_row = next(r for r in cf["rows"] if r["label"] == "Free Cash Flow")
+ assert fcf_row["is_total"] is True
+ # FY 2024: 118300 + (-9500) = 108800
+ assert fcf_row["values"][0] == 108_800.0
+ fcf_margin = next(r for r in cf["rows"] if r["label"] == "FCF margin")
+ assert fcf_margin["is_margin"] is True
+ assert fcf_margin["values"][0] is not None
+
+
+def test_get_financials_empty_statements(monkeypatch) -> None:
+ data_service.STATEMENT_CACHE.clear()
+ data_service.FINANCIALS_CACHE.clear()
+
+ monkeypatch.setattr(data_service, "get_income_statement", lambda sym, quarterly=False: pd.DataFrame())
+ monkeypatch.setattr(data_service, "get_balance_sheet", lambda sym, quarterly=False: pd.DataFrame())
+ monkeypatch.setattr(data_service, "get_cash_flow", lambda sym, quarterly=False: pd.DataFrame())
+
+ result = data_service.get_financials("AAPL", "annual")
+ assert result["income"]["columns"] == []
+ assert result["income"]["rows"] == []
+ assert result["balance"]["columns"] == []
+ assert result["cash_flow"]["columns"] == []