Files
openbb-invest-api/macro_service.py
Yaojia Wang f5b22deec3
All checks were successful
continuous-integration/drone/push Build is passing
fix: resolve curl_cffi TLS errors and fix FRED/upgrades endpoints
- Pin curl_cffi==0.7.4 to avoid BoringSSL bug in 0.12-0.14
- Patch curl_cffi Session to use safari TLS fingerprint instead of
  chrome, which triggers SSL_ERROR_SYSCALL on some networks
- Register FRED API key with OpenBB credentials at startup
- Fix macro overview to return latest data instead of oldest, and
  extract values by FRED series ID key
- Replace Finnhub upgrades endpoint (premium-only) with yfinance
  upgrades_downgrades which includes price target changes
- Remove redundant curl_cffi upgrade from Dockerfile
2026-03-19 15:40:41 +01:00

95 lines
2.7 KiB
Python

"""Macro economic data via OpenBB FRED provider."""
import asyncio
import logging
from typing import Any
from openbb import obb
logger = logging.getLogger(__name__)
PROVIDER = "fred"
# Key FRED series IDs
SERIES = {
"fed_funds_rate": "FEDFUNDS",
"us_10y_treasury": "DGS10",
"us_2y_treasury": "DGS2",
"cpi_yoy": "CPIAUCSL",
"unemployment_rate": "UNRATE",
"gdp_growth": "A191RL1Q225SBEA",
"sp500": "SP500",
"vix": "VIXCLS",
}
def _to_dicts(result: Any) -> list[dict[str, Any]]:
if result is None or result.results is None:
return []
if isinstance(result.results, list):
return [
item.model_dump() if hasattr(item, "model_dump") else vars(item)
for item in result.results
]
if hasattr(result.results, "model_dump"):
return [result.results.model_dump()]
return [vars(result.results)]
async def get_series(
series_id: str, limit: int = 10, latest: bool = False,
) -> list[dict[str, Any]]:
"""Get a FRED time series by ID."""
try:
fetch_limit = limit if not latest else None
kwargs: dict[str, Any] = {
"symbol": series_id,
"provider": PROVIDER,
}
if fetch_limit is not None:
kwargs["limit"] = fetch_limit
result = await asyncio.to_thread(
obb.economy.fred_series,
**kwargs,
)
items = _to_dicts(result)
items = [
{**item, "date": str(item["date"])}
if "date" in item and not isinstance(item["date"], str)
else item
for item in items
]
if latest:
items = items[-limit:]
return items
except Exception:
logger.warning("Failed to fetch FRED series %s", series_id, exc_info=True)
return []
async def get_macro_overview() -> dict[str, Any]:
"""Get a summary of key macro indicators."""
tasks = {
name: get_series(series_id, limit=1, latest=True)
for name, series_id in SERIES.items()
}
results = await asyncio.gather(*tasks.values(), return_exceptions=True)
overview: dict[str, Any] = {}
for (name, series_id), result in zip(SERIES.items(), results):
if isinstance(result, BaseException):
logger.warning("Failed to fetch %s: %s", name, result)
overview[name] = None
elif result and len(result) > 0:
entry = result[-1]
# FRED returns values keyed by series ID, not "value"
value = entry.get(series_id) or entry.get("value")
overview[name] = {
"value": value,
"date": str(entry.get("date", "")),
}
else:
overview[name] = None
return overview