feat: FXStreet calendar — free upcoming events with forecasts (no API key)

FXStreet calendar.fxstreet.com/eventdate/ returns ~300-400 events over
6 weeks including consensus forecasts, no authentication required.
FF HTML scraper is blocked by Cloudflare even on residential IPs.
FMP free plan returns 403 on /economic_calendar (requires Starter plan).

- Add backend/services/fxstreet_calendar.py: single GET request returning
  all major currencies; maps Volatility 0/1/2 → low/medium/high
- Add POST /api/eco/fxs-sync + GET /api/eco/fxs-sync/status endpoints
- Add FXStreet to daily background sync in main.py (runs every 24h)
- Add "Sync Upcoming (FXStreet)" button in ImportPanel (no key needed)
- Fix FMP 403 error message to say endpoint requires Starter plan
- Keep FMP panel for users who upgrade to FMP Starter ($14.99/month)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-26 17:54:15 +02:00
parent 5c86d6e715
commit bdbb87962d
5 changed files with 241 additions and 14 deletions

View File

@@ -293,10 +293,11 @@ def upcoming_events() -> List[Dict[str, Any]]:
# ── Forex Factory calendar ────────────────────────────────────────────────────
_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}
_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}
_fxs_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 = [
@@ -469,6 +470,39 @@ def fmp_sync_status_ep() -> Dict[str, Any]:
return _te_sync_status
# ── FXStreet calendar (no API key required) ───────────────────────────────────
def _run_fxs_sync(weeks_ahead: int):
global _fxs_sync_status
_fxs_sync_status["running"] = True
try:
from services.fxstreet_calendar import fetch_upcoming
result = fetch_upcoming(weeks_ahead=weeks_ahead)
_fxs_sync_status["last_result"] = result
except Exception as e:
logger.error(f"[eco/fxs-sync] Failed: {e}")
_fxs_sync_status["last_result"] = {"error": str(e)}
finally:
_fxs_sync_status["running"] = False
@router.post("/fxs-sync")
def fxs_sync(
background_tasks: BackgroundTasks,
weeks: int = Query(6, ge=1, le=12),
) -> Dict[str, Any]:
"""Fetch upcoming events + forecasts from FXStreet (no API key needed)."""
if _fxs_sync_status["running"]:
raise HTTPException(409, "FXStreet sync already running")
background_tasks.add_task(_run_fxs_sync, weeks)
return {"status": "started", "weeks_ahead": weeks}
@router.get("/fxs-sync/status")
def fxs_sync_status_ep() -> Dict[str, Any]:
return _fxs_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"),