feat: Trading Economics API for upcoming events with forecasts

- New services/te_calendar.py: fetch_upcoming(weeks_ahead) calls TE API
  for 9 countries (USD/EUR/GBP/JPY/AUD/CAD/NZD/CHF/CNY), converts to
  ff_calendar format, upserts with source='te_api'
- New endpoints: GET/POST /api/eco/te-key, POST /api/eco/te-sync,
  GET /api/eco/te-sync/status
- Daily scheduler in main.py: FF live sync + TE sync (if key configured)
  run 60s after startup then every 24h
- CalendarPage: TEPanel with key input (password field, Enter to save,
  "Get free key" link to tradingeconomics.com/api/login),
  "Sync upcoming (6 weeks)" button with polling

FF HTML scraper kept as fallback but TE API is the primary source
for upcoming forecasts (no Cloudflare blocking on server IPs).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-26 17:30:16 +02:00
parent d9762deca7
commit ec6fcc1e3d
4 changed files with 357 additions and 12 deletions

View File

@@ -296,6 +296,7 @@ def upcoming_events() -> List[Dict[str, Any]]:
_ff_import_status: Dict[str, Any] = {"running": False, "last_result": None}
_ff_sync_status: Dict[str, Any] = {"running": False, "last_result": None}
_ff_scrape_status: Dict[str, Any] = {"running": False, "last_result": None}
_te_sync_status: Dict[str, Any] = {"running": False, "last_result": None}
# CSV search order: Docker image (/app), uploaded to /tmp, local dev paths
_FF_CSV_CANDIDATES = [
@@ -419,6 +420,55 @@ def ff_scrape_status_ep() -> Dict[str, Any]:
return _ff_scrape_status
# ── Trading Economics calendar (upcoming weeks with forecasts) ─────────────────
@router.get("/te-key")
def get_te_key() -> Dict[str, Any]:
from services.te_calendar import check_te_key
return check_te_key()
@router.post("/te-key")
def save_te_key(key: str = Query(..., description="Trading Economics API key")) -> Dict[str, Any]:
from services.database import set_config
key = key.strip()
if not key:
raise HTTPException(400, "Empty key")
set_config("te_api_key", key)
return {"status": "saved", "preview": key[:4] + "" + key[-4:]}
def _run_te_sync(weeks_ahead: int):
global _te_sync_status
_te_sync_status["running"] = True
try:
from services.te_calendar import fetch_upcoming
result = fetch_upcoming(weeks_ahead=weeks_ahead)
_te_sync_status["last_result"] = result
except Exception as e:
logger.error(f"[eco/te-sync] Failed: {e}")
_te_sync_status["last_result"] = {"error": str(e)}
finally:
_te_sync_status["running"] = False
@router.post("/te-sync")
def te_sync(
background_tasks: BackgroundTasks,
weeks: int = Query(6, ge=1, le=12),
) -> Dict[str, Any]:
"""Fetch upcoming economic events + forecasts from Trading Economics API."""
if _te_sync_status["running"]:
raise HTTPException(409, "TE sync already running")
background_tasks.add_task(_run_te_sync, weeks)
return {"status": "started", "weeks_ahead": weeks}
@router.get("/te-sync/status")
def te_sync_status_ep() -> Dict[str, Any]:
return _te_sync_status
@router.get("/calendar")
def ff_calendar(
period: str = Query("recent", description="recent|today|tomorrow|yesterday|this_week|next_week|previous_week|this_month|next_month|previous_month|custom"),