feat: upcoming economic releases panel on Calendar page
- eco.py: GET /api/eco/upcoming — estimates next release date per series from last stored date + frequency + typical publication lag; returns status (imminent/due_soon/upcoming/scheduled/overdue/no_data) - CalendarPage.tsx: UpcomingPanel component in right sidebar showing next expected dates, J-N countdown, last value + direction signal; color-coded by urgency (orange=cette semaine, yellow=attendu, blue=ce mois) Dates are approximate (freq + typical lag), not official FRED schedule. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -192,6 +192,104 @@ def list_eco_events(
|
||||
return {"total": total, "offset": offset, "limit": limit, "events": events}
|
||||
|
||||
|
||||
# ── Upcoming releases ────────────────────────────────────────────────────────
|
||||
|
||||
# Typical publication lag (days) after the end of the reference period
|
||||
_RELEASE_LAG: Dict[str, Dict[str, Any]] = {
|
||||
"PAYEMS": {"freq_days": 30, "lag_days": 35, "note": "1er vendredi du mois"},
|
||||
"UNRATE": {"freq_days": 30, "lag_days": 35, "note": "Avec NFP"},
|
||||
"CPIAUCSL": {"freq_days": 30, "lag_days": 15, "note": "Mi-mois"},
|
||||
"CPILFESL": {"freq_days": 30, "lag_days": 15, "note": "Avec CPI"},
|
||||
"PCEPILFE": {"freq_days": 30, "lag_days": 30, "note": "Fin de mois"},
|
||||
"FEDFUNDS": {"freq_days": 45, "lag_days": 45, "note": "Réunion FOMC (~8/an)"},
|
||||
"ICSA": {"freq_days": 7, "lag_days": 4, "note": "Chaque jeudi"},
|
||||
"GDPC1": {"freq_days": 90, "lag_days": 30, "note": "Estimation avancée BEA"},
|
||||
"BAMLH0A0HYM2": {"freq_days": 7, "lag_days": 1, "note": "Hebdomadaire"},
|
||||
"T10Y2Y": {"freq_days": 7, "lag_days": 1, "note": "Hebdomadaire"},
|
||||
"T10Y3M": {"freq_days": 7, "lag_days": 1, "note": "Hebdomadaire"},
|
||||
}
|
||||
|
||||
|
||||
@router.get("/upcoming")
|
||||
def upcoming_events() -> List[Dict[str, Any]]:
|
||||
"""Estimate next release dates for each series based on last stored date + typical lag."""
|
||||
from services.database import get_conn
|
||||
from services.fred_bootstrap import FRED_SERIES
|
||||
from datetime import date, timedelta
|
||||
|
||||
conn = get_conn()
|
||||
today = date.today()
|
||||
upcoming = []
|
||||
|
||||
for sid, lag_info in _RELEASE_LAG.items():
|
||||
meta = FRED_SERIES.get(sid)
|
||||
if not meta:
|
||||
continue
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT MAX(event_date), actual_value, actual_unit, surprise_direction "
|
||||
"FROM economic_events WHERE series_id=?",
|
||||
(sid,),
|
||||
).fetchone()
|
||||
if not row or not row[0]:
|
||||
# No data in DB yet — still show as "not loaded"
|
||||
upcoming.append({
|
||||
"series_id": sid,
|
||||
"name": meta["name"],
|
||||
"note": lag_info["note"],
|
||||
"category": meta["category"],
|
||||
"last_release": None,
|
||||
"last_value": None,
|
||||
"last_unit": meta["unit"],
|
||||
"last_direction": None,
|
||||
"next_expected": None,
|
||||
"days_until": None,
|
||||
"status": "no_data",
|
||||
})
|
||||
continue
|
||||
|
||||
last_date = date.fromisoformat(row[0])
|
||||
freq_days = lag_info["freq_days"]
|
||||
lag_days = lag_info["lag_days"]
|
||||
|
||||
# Next expected = last ref period end + lag
|
||||
next_release = last_date + timedelta(days=freq_days + lag_days)
|
||||
|
||||
days_until = (next_release - today).days
|
||||
|
||||
if days_until < -freq_days:
|
||||
# Very overdue — likely bootstrap missing recent data
|
||||
status = "overdue"
|
||||
elif days_until < 0:
|
||||
status = "due_soon" # probably released, not yet in DB
|
||||
elif days_until <= 7:
|
||||
status = "imminent"
|
||||
elif days_until <= 30:
|
||||
status = "upcoming"
|
||||
else:
|
||||
status = "scheduled"
|
||||
|
||||
upcoming.append({
|
||||
"series_id": sid,
|
||||
"name": meta["name"],
|
||||
"note": lag_info["note"],
|
||||
"category": meta["category"],
|
||||
"last_release": str(last_date),
|
||||
"last_value": round(float(row[1]), 3) if row[1] is not None else None,
|
||||
"last_unit": row[2] or meta["unit"],
|
||||
"last_direction": row[3],
|
||||
"next_expected": str(next_release),
|
||||
"days_until": days_until,
|
||||
"status": status,
|
||||
})
|
||||
except Exception as e:
|
||||
logger.debug(f"[eco/upcoming] {sid}: {e}")
|
||||
|
||||
conn.close()
|
||||
upcoming.sort(key=lambda x: (x["next_expected"] or "9999", x["series_id"]))
|
||||
return upcoming
|
||||
|
||||
|
||||
# ── DB count summary ──────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/status")
|
||||
|
||||
Reference in New Issue
Block a user