summaryrefslogtreecommitdiff
path: root/backend/tests/test_api.py
diff options
context:
space:
mode:
Diffstat (limited to 'backend/tests/test_api.py')
-rw-r--r--backend/tests/test_api.py205
1 files changed, 205 insertions, 0 deletions
diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py
index 70b67a7..1c99cf9 100644
--- a/backend/tests/test_api.py
+++ b/backend/tests/test_api.py
@@ -6,6 +6,21 @@ from app import main
from app.services import data_service
+def clear_service_caches() -> None:
+ data_service.INFO_CACHE.clear()
+ data_service.FAST_INFO_CACHE.clear()
+ data_service.PRICE_CACHE.clear()
+ data_service.HISTORY_CACHE.clear()
+ data_service.STATEMENT_CACHE.clear()
+ data_service.SHARES_CACHE.clear()
+ data_service.RATIO_CACHE.clear()
+
+
+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 test_health() -> None:
assert main.health() == {"status": "ok"}
@@ -31,6 +46,22 @@ def test_mocked_ticker_overview(monkeypatch) -> None:
"quote": {"price": 100.0, "prev_close": 98.0, "change": 2.0, "change_pct": 0.0204},
"signals": [],
"stats": {"market_cap": None, "trailing_pe": None, "trailing_eps": None, "volume": None, "average_volume": None, "beta": None},
+ "ratios": {
+ "price_to_book": None,
+ "price_to_sales": None,
+ "ev_to_sales": None,
+ "ev_to_ebitda": None,
+ "gross_margin_ttm": None,
+ "operating_margin_ttm": None,
+ "net_margin_ttm": None,
+ "roe_ttm": None,
+ "roa_ttm": None,
+ "roic_ttm": None,
+ "debt_to_equity": None,
+ "current_ratio": None,
+ "dividend_yield_ttm": None,
+ "dividend_payout_ratio_ttm": None,
+ },
"range_52w": {"low": None, "high": None, "price": 100.0},
"short_interest": {"short_percent_of_float": None, "short_ratio": None, "shares_short": None, "shares_short_prior_month": None, "shares_short_delta_pct": None},
"meta": {"status": "partial", "is_partial": True, "field_availability": {}, "sources": {}},
@@ -40,6 +71,7 @@ def test_mocked_ticker_overview(monkeypatch) -> None:
def test_service_overview_prefers_info_fields(monkeypatch) -> None:
+ clear_service_caches()
monkeypatch.setattr(
data_service,
"get_company_info",
@@ -74,6 +106,7 @@ def test_service_overview_prefers_info_fields(monkeypatch) -> None:
def test_service_overview_falls_back_to_fast_info(monkeypatch) -> None:
+ clear_service_caches()
monkeypatch.setattr(data_service, "get_company_info", lambda symbol: {})
monkeypatch.setattr(
data_service,
@@ -103,6 +136,7 @@ def test_service_overview_falls_back_to_fast_info(monkeypatch) -> None:
def test_service_overview_falls_back_to_search_and_history(monkeypatch) -> None:
+ clear_service_caches()
month_history = [
{"date": "2026-01-01", "close": 98.0, "volume": 1000.0},
{"date": "2026-01-02", "close": 100.0, "volume": 1200.0},
@@ -124,6 +158,7 @@ def test_service_overview_falls_back_to_search_and_history(monkeypatch) -> None:
def test_service_overview_invalid_symbol(monkeypatch) -> None:
+ clear_service_caches()
monkeypatch.setattr(data_service, "get_company_info", lambda symbol: {})
monkeypatch.setattr(data_service, "get_fast_info", lambda symbol: {})
monkeypatch.setattr(data_service, "_pick_search_match", lambda symbol: {})
@@ -149,6 +184,22 @@ def test_ticker_overview_partial_response(monkeypatch) -> None:
"quote": {"price": 100.0, "prev_close": 98.0, "change": 2.0, "change_pct": 0.0204},
"signals": [],
"stats": {"market_cap": None, "trailing_pe": None, "trailing_eps": None, "volume": 1000.0, "average_volume": None, "beta": None},
+ "ratios": {
+ "price_to_book": None,
+ "price_to_sales": None,
+ "ev_to_sales": None,
+ "ev_to_ebitda": None,
+ "gross_margin_ttm": None,
+ "operating_margin_ttm": None,
+ "net_margin_ttm": None,
+ "roe_ttm": None,
+ "roa_ttm": None,
+ "roic_ttm": None,
+ "debt_to_equity": None,
+ "current_ratio": None,
+ "dividend_yield_ttm": None,
+ "dividend_payout_ratio_ttm": None,
+ },
"range_52w": {"low": None, "high": None, "price": 100.0},
"short_interest": {"short_percent_of_float": None, "short_ratio": None, "shares_short": None, "shares_short_prior_month": None, "shares_short_delta_pct": None},
"meta": {"status": "partial", "is_partial": True, "field_availability": {"stats.market_cap": False}, "sources": {"profile.name": "search"}},
@@ -179,3 +230,157 @@ def test_ticker_history_period_mapping(monkeypatch) -> None:
assert len(data_service.get_price_history("AAPL", period="3m")) == 1
assert len(data_service.get_price_history("AAPL", period="6m")) == 1
assert captured == ["1mo", "3mo", "6mo"]
+
+
+def test_compute_ttm_ratios_populates_overlapping_stats(monkeypatch) -> None:
+ clear_service_caches()
+ monkeypatch.setattr(data_service, "get_company_info", lambda symbol: {})
+ monkeypatch.setattr(data_service, "get_fast_info", lambda symbol: {})
+ monkeypatch.setattr(data_service, "get_latest_price", lambda symbol: 50.0)
+ monkeypatch.setattr(data_service, "get_shares_outstanding", lambda symbol: 100.0)
+ monkeypatch.setattr(
+ data_service,
+ "get_income_statement",
+ lambda symbol, quarterly=False: quarterly_frame(
+ {
+ "Total Revenue": [1_000.0, 1_000.0, 1_000.0, 1_000.0],
+ "Gross Profit": [500.0, 500.0, 500.0, 500.0],
+ "Operating Income": [250.0, 250.0, 250.0, 250.0],
+ "Net Income": [100.0, 100.0, 100.0, 100.0],
+ "EBIT": [150.0, 150.0, 150.0, 150.0],
+ "EBITDA": [200.0, 200.0, 200.0, 200.0],
+ "Tax Provision": [20.0, 20.0, 20.0, 20.0],
+ "Pretax Income": [120.0, 120.0, 120.0, 120.0],
+ }
+ ),
+ )
+ monkeypatch.setattr(
+ data_service,
+ "get_balance_sheet",
+ lambda symbol, quarterly=False: quarterly_frame(
+ {
+ "Stockholders Equity": [500.0, 0.0, 0.0, 0.0],
+ "Total Assets": [1_200.0, 0.0, 0.0, 0.0],
+ "Total Debt": [150.0, 0.0, 0.0, 0.0],
+ "Current Assets": [300.0, 0.0, 0.0, 0.0],
+ "Current Liabilities": [100.0, 0.0, 0.0, 0.0],
+ "Cash And Cash Equivalents": [50.0, 0.0, 0.0, 0.0],
+ }
+ ),
+ )
+ monkeypatch.setattr(
+ data_service,
+ "get_cash_flow",
+ lambda symbol, quarterly=False: quarterly_frame({"Cash Dividends Paid": [-10.0, -10.0, -10.0, -10.0]}),
+ )
+
+ ratios = data_service.compute_ttm_ratios("AAPL")
+ assert ratios["market_cap"] == 5_000.0
+ assert ratios["trailing_eps"] == 4.0
+ assert ratios["trailing_pe"] == 12.5
+ assert ratios["price_to_book"] == 10.0
+ assert ratios["price_to_sales"] == 1.25
+ assert ratios["ev_to_sales"] == 1.275
+ assert ratios["gross_margin_ttm"] == 0.5
+ assert ratios["operating_margin_ttm"] == 0.25
+ assert ratios["net_margin_ttm"] == 0.1
+ assert ratios["roe_ttm"] == 0.8
+ assert round(ratios["roic_ttm"], 6) == round((600.0 * (1 - (80.0 / 480.0))) / 600.0, 6)
+ assert ratios["debt_to_equity"] == 0.3
+ assert ratios["current_ratio"] == 3.0
+ assert ratios["dividend_yield_ttm"] == 0.008
+ assert ratios["dividend_payout_ratio_ttm"] == 0.1
+
+
+def test_compute_ttm_ratios_guardrails_suppress_outliers(monkeypatch) -> None:
+ clear_service_caches()
+ monkeypatch.setattr(data_service, "get_company_info", lambda symbol: {})
+ monkeypatch.setattr(data_service, "get_fast_info", lambda symbol: {})
+ monkeypatch.setattr(data_service, "get_latest_price", lambda symbol: 1_000.0)
+ monkeypatch.setattr(data_service, "get_shares_outstanding", lambda symbol: 100.0)
+ monkeypatch.setattr(
+ data_service,
+ "get_income_statement",
+ lambda symbol, quarterly=False: quarterly_frame(
+ {
+ "Total Revenue": [1.0, 1.0, 1.0, 1.0],
+ "Net Income": [1.0, 1.0, 1.0, 1.0],
+ "EBIT": [1.0, 1.0, 1.0, 1.0],
+ "EBITDA": [100_000.0, 100_000.0, 100_000.0, 100_000.0],
+ "Tax Provision": [0.0, 0.0, 0.0, 0.0],
+ "Pretax Income": [1.0, 1.0, 1.0, 1.0],
+ }
+ ),
+ )
+ monkeypatch.setattr(
+ data_service,
+ "get_balance_sheet",
+ lambda symbol, quarterly=False: quarterly_frame(
+ {
+ "Stockholders Equity": [1.0, 0.0, 0.0, 0.0],
+ "Total Assets": [10.0, 0.0, 0.0, 0.0],
+ "Total Debt": [1_000.0, 0.0, 0.0, 0.0],
+ "Cash And Cash Equivalents": [0.0, 0.0, 0.0, 0.0],
+ }
+ ),
+ )
+ monkeypatch.setattr(data_service, "get_cash_flow", lambda symbol, quarterly=False: pd.DataFrame())
+
+ ratios = data_service.compute_ttm_ratios("AAPL")
+ assert ratios["trailing_pe"] == 25_000.0
+ assert "price_to_book" not in ratios
+ assert "price_to_sales" not in ratios
+ assert "ev_to_sales" not in ratios
+ assert "ev_to_ebitda" not in ratios
+
+
+def test_overview_uses_computed_sources_and_ratios(monkeypatch) -> None:
+ clear_service_caches()
+ monkeypatch.setattr(
+ data_service,
+ "get_company_info",
+ lambda symbol: {
+ "longName": "Apple Inc.",
+ "exchange": "NMS",
+ "currentPrice": 120.0,
+ "previousClose": 118.0,
+ },
+ )
+ monkeypatch.setattr(data_service, "get_fast_info", lambda symbol: {})
+ monkeypatch.setattr(data_service, "get_price_history", lambda symbol, period="1m": [])
+ monkeypatch.setattr(data_service, "_pick_search_match", lambda symbol: {"symbol": "AAPL", "name": "Apple Inc.", "exchange": "NASDAQ"})
+ monkeypatch.setattr(data_service, "get_profile_enrichment", lambda symbol: {})
+ monkeypatch.setattr(
+ data_service,
+ "compute_ttm_ratios",
+ lambda symbol: {
+ "market_cap": 1_500_000_000.0,
+ "trailing_pe": 24.5,
+ "trailing_eps": 4.9,
+ "price_to_book": 8.0,
+ "price_to_sales": 6.2,
+ "ev_to_sales": 6.8,
+ "ev_to_ebitda": 22.1,
+ "gross_margin_ttm": 0.44,
+ "operating_margin_ttm": 0.27,
+ "net_margin_ttm": 0.18,
+ "roe_ttm": 0.31,
+ "roa_ttm": 0.12,
+ "roic_ttm": 0.19,
+ "debt_to_equity": 0.42,
+ "current_ratio": 1.8,
+ "dividend_yield_ttm": 0.005,
+ "dividend_payout_ratio_ttm": 0.12,
+ },
+ )
+
+ overview = data_service.get_ticker_overview("AAPL")
+ assert overview is not None
+ assert overview["stats"]["trailing_pe"] == 24.5
+ assert overview["stats"]["market_cap"] == 1_500_000_000.0
+ assert overview["ratios"]["price_to_book"] == 8.0
+ assert overview["meta"]["sources"]["stats.trailing_pe"] == "computed"
+ assert overview["meta"]["sources"]["stats.market_cap"] == "computed"
+ 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"])