"""Macro data: FRED, OECD (best effort), ECOS, economic calendar (FMP).""" import asyncio import os from functools import partial from typing import Any, Dict, Optional from fastapi import APIRouter, Query from server.services import fmp_client from server.services import macro_fetcher from server.services.global_macro_quadrant import get_global_macro_quadrant from server.services.smart_money_service import get_smart_money_snapshot from server.services.yield_fx_service import get_yield_fx_pair router = APIRouter() _MACRO_TIMEOUT_SECONDS = 8.0 async def _run_macro_task( func: Any, fallback: Dict[str, Any], *args: Any, timeout: float = _MACRO_TIMEOUT_SECONDS, ) -> Dict[str, Any]: try: call = partial(func, *args) return await asyncio.wait_for(asyncio.to_thread(call), timeout=timeout) except asyncio.TimeoutError: return {**fallback, "error": "upstream_timeout"} except Exception as exc: return {**fallback, "error": str(exc)} @router.get("/quadrant", summary="Global growth vs inflation quadrant (Z-scores)") async def macro_quadrant() -> Dict[str, Any]: return await _run_macro_task( get_global_macro_quadrant, {"updated_at": None, "points": []}, ) @router.get("/yield-fx", summary="US10Y spread vs FX (usdjpy|eurusd|usdkrw)") async def macro_yield_fx( pair: str = Query("usdjpy", description="usdjpy, eurusd, or usdkrw"), ) -> Dict[str, Any]: return await _run_macro_task( get_yield_fx_pair, { "pair": pair, "updated_at": None, "series": [], }, pair, ) @router.get("/smart-money", summary="Copper/Gold + RORO composite") async def macro_smart_money() -> Dict[str, Any]: return await _run_macro_task( get_smart_money_snapshot, { "updated_at": None, "roro_z": None, "roro_label": None, "copper_gold": [], "components": {}, }, ) @router.get("/subfactors", summary="4-category macro subfactor breakdown + cycle stage") async def macro_subfactors() -> Dict[str, Any]: from server.services.macro_cycle import get_subfactor_breakdown return await _run_macro_task( get_subfactor_breakdown, { "updated_at": None, "composite_score": 0.0, "cycle_stage": "Unknown", "categories": {}, }, ) @router.get("/fred/{series_id}", summary="FRED time series (public CSV)") async def macro_fred( series_id: str, start: Optional[str] = Query(None, description="YYYY-MM-DD"), end: Optional[str] = Query(None, description="YYYY-MM-DD"), ) -> Dict[str, Any]: try: rows = await asyncio.wait_for( asyncio.to_thread( macro_fetcher.fetch_fred_series, series_id, start, end, ), timeout=_MACRO_TIMEOUT_SECONDS, ) except asyncio.TimeoutError: rows = [] return {"series": series_id.strip().upper(), "count": 0, "data": rows, "error": "upstream_timeout"} return {"series": series_id.strip().upper(), "count": len(rows), "data": rows} @router.get("/oecd/mei", summary="OECD MEI (best effort)") async def macro_oecd_mei() -> Dict[str, Any]: rows = await asyncio.to_thread(macro_fetcher.fetch_oecd_mei) return {"count": len(rows), "data": rows} @router.get("/oecd/cli", summary="OECD CLI snapshot (DBnomics)") async def macro_oecd_cli() -> Dict[str, Any]: from server.services.oecd_cycle import get_oecd_cli_snapshot return await _run_macro_task( get_oecd_cli_snapshot, {"updated_at": None, "countries": []}, ) @router.get("/snapshot", summary="Macro cycle heatmap, countries, asset valuation") async def macro_snapshot() -> Dict[str, Any]: from server.services.macro_cycle import get_macro_cycle_snapshot return await _run_macro_task( get_macro_cycle_snapshot, { "updated_at": None, "cycle_score": 0.0, "regime": "Unknown", "cycle_heatmap": [], "country_heatmap": [], "asset_valuation": [], }, ) @router.get("/korea", summary="Korea indicators (ECOS or yfinance fallback)") async def macro_korea( api_key: Optional[str] = Query(None, description="ECOS API key (optional)"), ) -> Dict[str, Any]: from server.services.ecos_fetcher import get_korea_snapshot key = (api_key or os.getenv("ECOS_API_KEY") or "").strip() or None return await asyncio.to_thread(get_korea_snapshot, key) @router.get("/economic-calendar", summary="Economic calendar (scraped; no API key)") async def macro_economic_calendar( days: int = Query(7, ge=1, le=30), ) -> Dict[str, Any]: from server.services.economic_calendar import get_economic_calendar return await _run_macro_task( get_economic_calendar, {"events": [], "next_high_impact": None, "total": 0}, days, ) @router.get("/ecos", summary="Korea Bank ECOS (requires ECOS_API_KEY)") async def macro_ecos( stat_code: str = Query(..., description="ECOS statistics table code"), cycle: str = Query("M", description="D/W/M/Q/S/Y"), start_ym: str = Query("201501"), end_ym: Optional[str] = Query(None), ) -> Dict[str, Any]: rows = await asyncio.to_thread( macro_fetcher.fetch_ecos_series, stat_code, cycle, start_ym, end_ym, ) return {"stat_code": stat_code, "count": len(rows), "data": rows} @router.get("/calendar", summary="Economic calendar (FMP when key set)") async def macro_calendar( date_from: str = Query(..., description="YYYY-MM-DD"), date_to: str = Query(..., description="YYYY-MM-DD"), ) -> Dict[str, Any]: if not fmp_client.fmp_is_configured(): return { "source": "none", "data": [], "message": "Set FMP_API_KEY for economic_calendar, or use FRED directly.", } rows = await fmp_client.fetch_economic_calendar(date_from, date_to) if rows: return {"source": "fmp", "count": len(rows), "data": rows} return {"source": "fmp", "data": [], "message": "No rows (range or API limit)."}