From 02828fa1ab136592cd0d8df77e153e9544c2cc51 Mon Sep 17 00:00:00 2001 From: Openclaw Date: Sun, 29 Mar 2026 01:42:02 -0700 Subject: Migrate FMP ratios and improve comps fallbacks --- services/fmp_service.py | 81 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 59 insertions(+), 22 deletions(-) (limited to 'services/fmp_service.py') diff --git a/services/fmp_service.py b/services/fmp_service.py index bf31788..59d8215 100644 --- a/services/fmp_service.py +++ b/services/fmp_service.py @@ -1,65 +1,102 @@ -"""Financial Modeling Prep API — ratios, peers, company news.""" +"""Financial Modeling Prep API — stable ratios/metrics + company news.""" import os import requests import streamlit as st from dotenv import load_dotenv +from services.data_service import get_company_info load_dotenv() -BASE_URL = "https://financialmodelingprep.com/api/v3" +BASE_URL = "https://financialmodelingprep.com" +STABLE_BASE = f"{BASE_URL}/stable" +LEGACY_BASE = f"{BASE_URL}/api/v3" def _api_key() -> str: - key = os.getenv("FMP_API_KEY", "") - return key + return os.getenv("FMP_API_KEY", "") -def _get(endpoint: str, params: dict = None) -> dict | list | None: +def _get(base_url: str, endpoint: str, params: dict | None = None) -> dict | list | None: params = params or {} params["apikey"] = _api_key() try: - resp = requests.get(f"{BASE_URL}{endpoint}", params=params, timeout=10) + resp = requests.get(f"{base_url}{endpoint}", params=params, timeout=10) resp.raise_for_status() return resp.json() except Exception: return None +def _apply_yfinance_ratio_fallbacks(ticker: str, merged: dict) -> dict: + info = get_company_info(ticker) + if not info: + return merged + + fallback_map = { + "peRatioTTM": info.get("trailingPE"), + "priceToSalesRatioTTM": info.get("priceToSalesTrailing12Months"), + "priceToBookRatioTTM": info.get("priceToBook"), + "enterpriseValueMultipleTTM": info.get("enterpriseToEbitda"), + "evToEBITDATTM": info.get("enterpriseToEbitda"), + "evToSalesTTM": info.get("enterpriseToRevenue"), + "grossProfitMarginTTM": info.get("grossMargins"), + "operatingProfitMarginTTM": info.get("operatingMargins"), + "netProfitMarginTTM": info.get("profitMargins"), + "returnOnEquityTTM": info.get("returnOnEquity"), + "returnOnAssetsTTM": info.get("returnOnAssets"), + "debtToEquityRatioTTM": info.get("debtToEquity"), + "currentRatioTTM": info.get("currentRatio"), + "quickRatioTTM": info.get("quickRatio"), + "dividendYieldTTM": info.get("dividendYield"), + "dividendPayoutRatioTTM": info.get("payoutRatio"), + } + + for key, value in fallback_map.items(): + if merged.get(key) is None and value is not None: + merged[key] = value + + return merged + + @st.cache_data(ttl=3600) def get_key_ratios(ticker: str) -> dict: - """Return latest TTM key ratios.""" - data = _get(f"/ratios-ttm/{ticker.upper()}") - if data and isinstance(data, list) and len(data) > 0: - return data[0] - return {} + """Return merged stable TTM ratios + key metrics for a ticker, with yfinance fallbacks.""" + ticker = ticker.upper() + ratios = _get(STABLE_BASE, "/ratios-ttm", params={"symbol": ticker}) + metrics = _get(STABLE_BASE, "/key-metrics-ttm", params={"symbol": ticker}) + + merged = {"symbol": ticker} + if isinstance(ratios, list) and ratios: + merged.update(ratios[0]) + if isinstance(metrics, list) and metrics: + merged.update(metrics[0]) + + merged = _apply_yfinance_ratio_fallbacks(ticker, merged) + return merged if len(merged) > 1 else {} @st.cache_data(ttl=21600) def get_peers(ticker: str) -> list[str]: - """Return list of comparable ticker symbols.""" - data = _get(f"/stock_peers", params={"symbol": ticker.upper()}) - if data and isinstance(data, list) and len(data) > 0: - return data[0].get("peersList", []) + """Direct FMP peers endpoint was deprecated; peer discovery is handled in UI fallback logic.""" return [] @st.cache_data(ttl=3600) def get_ratios_for_tickers(tickers: list[str]) -> list[dict]: - """Return TTM ratios for a list of tickers (for comps table).""" + """Return merged TTM ratios/metrics rows for a list of tickers.""" results = [] for t in tickers: - data = _get(f"/ratios-ttm/{t}") - if data and isinstance(data, list) and len(data) > 0: - row = data[0] - row["symbol"] = t + row = get_key_ratios(t) + if row: + row["symbol"] = t.upper() results.append(row) return results @st.cache_data(ttl=600) def get_company_news(ticker: str, limit: int = 20) -> list[dict]: - """Return recent news articles for a ticker.""" - data = _get("/stock_news", params={"tickers": ticker.upper(), "limit": limit}) + """Return recent news articles for a ticker via legacy endpoint fallback.""" + data = _get(LEGACY_BASE, "/stock_news", params={"tickers": ticker.upper(), "limit": limit}) if data and isinstance(data, list): return data return [] -- cgit v1.3-2-g0d8e