""" Trading Economics calendar service — upcoming events with consensus forecasts. NOTE: confirmed (2026-07) that the calendar endpoint is NOT actually free despite the signup page — their base paid plan is $149/month. Kept wired in case a key is ever available, but don't recommend this as a free option (same dead end as FMP, whose free tier also excludes the economic_calendar endpoint — needs their $14.99/mo Starter). Signup: https://tradingeconomics.com/api/login Endpoint: GET https://api.tradingeconomics.com/calendar/country/united%20states?c=KEY """ import logging from datetime import datetime, timezone, timedelta, date from typing import Any, Dict, List, Optional import httpx logger = logging.getLogger(__name__) _TE_BASE = "https://api.tradingeconomics.com" # TE country names → our currency codes _TE_COUNTRY_TO_CCY: Dict[str, str] = { "united states": "USD", "euro area": "EUR", "united kingdom":"GBP", "japan": "JPY", "australia": "AUD", "canada": "CAD", "new zealand": "NZD", "switzerland": "CHF", "china": "CNY", } # TE importance (1=low, 2=medium, 3=high) → our impact labels _TE_IMPORTANCE = {1: "low", 2: "medium", 3: "high"} _COUNTRIES = list(_TE_COUNTRY_TO_CCY.keys()) def _get_te_key() -> str: from services.database import get_config return (get_config("te_api_key") or "").strip() def check_te_key() -> Dict[str, Any]: key = _get_te_key() return { "configured": bool(key), "preview": (key[:4] + "…" + key[-4:]) if len(key) > 8 else ("***" if key else ""), } def _parse_te_datetime(dt_str: str) -> tuple[str, str]: """Parse TE datetime '2026-07-04T12:30:00' (UTC) → (date, time).""" if not dt_str: return "", "00:00" try: # TE returns UTC datetimes dt = datetime.fromisoformat(dt_str.replace("Z", "+00:00")) return dt.strftime("%Y-%m-%d"), dt.strftime("%H:%M") except Exception: return dt_str[:10] if len(dt_str) >= 10 else "", "00:00" def fetch_upcoming(weeks_ahead: int = 6) -> Dict[str, Any]: """ Fetch upcoming calendar events from Trading Economics API and upsert into ff_calendar. Covers today → today + weeks_ahead weeks. """ from services.database import get_conn from services.ff_calendar import _upsert_batch, FF_TO_FRED, SUPPORTED_CURRENCIES key = _get_te_key() if not key: return {"error": "Trading Economics API key not configured. Register free at tradingeconomics.com/api/login"} today = date.today() date_to = today + timedelta(weeks=weeks_ahead) params = { "c": key, "d1": str(today), "d2": str(date_to), "importance": "1,2,3", # all impact levels } total_inserted = 0 conn = get_conn() for country in _COUNTRIES: ccy = _TE_COUNTRY_TO_CCY[country] try: url = f"{_TE_BASE}/calendar/country/{country.replace(' ', '%20')}" resp = httpx.get(url, params=params, timeout=20, follow_redirects=True) print(f"[TE calendar] {country}: HTTP {resp.status_code}", flush=True) if resp.status_code == 401: conn.close() return {"error": "Invalid Trading Economics API key"} if resp.status_code == 403: conn.close() return {"error": "TE API key quota exceeded or invalid plan"} resp.raise_for_status() events = resp.json() if not isinstance(events, list): continue batch = [] for ev in events: event_name = (ev.get("Event") or ev.get("event") or "").strip() if not event_name: continue importance = ev.get("Importance") or ev.get("importance") or 1 impact = _TE_IMPORTANCE.get(int(importance), "low") dt_str = ev.get("Date") or ev.get("date") or "" ev_date, ev_time = _parse_te_datetime(dt_str) if not ev_date: continue actual = _fmt_val(ev.get("Actual") or ev.get("actual")) forecast = _fmt_val(ev.get("Forecast") or ev.get("forecast")) previous = _fmt_val(ev.get("Previous") or ev.get("previous")) series_id = FF_TO_FRED.get(event_name) batch.append(( ev_date, ev_time, ccy, impact, event_name, actual or None, forecast or None, previous or None, series_id, None, "te_api", )) if batch: _upsert_batch(conn, batch) conn.commit() total_inserted += len(batch) print(f"[TE calendar] {country}: {len(batch)} events upserted", flush=True) except Exception as e: logger.warning(f"[TE calendar] {country}: {e}") print(f"[TE calendar] {country} error: {e}", flush=True) conn.close() return { "total_upserted": total_inserted, "date_from": str(today), "date_to": str(date_to), "countries": len(_COUNTRIES), } def _fmt_val(v: Any) -> str: """Convert TE numeric value to string, return empty if None/NaN.""" if v is None: return "" try: f = float(v) if f != f: # NaN check return "" # Format cleanly: no trailing .0 for integers return str(int(f)) if f == int(f) else str(round(f, 4)) except (TypeError, ValueError): return str(v).strip()