feat: import Trading Economics HTML calendar — 993 events with forecasts
Adds a one-click upload+parse flow for the TE calendar page HTML. The TE page (tradingeconomics.com/calendar) contains ~1000 events with actuals, previous, and analyst consensus (forecast) values. - Add backend/services/te_html_parser.py: - parse_html(html): extracts events from <tr data-event> rows - Maps 14 countries to major currencies (all Eurozone → EUR) - Impact inferred from data-category (high/medium/low) - Times converted from Europe/Zurich (CET/CEST) → UTC via zoneinfo - Actual=td[5], Forecast=td[7] (analyst consensus), Previous=td[6] - Add POST /api/eco/te-html-upload (saves file to /tmp) - Add POST /api/eco/te-html-import + GET /status (background parse) - Add "Import TE HTML" upload button in CalendarPage ImportPanel Tested locally: 993 events parsed, 854 with forecast, 861 with actual, date range 2025-03-31 → 2026-06-17, timezone conversion verified. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -293,11 +293,27 @@ 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}
|
||||
_fxs_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}
|
||||
_te_html_status: Dict[str, Any] = {"running": False, "last_result": None}
|
||||
|
||||
_TE_HTML_CANDIDATES = [
|
||||
"/app/calendar_data.txt",
|
||||
"/tmp/calendar_data.txt",
|
||||
os.path.join(os.path.dirname(__file__), "..", "..", "calendar_data.txt"),
|
||||
os.path.join(os.path.dirname(__file__), "..", "calendar_data.txt"),
|
||||
]
|
||||
|
||||
|
||||
def _find_te_html() -> str | None:
|
||||
for p in _TE_HTML_CANDIDATES:
|
||||
full = os.path.abspath(p)
|
||||
if os.path.exists(full):
|
||||
return full
|
||||
return None
|
||||
|
||||
# CSV search order: Docker image (/app), uploaded to /tmp, local dev paths
|
||||
_FF_CSV_CANDIDATES = [
|
||||
@@ -470,6 +486,57 @@ def fmp_sync_status_ep() -> Dict[str, Any]:
|
||||
return _te_sync_status
|
||||
|
||||
|
||||
# ── Trading Economics HTML import ─────────────────────────────────────────────
|
||||
|
||||
@router.post("/te-html-upload")
|
||||
async def te_html_upload(file: UploadFile = File(...)) -> Dict[str, Any]:
|
||||
"""Upload calendar_data.txt (TE HTML page) from the browser."""
|
||||
dest = "/tmp/calendar_data.txt"
|
||||
try:
|
||||
content = await file.read()
|
||||
with open(dest, "wb") as f:
|
||||
f.write(content)
|
||||
size_mb = round(len(content) / 1_048_576, 1)
|
||||
print(f"[TE HTML upload] Saved {size_mb} MB to {dest}", flush=True)
|
||||
return {"status": "uploaded", "path": dest, "size_mb": size_mb}
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"Upload failed: {e}")
|
||||
|
||||
|
||||
def _run_te_html_import():
|
||||
global _te_html_status
|
||||
_te_html_status["running"] = True
|
||||
try:
|
||||
from services.te_html_parser import import_html_file
|
||||
path = _find_te_html()
|
||||
if not path:
|
||||
_te_html_status["last_result"] = {"error": "calendar_data.txt not found — upload first"}
|
||||
return
|
||||
result = import_html_file(path)
|
||||
_te_html_status["last_result"] = result
|
||||
except Exception as e:
|
||||
logger.error(f"[eco/te-html-import] Failed: {e}")
|
||||
_te_html_status["last_result"] = {"error": str(e)}
|
||||
finally:
|
||||
_te_html_status["running"] = False
|
||||
|
||||
|
||||
@router.post("/te-html-import")
|
||||
def te_html_import(background_tasks: BackgroundTasks) -> Dict[str, Any]:
|
||||
"""Parse calendar_data.txt (TE HTML) and upsert into ff_calendar."""
|
||||
if _te_html_status["running"]:
|
||||
raise HTTPException(409, "TE HTML import already running")
|
||||
if not _find_te_html():
|
||||
raise HTTPException(404, "calendar_data.txt not found — upload via POST /api/eco/te-html-upload")
|
||||
background_tasks.add_task(_run_te_html_import)
|
||||
return {"status": "started"}
|
||||
|
||||
|
||||
@router.get("/te-html-import/status")
|
||||
def te_html_import_status() -> Dict[str, Any]:
|
||||
return _te_html_status
|
||||
|
||||
|
||||
# ── FXStreet calendar (no API key required) ───────────────────────────────────
|
||||
|
||||
def _run_fxs_sync(weeks_ahead: int):
|
||||
|
||||
Reference in New Issue
Block a user