From 325d52b3b945efbf6b13a47bfe30b945ef1a8d18 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Fri, 26 Jun 2026 14:23:09 +0200 Subject: [PATCH] feat: market events db migration + generation date filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Database migration: - Add 'origin' and 'source_refs' columns to market_events ALTER TABLE migration (sub_type/actual_value/expected_value/surprise_pct were already there) - All new tables (macro_gauge_snapshots, ai_desks) created via CREATE TABLE IF NOT EXISTS on next init_db() call (container restart) Backend: - GET /api/market-events/db-status — health check returning row counts, latest dates, and missing columns for all 6 tables needed by the detector - list_events() now accepts gen_date_from / gen_date_to query params filtering by date(created_at) — separate from start_date event date filters Frontend (MarketEvents.tsx): - MarketEvent interface: add created_at field - EventRow: show generation date as ⚡MM-DD next to event date - Extended filters: new '⚡ Date de génération' section with from/to inputs filtered independently from the event date range - Clear-all button includes genFrom/genTo reset Co-Authored-By: Claude Sonnet 4.6 --- backend/routers/market_events.py | 50 ++++++++++++++++++++++++++++- backend/services/database.py | 2 ++ frontend/src/pages/MarketEvents.tsx | 44 +++++++++++++++++++++++-- 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/backend/routers/market_events.py b/backend/routers/market_events.py index d19a1f7..183543c 100644 --- a/backend/routers/market_events.py +++ b/backend/routers/market_events.py @@ -84,6 +84,45 @@ _SORT_COLS = { "instrument_score": "COALESCE(ii.adjusted_score, ii.impact_score)", } +@router.get("/db-status") +def db_status() -> Dict[str, Any]: + """Quick health-check of all tables needed for market event generation.""" + from services.database import get_conn + conn = get_conn() + status: Dict[str, Any] = {} + + tables = { + "market_events": "SELECT COUNT(*), MAX(created_at) FROM market_events", + "economic_events": "SELECT COUNT(*), MAX(fetched_at) FROM economic_events", + "macro_gauge_snapshots":"SELECT COUNT(*), MAX(snapshot_date) FROM macro_gauge_snapshots", + "macro_regime_history": "SELECT COUNT(*), MAX(timestamp) FROM macro_regime_history", + "institutional_reports":"SELECT COUNT(*), MAX(fetch_date) FROM institutional_reports", + "ai_desks": "SELECT COUNT(*), NULL FROM ai_desks", + } + for table, sql in tables.items(): + try: + row = conn.execute(sql).fetchone() + status[table] = {"count": row[0], "latest": row[1]} + except Exception as e: + status[table] = {"count": 0, "latest": None, "error": str(e)} + + # Check market_events columns + existing_cols = {row[1] for row in conn.execute("PRAGMA table_info(market_events)").fetchall()} + required_cols = ["sub_type", "origin", "source_refs", "actual_value", "expected_value", "surprise_pct"] + status["market_events"]["columns_ok"] = all(c in existing_cols for c in required_cols) + status["market_events"]["missing_columns"] = [c for c in required_cols if c not in existing_cols] + + # Summary of ai_desks types + try: + desks = conn.execute("SELECT type, active FROM ai_desks").fetchall() + status["ai_desks"]["desks"] = [{"type": r[0], "active": bool(r[1])} for r in desks] + except Exception: + pass + + conn.close() + return status + + @router.get("") def list_events( # text / category / level @@ -91,9 +130,12 @@ def list_events( category: Optional[str] = Query(None), level: Optional[str] = Query(None), origin: Optional[str] = Query(None), - # date range + # event date range (start_date) date_from: Optional[str] = Query(None), date_to: Optional[str] = Query(None), + # generation date range (created_at) + gen_date_from: Optional[str] = Query(None, description="Filter by generation date (created_at) from"), + gen_date_to: Optional[str] = Query(None, description="Filter by generation date (created_at) to"), # global impact score min_score: float = Query(0.0, ge=0.0, le=1.0), # instrument impact filter @@ -143,6 +185,12 @@ def list_events( if date_to: where_parts.append("me.start_date<=?") params.append(date_to) + if gen_date_from: + where_parts.append("date(me.created_at)>=?") + params.append(gen_date_from) + if gen_date_to: + where_parts.append("date(me.created_at)<=?") + params.append(gen_date_to) if min_score > 0: where_parts.append("me.impact_score>=?") params.append(min_score) diff --git a/backend/services/database.py b/backend/services/database.py index ca15087..a9032a7 100644 --- a/backend/services/database.py +++ b/backend/services/database.py @@ -774,6 +774,8 @@ def init_db(): ("surprise_pct", "REAL DEFAULT NULL"), ("unit", "TEXT DEFAULT NULL"), ("sub_type", "TEXT DEFAULT NULL"), + ("origin", "TEXT DEFAULT NULL"), + ("source_refs", "TEXT DEFAULT '[]'"), ]: try: c.execute(f"ALTER TABLE market_events ADD COLUMN {_col} {_def}") diff --git a/frontend/src/pages/MarketEvents.tsx b/frontend/src/pages/MarketEvents.tsx index 9834ec7..c13b554 100644 --- a/frontend/src/pages/MarketEvents.tsx +++ b/frontend/src/pages/MarketEvents.tsx @@ -45,6 +45,7 @@ interface MarketEvent { absorption_pct: number | null source_refs: SourceRef[] origin?: string + created_at?: string impacts?: InstrumentImpact[] evaluated?: boolean } @@ -780,6 +781,11 @@ function EventRow({ {(ORIGIN_META[ev.origin] ?? ORIGIN_META['unknown']).icon} )} + {ev.created_at && ( + + ⚡{ev.created_at.slice(5, 10)} + + )}
@@ -1130,6 +1136,9 @@ export default function MarketEvents() { const [dateFrom, setDateFrom] = useState('') const [dateTo, setDateTo] = useState('') const [datePreset, setDatePreset] = useState('all') // 7d|30d|90d|6m|1y|all|custom + // Generation date filter (created_at) + const [genFrom, setGenFrom] = useState('') + const [genTo, setGenTo] = useState('') // Instrument impact filter const [instTicker, setInstTicker] = useState('') const [instMinScore, setInstMin] = useState(0.3) @@ -1177,6 +1186,8 @@ export default function MarketEvents() { if (minScore) params.set('min_score', String(minScore)) if (dateFrom) params.set('date_from', dateFrom) if (dateTo) params.set('date_to', dateTo) + if (genFrom) params.set('gen_date_from', genFrom) + if (genTo) params.set('gen_date_to', genTo) if (evaluated) params.set('evaluated', evaluated) if (instTicker) { params.set('instrument', instTicker) @@ -1197,7 +1208,7 @@ export default function MarketEvents() { if (e.name === 'AbortError') return // superseded by a newer load } setLoading(false) - }, [search, category, level, minScore, dateFrom, dateTo, evaluated, + }, [search, category, level, minScore, dateFrom, dateTo, genFrom, genTo, evaluated, instTicker, instMinScore, instDir, sortBy, sortDir]) useEffect(() => { @@ -1418,6 +1429,34 @@ export default function MarketEvents() {
)} + + {/* ── Generation date filter ── */} +
+
+ ⚡ Date de génération + {(genFrom || genTo) && ( + + )} +
+
+
+
Généré depuis
+ setGenFrom(e.target.value)} + className="w-full bg-dark-900 border border-slate-700 rounded px-2 py-1 text-slate-300 text-xs" /> +
+
+
Généré jusqu'à
+ setGenTo(e.target.value)} + className="w-full bg-dark-900 border border-slate-700 rounded px-2 py-1 text-slate-300 text-xs" /> +
+
+ {(genFrom || genTo) && ( +
+ ⚡ Filtre actif sur date de génération (created_at) +
+ )} +
)} @@ -1439,10 +1478,11 @@ export default function MarketEvents() {
{loading ? '...' : total} événement{total !== 1 ? 's' : ''} {instTicker && · impact {instTicker}} - {(category || level || search || minScore > 0 || evaluated || dateFrom || dateTo || instTicker) && ( + {(category || level || search || minScore > 0 || evaluated || dateFrom || dateTo || genFrom || genTo || instTicker) && ( )}