Calendar synchro
This commit is contained in:
@@ -106,41 +106,28 @@ def startup():
|
||||
from services.institutional_scheduler import start_institutional_scheduler
|
||||
start_institutional_scheduler()
|
||||
|
||||
# Daily calendars sync — FF live (this week) + TE upcoming (if key configured)
|
||||
# Calendar sync loop — FXStreet primary, FF fallback; interval from calendar_refresh_h config (default 6h)
|
||||
import threading, time as _time
|
||||
def _daily_calendar_sync():
|
||||
_time.sleep(60) # let server fully boot first
|
||||
def _calendar_sync_loop():
|
||||
_time.sleep(60) # let server fully boot
|
||||
while True:
|
||||
try:
|
||||
# FF live JSON — this week actuals
|
||||
from services.ff_calendar import sync_live
|
||||
r = sync_live()
|
||||
print(f"[Daily] FF live sync: {r}", flush=True)
|
||||
from datetime import datetime as _dt, timezone as _tz, timedelta as _td
|
||||
interval_h = float(get_config("calendar_refresh_h") or 6)
|
||||
from services.calendar_sync import calendar_sync, set_next_run
|
||||
set_next_run((_dt.now(_tz.utc) + _td(hours=interval_h)).isoformat())
|
||||
r = calendar_sync()
|
||||
src = r.get("source") or ("ff_fallback" if r.get("_fallback") else "fxstreet")
|
||||
print(f"[Calendar] Sync done — {src}, upserted={r.get('total_upserted', '?')}", flush=True)
|
||||
except Exception as e:
|
||||
print(f"[Daily] FF live sync error: {e}", flush=True)
|
||||
|
||||
print(f"[Calendar] Sync loop error: {e}", flush=True)
|
||||
try:
|
||||
# FXStreet — upcoming forecasts (no API key needed)
|
||||
from services.fxstreet_calendar import fetch_upcoming as fxs_fetch
|
||||
r = fxs_fetch(weeks_ahead=6)
|
||||
print(f"[Daily] FXStreet sync: {r.get('total_upserted', 0)} upserted", flush=True)
|
||||
except Exception as e:
|
||||
print(f"[Daily] FXStreet sync error: {e}", flush=True)
|
||||
interval_h = float(get_config("calendar_refresh_h") or 6)
|
||||
except Exception:
|
||||
interval_h = 6
|
||||
_time.sleep(int(interval_h * 3600))
|
||||
|
||||
try:
|
||||
# FMP — upcoming forecasts (only if key is set + Starter plan)
|
||||
from services.database import get_config
|
||||
fmp_key = get_config("fmp_api_key") or ""
|
||||
if fmp_key:
|
||||
from services.fmp_calendar import fetch_upcoming
|
||||
r = fetch_upcoming(weeks_ahead=6)
|
||||
print(f"[Daily] FMP sync: {r.get('total_upserted', 0)} upserted", flush=True)
|
||||
except Exception as e:
|
||||
print(f"[Daily] FMP sync error: {e}", flush=True)
|
||||
|
||||
_time.sleep(86400) # repeat every 24h
|
||||
|
||||
threading.Thread(target=_daily_calendar_sync, daemon=True).start()
|
||||
threading.Thread(target=_calendar_sync_loop, daemon=True).start()
|
||||
|
||||
# Auto-import Forex Factory CSV if file is present (force, then delete CSV)
|
||||
import threading
|
||||
|
||||
@@ -12,6 +12,7 @@ class ApiKeysRequest(BaseModel):
|
||||
newsapi_key: Optional[str] = None
|
||||
eia_api_key: Optional[str] = None
|
||||
fred_api_key: Optional[str] = None
|
||||
calendar_refresh_h: Optional[str] = None
|
||||
|
||||
|
||||
class SourcesRequest(BaseModel):
|
||||
@@ -61,6 +62,9 @@ def update_api_keys(req: ApiKeysRequest):
|
||||
if req.fred_api_key is not None:
|
||||
set_config("fred_api_key", req.fred_api_key)
|
||||
updated.append("fred")
|
||||
if req.calendar_refresh_h is not None:
|
||||
set_config("calendar_refresh_h", req.calendar_refresh_h)
|
||||
updated.append("calendar_refresh_h")
|
||||
return {"status": "ok", "updated": updated}
|
||||
|
||||
|
||||
|
||||
@@ -570,6 +570,36 @@ def fxs_sync_status_ep() -> Dict[str, Any]:
|
||||
return _fxs_sync_status
|
||||
|
||||
|
||||
# ── Unified calendar sync ─────────────────────────────────────────────────────
|
||||
|
||||
def _run_calendar_sync(weeks_ahead: int):
|
||||
from services.calendar_sync import calendar_sync
|
||||
try:
|
||||
calendar_sync(weeks_ahead=weeks_ahead)
|
||||
except Exception as e:
|
||||
logger.error(f"[eco/calendar-sync] Failed: {e}")
|
||||
|
||||
|
||||
@router.post("/calendar-sync")
|
||||
def calendar_sync_ep(
|
||||
background_tasks: BackgroundTasks,
|
||||
weeks: int = Query(8, ge=1, le=12),
|
||||
) -> Dict[str, Any]:
|
||||
"""Trigger unified calendar sync (FXStreet primary → FF fallback) in background."""
|
||||
from services.calendar_sync import get_sync_status
|
||||
if get_sync_status()["running"]:
|
||||
raise HTTPException(409, "Calendar sync already running")
|
||||
background_tasks.add_task(_run_calendar_sync, weeks)
|
||||
return {"status": "started", "weeks_ahead": weeks}
|
||||
|
||||
|
||||
@router.get("/calendar-sync/status")
|
||||
def calendar_sync_status_ep() -> Dict[str, Any]:
|
||||
"""Get unified calendar sync status (last_run, source, last_result, next_run)."""
|
||||
from services.calendar_sync import get_sync_status
|
||||
return get_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"),
|
||||
|
||||
61
backend/services/calendar_sync.py
Normal file
61
backend/services/calendar_sync.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
Unified calendar sync — FXStreet (primary) with FF live fallback.
|
||||
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:
|
||||
1. Try FXStreet (no API key, ~300-400 events over weeks_ahead weeks)
|
||||
2. Fall back to FF live JSON if FXStreet returns 403 or fails
|
||||
"""
|
||||
global _sync_status
|
||||
_sync_status["running"] = True
|
||||
_sync_status["last_run"] = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
result: Dict[str, Any] = {}
|
||||
source = "unknown"
|
||||
|
||||
try:
|
||||
from services.fxstreet_calendar import fetch_upcoming as fxs_fetch
|
||||
r = fxs_fetch(weeks_ahead=weeks_ahead)
|
||||
if r.get("error"):
|
||||
logger.warning(f"[calendar_sync] FXStreet error: {r['error']} — falling back to FF live")
|
||||
from services.ff_calendar import sync_live
|
||||
r_ff = sync_live()
|
||||
result = {**r_ff, "_fallback": True, "_fxs_error": r["error"]}
|
||||
source = "ff_fallback"
|
||||
else:
|
||||
result = r
|
||||
source = "fxstreet"
|
||||
except Exception as e:
|
||||
logger.error(f"[calendar_sync] Exception: {e}")
|
||||
result = {"error": str(e)}
|
||||
source = "error"
|
||||
|
||||
_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
|
||||
Reference in New Issue
Block a user