""" Economic Calendar (Financial Modeling Prep, free tier). Fetches upcoming high-impact macro events (FOMC, CPI, NFP, GDP, rate decisions...) that move risk assets including crypto. Needs a free API key: FMP_API_KEY=... (free at https://site.financialmodelingprep.com) Like the NewsData collector, this skips gracefully when the key is absent, so the app runs fine without it — the Economic Calendar / Market Impact Forecast panels simply show "add FMP_API_KEY". """ import os import logging import requests from datetime import datetime, timezone, timedelta logger = logging.getLogger(__name__) _FMP_URL = "https://financialmodelingprep.com/api/v3/economic_calendar" # Countries whose data actually moves crypto (US dominates) _COUNTRIES = {"US", "EA", "GB", "CN"} # Only events that matter — match on keywords (FMP impact field is unreliable) _HIGH_IMPACT_KEYS = [ "fomc", "fed ", "federal funds", "interest rate", "rate decision", "cpi", "core cpi", "inflation", "ppi", "nonfarm", "non-farm", "nfp", "unemployment", "jobless", "gdp", "pce", "powell", "fed chair", "retail sales", "ism", "treasury", "consumer price", ] def fmp_configured() -> bool: return bool(os.getenv("FMP_API_KEY")) def _impact_rank(ev: dict) -> int: name = (ev.get("event") or "").lower() imp = (ev.get("impact") or "").lower() if any(k in name for k in ("fomc", "rate decision", "interest rate", "cpi", "nonfarm", "nfp", "powell")): return 3 if imp == "high" or any(k in name for k in _HIGH_IMPACT_KEYS): return 2 if imp == "medium": return 1 return 0 class EconomicCalendar: def fetch(self, days_ahead: int = 7) -> list[dict]: from monitoring import monitor if not fmp_configured(): return [] now = datetime.now(timezone.utc) params = { "from": now.strftime("%Y-%m-%d"), "to": (now + timedelta(days=days_ahead)).strftime("%Y-%m-%d"), "apikey": os.getenv("FMP_API_KEY"), } try: resp = requests.get(_FMP_URL, params=params, timeout=15) resp.raise_for_status() data = resp.json() monitor.source_ok("FMP:calendar", len(data) if isinstance(data, list) else 0) except Exception as exc: monitor.source_failure("FMP:calendar", str(exc)[:120]) return [] if not isinstance(data, list): return [] events = [] for ev in data: country = (ev.get("country") or "").upper() if country not in _COUNTRIES: continue rank = _impact_rank(ev) if rank < 2: # keep only high-impact continue dt = _parse_dt(ev.get("date")) if not dt or dt < now - timedelta(hours=2): continue # past events.append({ "event": ev.get("event", ""), "country": country, "datetime": dt.isoformat(), "hours_until": round((dt - now).total_seconds() / 3600, 1), "impact": "HIGH" if rank >= 3 else "MED", "estimate": ev.get("estimate"), "previous": ev.get("previous"), }) events.sort(key=lambda e: e["datetime"]) return events[:12] def _parse_dt(s: str): if not s: return None for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d"): try: return datetime.strptime(s, fmt).replace(tzinfo=timezone.utc) except Exception: continue return None