From b8a1d2ad071ca3a00110193b5e417831e311ccb8 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Tue, 30 Jun 2026 18:45:12 +0200 Subject: [PATCH] feat: instrument analysis --- frontend/src/pages/InstrumentDashboard.tsx | 137 +++++++-------------- 1 file changed, 44 insertions(+), 93 deletions(-) diff --git a/frontend/src/pages/InstrumentDashboard.tsx b/frontend/src/pages/InstrumentDashboard.tsx index 0d57704..96e728e 100644 --- a/frontend/src/pages/InstrumentDashboard.tsx +++ b/frontend/src/pages/InstrumentDashboard.tsx @@ -741,7 +741,7 @@ const FRISE_POPUP_W = 220 // largeur du popup function CausalFrise({ events, templates, priceData, selectedDate, causalInsts, - chartDateToX, chartCanvasLeft, chartReady, causalScores, + chartDateToX, chartCanvasLeft, chartReady, }: { events: SnapshotEvent[] templates: CausalTemplate[] @@ -751,7 +751,6 @@ function CausalFrise({ chartDateToX?: (d: string) => number | null chartCanvasLeft?: number chartReady?: number - causalScores: Record }) { const navigate = useNavigate() const containerRef = useRef(null) @@ -1062,13 +1061,29 @@ function CausalFrise({ // ── ExplanationScore ────────────────────────────────────────────────────────── +function computeMagnitudeScore(analysis: { prediction_json: Record; actual_json: Record; graph_json: { nodes?: { id: string; type?: string; instrument?: string }[] } }): number | null { + const preds = analysis.prediction_json || {} + const actuals = analysis.actual_json || {} + const outputNodes = (analysis.graph_json?.nodes || []).filter(n => n.type === 'market_asset' || n.type === 'output') + const nodeScores = outputNodes.map(n => { + const pred = preds[n.id] + const act = actuals[n.instrument || n.id] ?? actuals[n.id] + if (pred == null || act == null) return null + if (pred === 0 && act === 0) return 100 + if (pred === 0) return 40 + if (pred * act <= 0) return 0 + const ratio = Math.min(Math.abs(act), Math.abs(pred)) / Math.max(Math.abs(act), Math.abs(pred)) + return Math.round(50 + ratio * 50) + }).filter((s): s is number => s != null) + return nodeScores.length > 0 ? Math.round(nodeScores.reduce((a, b) => a + b, 0) / nodeScores.length) : null +} + function ExplanationScore({ - events, templates, causalInsts, onRefreshDone, onDebugResult, debugInfo, causalScores, + events, templates, causalInsts, onRefreshDone, onDebugResult, debugInfo, }: { events: SnapshotEvent[] templates: CausalTemplate[] causalInsts: string[] - causalScores: Record onRefreshDone?: () => void // eslint-disable-next-line @typescript-eslint/no-explicit-any onDebugResult?: (data: any) => void @@ -1077,51 +1092,33 @@ function ExplanationScore({ }) { const [refreshing, setRefreshing] = useState(false) const [refreshDone, setRefreshDone] = useState(false) + const [liveScores, setLiveScores] = useState>({}) + + // Fetch all linked event analyses dynamically — same formula as MarketEvents expanded panel + const linkedIds = events.filter(ev => ev.template_id && ev.id).map(ev => ev.id as number) + const linkedKey = linkedIds.join(',') + useEffect(() => { + if (!linkedIds.length) return + Promise.all(linkedIds.map(id => + api.get(`/causal-lab/analyses?market_event_id=${id}&limit=1`) + .then(r => ({ id, score: computeMagnitudeScore(r.data?.[0]) })) + .catch(() => ({ id, score: null })) + )).then(results => { + setLiveScores(Object.fromEntries(results.map(r => [r.id, r.score]))) + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [linkedKey]) - // 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; surprisePct?: number | null; rawPred?: string}[] = [] for (const ev of events) { if (!ev.template_id) continue const tmpl = templates.find(t => t.id === ev.template_id) if (!tmpl) continue - const inst = causalInsts.find(ci => tmpl.instruments.includes(ci)) - if (!inst) continue + if (!causalInsts.some(ci => tmpl.instruments.includes(ci))) 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, - surprisePct: ev.surprise_pct ?? null, - rawPred: (ev.prediction_json ?? 'null').slice(0, 30), - }) - - const snapScore = ev.activation_score != null ? Math.round(ev.activation_score * 100) : null - const s = ev.id != null ? (causalScores[ev.id] ?? snapScore) : snapScore - if (debugRows.length > 0) debugRows[debugRows.length - 1].score = s + const s = ev.id != null ? liveScores[ev.id] : null if (s != null) scored.push(s) } @@ -1184,57 +1181,13 @@ function ExplanationScore({ )} - {/* 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.surprisePct != null - ? {r.surprisePct > 0 ? '+' : ''}{r.surprisePct.toFixed(1)}% - : surp?} - {r.score != null && {r.score}%} - {r.rawPred} -
- ))} - {debugInfo && ( -
- {debugInfo.error - ? Erreur: {debugInfo.error} - : Refresh: {debugInfo.refreshed} ok / {debugInfo.failed} err / {debugInfo.total} total - } - {debugInfo.details?.map((d: any, i: number) => ( -
- {d.result} - #{d.event_id} {(d.event_name ?? '').slice(0, 20)} - {d.run_error && err:{d.run_error.slice(0, 40)}} -
- surp:{d.surprise_pct != null ? Number(d.surprise_pct).toFixed(1)+'%' : `null (act=${d.actual_value} exp=${d.expected_value})`} - {' | '}inputs:{JSON.stringify(d.inputs ?? {})} - {' | '}pred_keys:[{(d.node_keys ?? []).join(',')}] - {' | '}actual:{JSON.stringify(d.actual_moves ?? {})} -
-
- ))} - {/* DB state: show rows not in refresh (non-empty pred/actual but still 'vide' in snapshot) */} - {debugInfo.db_state?.length > 0 && ( -
-
DB state ({debugInfo.db_state.length} analyses)
- {debugInfo.db_state.map((r: any, i: number) => ( -
- #{r.event_id} {(r.event_name ?? '').slice(0, 18)} - pred:{r.pred_empty ? '∅' : `[${(r.pred_keys ?? []).join(',')}]`} - act:{r.actual_empty ? '∅' : JSON.stringify(r.actual_keys)} -
- ))} -
- )} -
- )} + {/* Refresh result panel */} + {debugInfo && ( +
+ {debugInfo.error + ? Erreur: {debugInfo.error} + : Refresh: {debugInfo.refreshed} ok / {debugInfo.failed} err / {debugInfo.total} total + }
)}
@@ -1666,7 +1619,6 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i chartDateToX={chartDateToXRef.current ?? undefined} chartCanvasLeft={chartCanvasLeftRef.current} chartReady={chartReady} - causalScores={causalScores} /> @@ -1713,7 +1665,6 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i events={snapshot.events} templates={templates} causalInsts={causalInsts} - causalScores={causalScores} onRefreshDone={fetchSnapshotSilent} onDebugResult={setRefreshDebug} debugInfo={refreshDebug}