feat: calendar
This commit is contained in:
@@ -486,88 +486,56 @@ def fmp_sync_status_ep() -> Dict[str, Any]:
|
||||
return _te_sync_status
|
||||
|
||||
|
||||
# ── Trading Economics HTML import ─────────────────────────────────────────────
|
||||
# ── ForexFactory page HTML import ─────────────────────────────────────────────
|
||||
# User saves the FF calendar page source (Ctrl+U → Save As, or copy-paste HTML)
|
||||
# and uploads it. We extract window.calendarComponentStates[1].days JSON.
|
||||
|
||||
@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"
|
||||
_ff_page_status: Dict[str, Any] = {"running": False, "last_result": None}
|
||||
_FF_PAGE_TMP = os.path.join(os.path.dirname(__file__), "..", "data", "ff_page.html")
|
||||
|
||||
|
||||
@router.post("/ff-page-upload")
|
||||
async def ff_page_upload(file: UploadFile = File(...)) -> Dict[str, Any]:
|
||||
"""Upload the ForexFactory calendar page HTML (Ctrl+U → Save As, any week)."""
|
||||
try:
|
||||
content = await file.read()
|
||||
with open(dest, "wb") as f:
|
||||
with open(_FF_PAGE_TMP, "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}
|
||||
size_mb = round(len(content) / 1_048_576, 2)
|
||||
print(f"[FF page upload] Saved {size_mb} MB", flush=True)
|
||||
return {"status": "uploaded", "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
|
||||
def _run_ff_page_import():
|
||||
global _ff_page_status
|
||||
_ff_page_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
|
||||
from services.ff_page_parser import import_ff_page_file
|
||||
result = import_ff_page_file(os.path.abspath(_FF_PAGE_TMP))
|
||||
_ff_page_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)}
|
||||
logger.error(f"[eco/ff-page-import] Failed: {e}")
|
||||
_ff_page_status["last_result"] = {"error": str(e)}
|
||||
finally:
|
||||
_te_html_status["running"] = False
|
||||
_ff_page_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)
|
||||
@router.post("/ff-page-import")
|
||||
def ff_page_import(background_tasks: BackgroundTasks) -> Dict[str, Any]:
|
||||
"""Parse uploaded FF calendar HTML and upsert into ff_calendar (ForexFactory format)."""
|
||||
if _ff_page_status["running"]:
|
||||
raise HTTPException(409, "FF page import already running")
|
||||
if not os.path.exists(_FF_PAGE_TMP):
|
||||
raise HTTPException(404, "No FF page uploaded yet — POST /api/eco/ff-page-upload first")
|
||||
background_tasks.add_task(_run_ff_page_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):
|
||||
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("/ff-page-import/status")
|
||||
def ff_page_import_status() -> Dict[str, Any]:
|
||||
return _ff_page_status
|
||||
|
||||
|
||||
# ── Unified calendar sync ─────────────────────────────────────────────────────
|
||||
@@ -585,7 +553,7 @@ 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."""
|
||||
"""Trigger unified calendar sync (FF live JSON thisweek+nextweek + FF HTML scraper) in background."""
|
||||
from services.calendar_sync import get_sync_status
|
||||
if get_sync_status()["running"]:
|
||||
raise HTTPException(409, "Calendar sync already running")
|
||||
@@ -688,6 +656,23 @@ def series_history(
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/ff-purge-fxstreet")
|
||||
def ff_purge_fxstreet() -> Dict[str, Any]:
|
||||
"""
|
||||
Delete all rows sourced from FXStreet (source='fxstreet').
|
||||
Run once to clean up corrupted/decimal-format data, then POST /calendar-sync to refill from FF.
|
||||
"""
|
||||
from services.database import get_conn
|
||||
conn = get_conn()
|
||||
try:
|
||||
count = conn.execute("SELECT COUNT(*) FROM ff_calendar WHERE source='fxstreet'").fetchone()[0]
|
||||
conn.execute("DELETE FROM ff_calendar WHERE source='fxstreet'")
|
||||
conn.commit()
|
||||
return {"deleted": count, "next_step": "POST /api/eco/calendar-sync to refill from ForexFactory"}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.get("/ff-stats")
|
||||
def ff_stats() -> Dict[str, Any]:
|
||||
"""Quick inventory of ff_calendar table."""
|
||||
|
||||
Reference in New Issue
Block a user