From 959c2825dfc0bfcd93e0d6b929b2b4e281ccad78 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Mon, 29 Jun 2026 22:38:50 +0200 Subject: [PATCH] feat: instrument analysis --- backend/routers/causal_lab.py | 55 +++++++++-- frontend/src/pages/InstrumentDashboard.tsx | 110 ++++++++++++++++----- 2 files changed, 135 insertions(+), 30 deletions(-) diff --git a/backend/routers/causal_lab.py b/backend/routers/causal_lab.py index 780e9d7..881568b 100644 --- a/backend/routers/causal_lab.py +++ b/backend/routers/causal_lab.py @@ -1391,21 +1391,47 @@ Règles : coef intermédiaire ∈ [-5,5] ; coef output en pips, |val| ∈ [30,20 def refresh_auto_analyses(): """ Re-run _run_auto_analysis for all causal_event_analyses rows with empty actual_json. - Fills in real price data so comprehension scores can be computed. + Returns detailed per-event debug info. """ try: from services.database import get_conn conn = get_conn() - rows = conn.execute(""" + + # Also return a DB state summary for all analyses (not just empty ones) + all_rows = conn.execute(""" SELECT a.id AS cea_id, a.market_event_id, a.template_id, - a.actual_json + a.actual_json, a.prediction_json, + e.name AS event_name, e.start_date FROM causal_event_analyses a - WHERE a.actual_json IS NULL OR a.actual_json = '{}' OR a.actual_json = 'null' + LEFT JOIN market_events e ON e.id = a.market_event_id + ORDER BY a.id DESC """).fetchall() + + db_state = [] + for r in all_rows: + actual = r["actual_json"] or "" + pred = r["prediction_json"] or "" + db_state.append({ + "cea_id": r["cea_id"], + "event_id": r["market_event_id"], + "event_name": r["event_name"], + "start_date": r["start_date"], + "template_id": r["template_id"], + "actual_empty": actual in (None, "", "{}", "null"), + "pred_empty": pred in (None, "", "{}", "null"), + "actual_keys": list(json.loads(actual).keys()) if actual not in (None, "", "{}", "null") else [], + "pred_keys": list(json.loads(pred).keys()) if pred not in (None, "", "{}", "null") else [], + }) + + rows = [r for r in all_rows if (r["actual_json"] or "") in (None, "", "{}", "null")] + print(f"[refresh] {len(rows)} analyses with empty actual_json out of {len(all_rows)} total", flush=True) conn.close() + details = [] refreshed, failed = 0, 0 for row in rows: + detail: dict = {"cea_id": row["cea_id"], "event_id": row["market_event_id"], + "event_name": row["event_name"], "template_id": row["template_id"]} try: conn2 = get_conn() ev_row = conn2.execute( @@ -1413,18 +1439,33 @@ def refresh_auto_analyses(): ).fetchone() conn2.close() if not ev_row: + detail["result"] = "event_not_found" + details.append(detail) continue - ok = _run_auto_analysis(dict(ev_row), row["template_id"]) + ev = dict(ev_row) + detail["surprise_pct"] = ev.get("surprise_pct") + detail["start_date"] = ev.get("start_date") + ok = _run_auto_analysis(ev, row["template_id"]) + detail["result"] = "ok" if ok else "failed" if ok: refreshed += 1 else: failed += 1 + detail["result"] = "run_returned_false" except Exception as _e: logger.warning(f"[refresh_auto_analyses] cea#{row['cea_id']}: {_e}") + detail["result"] = f"exception: {_e}" failed += 1 + details.append(detail) - logger.info(f"[refresh_auto_analyses] done: {refreshed} refreshed, {failed} failed") - return {"refreshed": refreshed, "failed": failed, "total": len(rows)} + print(f"[refresh] done: {refreshed} refreshed, {failed} failed", flush=True) + return { + "refreshed": refreshed, + "failed": failed, + "total": len(rows), + "db_state": db_state, + "details": details, + } except Exception as e: logger.error(f"[refresh_auto_analyses] {e}") raise HTTPException(500, str(e)) diff --git a/frontend/src/pages/InstrumentDashboard.tsx b/frontend/src/pages/InstrumentDashboard.tsx index c84de24..0dce606 100644 --- a/frontend/src/pages/InstrumentDashboard.tsx +++ b/frontend/src/pages/InstrumentDashboard.tsx @@ -1023,10 +1023,12 @@ function ExplanationScore({ }) { const [refreshing, setRefreshing] = useState(false) const [refreshDone, setRefreshDone] = useState(false) + const [debugInfo, setDebugInfo] = useState(null) // For each linked event, compute the comprehension score and collect scored ones const scored: number[] = [] const totalLinked: number[] = [] + const debugRows: {id?: number; name: string; pred: string; actual: string; score: number | null; reason: string}[] = [] for (const ev of events) { if (!ev.template_id) continue @@ -1035,7 +1037,35 @@ function ExplanationScore({ const inst = causalInsts.find(ci => tmpl.instruments.includes(ci)) if (!inst) continue totalLinked.push(ev.id ?? 0) + + // Detailed debug per event + const hasPred = !!(ev.prediction_json && ev.prediction_json !== '{}') + const hasActual = !!(ev.actual_json && ev.actual_json !== '{}') + let reason = '' + if (!hasPred) reason = 'prediction_json vide' + else if (!hasActual) reason = 'actual_json vide' + else { + try { + const preds = JSON.parse(ev.prediction_json!) + const actuals = JSON.parse(ev.actual_json!) + const outputNode = tmpl.graph_json.nodes.find(n => + (n.type === 'market_asset' || n.type === 'output') && n.instrument?.toUpperCase() === inst.toUpperCase() + ) + if (!outputNode) reason = `nœud output "${inst}" introuvable (types: ${[...new Set(tmpl.graph_json.nodes.map(n => n.type))].join(',')})` + else if (preds[outputNode.id] == null) reason = `preds["${outputNode.id}"] absent` + else if (actuals[inst] == null && actuals[inst.toUpperCase()] == null) reason = `actuals["${inst}"] absent — clés: ${Object.keys(actuals).join(',')}` + else reason = 'ok' + } catch (e) { reason = `parse error: ${e}` } + } + debugRows.push({ + id: ev.id, name: ev.title?.slice(0, 30) ?? '?', + pred: hasPred ? '✓' : '✗', + actual: hasActual ? '✓' : '✗', + score: null, reason, + }) + const s = comprehensionScore(ev, tmpl, inst) + if (debugRows.length > 0) debugRows[debugRows.length - 1].score = s if (s != null) scored.push(s) } @@ -1056,12 +1086,17 @@ function ExplanationScore({ : globalScore >= 70 ? 'bg-emerald-500' : globalScore >= 40 ? 'bg-amber-500' : 'bg-red-500' const handleRefresh = async () => { + setDebugInfo(null) setRefreshing(true) try { - await api.post('/causal-lab/auto-analyze/refresh') + const res = await api.post('/causal-lab/auto-analyze/refresh') + const data = res.data + setDebugInfo(data) setRefreshDone(true) onRefreshDone?.() - setTimeout(() => setRefreshDone(false), 3000) + setTimeout(() => setRefreshDone(false), 8000) + } catch (err: any) { + setDebugInfo({ error: String(err) }) } finally { setRefreshing(false) } @@ -1070,28 +1105,57 @@ function ExplanationScore({ const needsRefresh = totalLinked.length > 0 && scored.length === 0 && !refreshDone return ( -
- Note globale -
-
+
+
+ Note globale +
+
+
+ + {globalScore != null ? `${globalScore}% — ` : ''}{verdict} + + + {scored.length}/{totalLinked.length} graphes + + {(needsRefresh || scored.length === 0) && !refreshDone && totalLinked.length > 0 && ( + + )}
- - {globalScore != null ? `${globalScore}% — ` : ''}{verdict} - - - {scored.length}/{totalLinked.length} graphes - - {needsRefresh && ( - - )} - {refreshDone && ( - ✓ Rechargez la page + + {/* Debug panel — score diagnostics per event */} + {debugRows.length > 0 && scored.length === 0 && ( +
+
Diagnostic scores ({debugRows.length} graphes)
+ {debugRows.map((r, i) => ( +
+ {r.pred} + {r.actual} + {r.name} + {r.reason} + {r.score != null && {r.score}%} +
+ ))} + {debugInfo && ( +
+ {debugInfo.error + ? Erreur: {debugInfo.error} + : Refresh: {debugInfo.refreshed} ok / {debugInfo.failed} err / {debugInfo.total} total + } + {debugInfo.details?.map((d: any, i: number) => ( +
+ #{d.event_id} {(d.event_name ?? '').slice(0, 25)} → {d.result} + {d.surprise_pct != null && surp:{d.surprise_pct}} +
+ ))} +
+ )} +
)}
)