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:
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user