feat: Forex Factory calendar — unified past+future list style Trading Economics
- New ff_calendar table (event_date, time, currency, impact, actual, forecast, previous) - New service ff_calendar.py: bulk CSV import (83K events 2007-2025) + live sync from faireconomy.media JSON endpoint (this week / next week) - New API endpoints: POST /api/eco/ff-import, POST /api/eco/ff-sync, GET /api/eco/calendar (period filter), GET /api/eco/ff-stats - CalendarPage.tsx full rewrite: period tabs (Recent/Today/Tomorrow/This Week…), currency flags filter, impact filter, unified date-grouped table with Time·Flag·Currency·Impact·Event·Actual·Forecast·Previous columns, green/red actual vs forecast, TODAY badge, auto-refresh 60s Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
"""
|
||||
Economic calendar router — FRED-backed historical events + bootstrap endpoint.
|
||||
Economic calendar router — FRED-backed historical events + Forex Factory calendar.
|
||||
Prefix: /api/eco
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, HTTPException, Query
|
||||
@@ -290,6 +291,125 @@ def upcoming_events() -> List[Dict[str, Any]]:
|
||||
return upcoming
|
||||
|
||||
|
||||
# ── 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_CSV_PATH = os.path.join(
|
||||
os.path.dirname(__file__), "..", "..", "forex_factory_cache.csv"
|
||||
)
|
||||
|
||||
|
||||
def _run_ff_import():
|
||||
global _ff_import_status
|
||||
_ff_import_status["running"] = True
|
||||
try:
|
||||
from services.ff_calendar import import_csv
|
||||
csv_path = os.path.abspath(_FF_CSV_PATH)
|
||||
result = import_csv(csv_path)
|
||||
_ff_import_status["last_result"] = result
|
||||
except Exception as e:
|
||||
logger.error(f"[eco/ff-import] Failed: {e}")
|
||||
_ff_import_status["last_result"] = {"error": str(e)}
|
||||
finally:
|
||||
_ff_import_status["running"] = False
|
||||
|
||||
|
||||
def _run_ff_sync():
|
||||
global _ff_sync_status
|
||||
_ff_sync_status["running"] = True
|
||||
try:
|
||||
from services.ff_calendar import sync_live
|
||||
result = sync_live()
|
||||
_ff_sync_status["last_result"] = result
|
||||
except Exception as e:
|
||||
logger.error(f"[eco/ff-sync] Failed: {e}")
|
||||
_ff_sync_status["last_result"] = {"error": str(e)}
|
||||
finally:
|
||||
_ff_sync_status["running"] = False
|
||||
|
||||
|
||||
@router.post("/ff-import")
|
||||
def ff_import(background_tasks: BackgroundTasks) -> Dict[str, Any]:
|
||||
"""Import forex_factory_cache.csv into ff_calendar table (background)."""
|
||||
if _ff_import_status["running"]:
|
||||
raise HTTPException(409, "Import already running")
|
||||
csv_path = os.path.abspath(_FF_CSV_PATH)
|
||||
if not os.path.exists(csv_path):
|
||||
raise HTTPException(404, f"CSV not found: {csv_path}")
|
||||
background_tasks.add_task(_run_ff_import)
|
||||
return {"status": "started", "csv": csv_path}
|
||||
|
||||
|
||||
@router.get("/ff-import/status")
|
||||
def ff_import_status() -> Dict[str, Any]:
|
||||
return _ff_import_status
|
||||
|
||||
|
||||
@router.post("/ff-sync")
|
||||
def ff_sync(background_tasks: BackgroundTasks) -> Dict[str, Any]:
|
||||
"""Fetch this week + next week from faireconomy.media and upsert."""
|
||||
if _ff_sync_status["running"]:
|
||||
raise HTTPException(409, "Sync already running")
|
||||
background_tasks.add_task(_run_ff_sync)
|
||||
return {"status": "started"}
|
||||
|
||||
|
||||
@router.get("/ff-sync/status")
|
||||
def ff_sync_status_ep() -> Dict[str, Any]:
|
||||
return _ff_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"),
|
||||
currencies: Optional[str] = Query(None, description="Comma-separated: USD,EUR,GBP,..."),
|
||||
impacts: Optional[str] = Query(None, description="Comma-separated: high,medium,low"),
|
||||
limit: int = Query(300, ge=1, le=2000),
|
||||
offset: int = Query(0, ge=0),
|
||||
) -> Dict[str, Any]:
|
||||
"""Unified Forex Factory calendar — past releases + upcoming events."""
|
||||
from services.ff_calendar import get_calendar
|
||||
cur_list = [c.strip().upper() for c in currencies.split(",") if c.strip()] if currencies else None
|
||||
imp_list = [i.strip().lower() for i in impacts.split(",") if i.strip()] if impacts else None
|
||||
return get_calendar(
|
||||
period=period,
|
||||
currencies=cur_list,
|
||||
impacts=imp_list,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/ff-stats")
|
||||
def ff_stats() -> Dict[str, Any]:
|
||||
"""Quick inventory of ff_calendar table."""
|
||||
from services.database import get_conn
|
||||
conn = get_conn()
|
||||
try:
|
||||
total = conn.execute("SELECT COUNT(*) FROM ff_calendar").fetchone()[0]
|
||||
if total == 0:
|
||||
return {"total": 0}
|
||||
earliest = conn.execute("SELECT MIN(event_date) FROM ff_calendar").fetchone()[0]
|
||||
latest = conn.execute("SELECT MAX(event_date) FROM ff_calendar").fetchone()[0]
|
||||
by_currency = conn.execute(
|
||||
"SELECT currency, COUNT(*) AS cnt FROM ff_calendar GROUP BY currency ORDER BY cnt DESC"
|
||||
).fetchall()
|
||||
by_impact = conn.execute(
|
||||
"SELECT impact, COUNT(*) AS cnt FROM ff_calendar GROUP BY impact ORDER BY cnt DESC"
|
||||
).fetchall()
|
||||
return {
|
||||
"total": total,
|
||||
"earliest": earliest,
|
||||
"latest": latest,
|
||||
"by_currency": [dict(r) for r in by_currency],
|
||||
"by_impact": [dict(r) for r in by_impact],
|
||||
}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ── DB count summary ──────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/status")
|
||||
|
||||
Reference in New Issue
Block a user