Files
OpenFin/backend/services/calendar_sync.py
2026-07-21 11:14:11 +02:00

92 lines
3.2 KiB
Python

"""
Unified calendar sync.
1. FF live JSON (nfs.faireconomy.media) → this week + next week (actual values, precise)
2. FF HTML scraper → weeks 3..N ahead (upcoming forecasts)
3. FMP API fallback → weeks 3..N ahead, only if step 2 got blocked (ForexFactory now
403s the scraper — likely Cloudflare) AND an FMP key is configured (services.fmp_calendar)
Called by the background loop in main.py and manually via POST /api/eco/calendar-sync.
"""
import logging
from datetime import datetime, timezone
from typing import Any, Dict
logger = logging.getLogger(__name__)
_sync_status: Dict[str, Any] = {
"running": False,
"last_run": None,
"last_result": None,
"source": None,
"next_run": None,
}
def calendar_sync(weeks_ahead: int = 8) -> Dict[str, Any]:
"""
Sync upcoming economic events from ForexFactory only:
- FF live JSON covers this week + next week (with actuals as they publish)
- FF HTML scraper covers weeks 3..weeks_ahead for forecasts
"""
global _sync_status
_sync_status["running"] = True
_sync_status["last_run"] = datetime.now(timezone.utc).isoformat()
result: Dict[str, Any] = {}
source = "ff"
try:
from services.ff_calendar import sync_live, scrape_upcoming
# Step 1 — live JSON (thisweek + nextweek) — picks up actuals in real time
r_live = sync_live()
result["live"] = r_live
# Step 2 — HTML scraper for weeks 3..N ahead (forecasts only, no actuals yet)
scrape_weeks = max(0, weeks_ahead - 2)
if scrape_weeks > 0:
r_scrape = scrape_upcoming(weeks_ahead=scrape_weeks)
result["scrape"] = r_scrape
# FF's scraper gets 403'd (Cloudflare) often enough that "weeks 3..N" can
# silently come back empty — fall back to FMP for that same range when it does.
if not r_scrape.get("total_upserted"):
from services.fmp_calendar import check_fmp_key, fetch_upcoming
if check_fmp_key()["configured"]:
r_fmp = fetch_upcoming(weeks_ahead=weeks_ahead)
result["fmp_fallback"] = r_fmp
if r_fmp.get("total_upserted"):
source = "ff+fmp"
else:
result["fmp_fallback"] = {"skipped": "no_key"}
else:
result["scrape"] = {"skipped": True}
except Exception as e:
logger.error(f"[calendar_sync] FF sync failed: {e}")
result = {"error": str(e)}
source = "error"
# Log forecast/actual changes to macro_series_log
try:
from services.database import get_conn
from services.macro_series_log import scan_and_log_all
_conn = get_conn()
log_result = scan_and_log_all(_conn, source=source)
_conn.close()
result["_log"] = log_result
except Exception as e:
logger.warning(f"[calendar_sync] macro_series_log scan failed: {e}")
_sync_status["running"] = False
_sync_status["last_result"] = result
_sync_status["source"] = source
return result
def get_sync_status() -> Dict[str, Any]:
return dict(_sync_status)
def set_next_run(ts: str):
_sync_status["next_run"] = ts