feat: replace Trading Economics with FMP for upcoming calendar forecasts

TE costs $199/month and its Economic Calendar is not in the free tier.
FMP (Financial Modeling Prep) offers a free API key (250 req/day) with
full economic calendar coverage including consensus estimates (forecasts).

- Add backend/services/fmp_calendar.py: fetches upcoming events from
  GET /api/v3/economic_calendar (one request, all countries, date range)
- Replace /api/eco/te-key + te-sync endpoints with fmp-key + fmp-sync
- Update daily background sync in main.py to use fmp_calendar
- Replace TEPanel with FMPPanel in CalendarPage.tsx (link to FMP docs)
- Remove broken Cloudflare-blocked FF HTML scrape button from ImportPanel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-26 17:42:09 +02:00
parent ec6fcc1e3d
commit 5c86d6e715
4 changed files with 199 additions and 74 deletions

View File

@@ -420,52 +420,52 @@ def ff_scrape_status_ep() -> Dict[str, Any]:
return _ff_scrape_status
# ── Trading Economics calendar (upcoming weeks with forecasts) ─────────────────
# ── FMP calendar (upcoming weeks with consensus 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.get("/fmp-key")
def get_fmp_key() -> Dict[str, Any]:
from services.fmp_calendar import check_fmp_key
return check_fmp_key()
@router.post("/te-key")
def save_te_key(key: str = Query(..., description="Trading Economics API key")) -> Dict[str, Any]:
@router.post("/fmp-key")
def save_fmp_key(key: str = Query(..., description="FMP 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)
set_config("fmp_api_key", key)
return {"status": "saved", "preview": key[:4] + "" + key[-4:]}
def _run_te_sync(weeks_ahead: int):
def _run_fmp_sync(weeks_ahead: int):
global _te_sync_status
_te_sync_status["running"] = True
try:
from services.te_calendar import fetch_upcoming
from services.fmp_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}")
logger.error(f"[eco/fmp-sync] Failed: {e}")
_te_sync_status["last_result"] = {"error": str(e)}
finally:
_te_sync_status["running"] = False
@router.post("/te-sync")
def te_sync(
@router.post("/fmp-sync")
def fmp_sync(
background_tasks: BackgroundTasks,
weeks: int = Query(6, ge=1, le=12),
) -> Dict[str, Any]:
"""Fetch upcoming economic events + forecasts from Trading Economics API."""
"""Fetch upcoming economic events + forecasts from FMP API (free, 250 req/day)."""
if _te_sync_status["running"]:
raise HTTPException(409, "TE sync already running")
background_tasks.add_task(_run_te_sync, weeks)
raise HTTPException(409, "FMP sync already running")
background_tasks.add_task(_run_fmp_sync, weeks)
return {"status": "started", "weeks_ahead": weeks}
@router.get("/te-sync/status")
def te_sync_status_ep() -> Dict[str, Any]:
@router.get("/fmp-sync/status")
def fmp_sync_status_ep() -> Dict[str, Any]:
return _te_sync_status