diff options
| -rw-r--r-- | backend/app/services/data_service.py | 61 | ||||
| -rw-r--r-- | backend/tests/test_api.py | 43 |
2 files changed, 81 insertions, 23 deletions
diff --git a/backend/app/services/data_service.py b/backend/app/services/data_service.py index ab06723..f913ec5 100644 --- a/backend/app/services/data_service.py +++ b/backend/app/services/data_service.py @@ -830,8 +830,6 @@ def compute_sector_ratio_benchmarks(symbol: str) -> dict[str, float]: """Median TTM ratio benchmarks from same-sector peers (FMP-backed when available).""" sym = normalize_symbol(symbol) fmp_key = os.getenv("FMP_API_KEY") - if not fmp_key: - return {} info = get_company_info(sym) sector_raw = info.get("sector") if isinstance(info, dict) else None @@ -843,27 +841,44 @@ def compute_sector_ratio_benchmarks(symbol: str) -> dict[str, float]: return {} peer_symbols: list[str] = [] - try: - with httpx.Client(timeout=3.5) as client: - res = client.get( - "https://financialmodelingprep.com/api/v3/stock-screener", - params={ - "sector": sector, - "isEtf": "false", - "isActivelyTrading": "true", - "limit": 12, - "apikey": fmp_key, - }, - ) - rows = res.json() - if isinstance(rows, list): - for row in rows: - psym = normalize_symbol((row or {}).get("symbol")) - if not psym or psym == sym: - continue - peer_symbols.append(psym) - except Exception: - return {} + if fmp_key: + try: + with httpx.Client(timeout=3.5) as client: + res = client.get( + "https://financialmodelingprep.com/api/v3/stock-screener", + params={ + "sector": sector, + "isEtf": "false", + "isActivelyTrading": "true", + "limit": 12, + "apikey": fmp_key, + }, + ) + rows = res.json() + if isinstance(rows, list): + for row in rows: + psym = normalize_symbol((row or {}).get("symbol")) + if not psym or psym == sym: + continue + peer_symbols.append(psym) + except Exception: + peer_symbols = [] + + # No-key or FMP failure fallback: search by sector term, then filter by exact sector. + if not peer_symbols: + try: + candidates = search_tickers(sector) + except Exception: + candidates = [] + target_sector = sector.lower() + for row in candidates[:24]: + psym = normalize_symbol((row or {}).get("symbol")) + if not psym or psym == sym: + continue + pinfo = get_company_info(psym) + psector = str((pinfo or {}).get("sector") or "").strip().lower() + if psector and psector == target_sector: + peer_symbols.append(psym) if not peer_symbols: return {} diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 300069c..345c0a3 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -1212,6 +1212,49 @@ def test_get_ratios_sector_benchmark_fields(monkeypatch) -> None: assert result["dividend_yield"]["vs_sector"] == pytest.approx(0.012) +def test_compute_sector_ratio_benchmarks_without_fmp_key(monkeypatch) -> None: + clear_service_caches() + monkeypatch.delenv("FMP_API_KEY", raising=False) + monkeypatch.setattr( + data_service, + "get_company_info", + lambda symbol: ( + {"sector": "Technology"} + if symbol == "AAPL" + else {"sector": "Technology"} + if symbol == "MSFT" + else {"sector": "Technology"} + if symbol == "NVDA" + else {"sector": "Healthcare"} + ), + ) + monkeypatch.setattr( + data_service, + "search_tickers", + lambda query: [ + {"symbol": "MSFT", "name": "Microsoft", "exchange": "NASDAQ"}, + {"symbol": "NVDA", "name": "NVIDIA", "exchange": "NASDAQ"}, + {"symbol": "UNH", "name": "UnitedHealth", "exchange": "NYSE"}, + ], + ) + monkeypatch.setattr( + data_service, + "compute_ttm_ratios", + lambda symbol: ( + {"trailing_pe": 30.0, "current_ratio": 2.0} + if symbol == "MSFT" + else {"trailing_pe": 20.0, "current_ratio": 1.5} + if symbol == "NVDA" + else {"trailing_pe": 10.0, "current_ratio": 1.0} + ), + ) + + result = data_service.compute_sector_ratio_benchmarks("AAPL") + + assert result["trailing_pe"] == pytest.approx(25.0) + assert result["current_ratio"] == pytest.approx(1.75) + + def test_ticker_ratios_route(monkeypatch) -> None: """GET /api/tickers/{symbol}/ratios returns a valid RatiosResponse shape.""" clear_service_caches() |
