feat: FF→FRED series mapping + MacroSeries visualization page

Backend:
- ff_calendar: add series_id column (migration) + FF_TO_FRED mapping dict
  (NFP→PAYEMS, CPI→CPIAUCSL, Jobless Claims→ICSA, GDP→GDPC1, FEDFUNDS, PCE)
- import_csv + sync_live now populate series_id on each FF event
- New GET /api/eco/series/{id}/history: FRED time series + linked FF events
  (surprises, forecast, actual) merged by date — enables context queries

Frontend:
- New MacroSeriesPage.tsx: sidebar with 11 FRED series grouped by category,
  recharts ComposedChart with area + z-score surprise reference lines (|z|≥1.5),
  KPI cards (latest/prev/min/max), FF events table (actual vs forecast coloring),
  z-score bar chart for recent surprises, range selector (1Y/2Y/5Y/10Y/All)
- Route /macro-series + Sidebar entry "Macro Series"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-26 16:51:55 +02:00
parent b187107468
commit afbfeff468
6 changed files with 650 additions and 4 deletions

View File

@@ -407,6 +407,67 @@ def ff_calendar(
)
@router.get("/series/{series_id}/history")
def series_history(
series_id: str,
from_date: Optional[str] = Query(None, description="YYYY-MM-DD"),
to_date: Optional[str] = Query(None, description="YYYY-MM-DD"),
) -> Dict[str, Any]:
"""
Time series data for a FRED series (from economic_events table)
combined with the matching FF calendar events (surprises, forecasts).
"""
from services.database import get_conn
from services.fred_bootstrap import FRED_SERIES
meta = FRED_SERIES.get(series_id)
if not meta:
raise HTTPException(404, f"Unknown series: {series_id}")
conn = get_conn()
try:
# ── FRED time series ──────────────────────────────────────────────
ts_where = ["series_id = ?"]
ts_params: list = [series_id]
if from_date:
ts_where.append("event_date >= ?"); ts_params.append(from_date)
if to_date:
ts_where.append("event_date <= ?"); ts_params.append(to_date)
ts_rows = conn.execute(
f"SELECT event_date, actual_value, surprise_zscore, surprise_direction "
f"FROM economic_events WHERE {' AND '.join(ts_where)} ORDER BY event_date ASC",
ts_params,
).fetchall()
# ── FF events for this series ─────────────────────────────────────
ff_where = ["series_id = ?", "currency = 'USD'"]
ff_params: list = [series_id]
if from_date:
ff_where.append("event_date >= ?"); ff_params.append(from_date)
if to_date:
ff_where.append("event_date <= ?"); ff_params.append(to_date)
ff_rows = conn.execute(
f"SELECT event_date, event_time, event_name, actual_value, "
f"forecast_value, previous_value, impact "
f"FROM ff_calendar WHERE {' AND '.join(ff_where)} ORDER BY event_date ASC",
ff_params,
).fetchall()
return {
"series_id": series_id,
"name": meta["name"],
"unit": meta["unit"],
"category": meta["category"],
"freq": meta["freq"],
"timeseries": [dict(r) for r in ts_rows],
"events": [dict(r) for r in ff_rows],
}
finally:
conn.close()
@router.get("/ff-stats")
def ff_stats() -> Dict[str, Any]:
"""Quick inventory of ff_calendar table."""