feat: instrument analysis

This commit is contained in:
OpenSquared
2026-06-30 18:45:12 +02:00
parent 479b2b7b05
commit b8a1d2ad07

View File

@@ -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<number, number | null>
}) {
const navigate = useNavigate()
const containerRef = useRef<HTMLDivElement>(null)
@@ -1062,13 +1061,29 @@ function CausalFrise({
// ── ExplanationScore ──────────────────────────────────────────────────────────
function computeMagnitudeScore(analysis: { prediction_json: Record<string, number>; actual_json: Record<string, number>; 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<number, number | null>
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<Record<number, number | null>>({})
// 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({
)}
</div>
{/* Debug panel — score diagnostics per event */}
{debugRows.length > 0 && scored.length === 0 && (
<div className="rounded-lg border border-slate-700/30 bg-dark-900/60 p-3 text-[10px] font-mono">
<div className="text-slate-500 mb-1.5 uppercase tracking-wide">Diagnostic scores ({debugRows.length} graphes)</div>
{debugRows.map((r, i) => (
<div key={i} className="flex gap-2 py-0.5 border-b border-slate-800/40 last:border-0">
<span className={clsx('shrink-0 w-4', r.pred === '✓' ? 'text-emerald-600' : 'text-red-600')}>{r.pred}</span>
<span className={clsx('shrink-0 w-4', r.actual === '✓' ? 'text-emerald-600' : 'text-red-600')}>{r.actual}</span>
<span className="text-slate-400 w-32 shrink-0 truncate" title={r.name}>{r.name}</span>
<span className={clsx('flex-1', r.reason === 'ok' ? 'text-emerald-600' : 'text-amber-600')}>{r.reason}</span>
{r.surprisePct != null
? <span className="text-cyan-600 shrink-0">{r.surprisePct > 0 ? '+' : ''}{r.surprisePct.toFixed(1)}%</span>
: <span className="text-red-800 shrink-0">surp?</span>}
{r.score != null && <span className="text-violet-400 shrink-0">{r.score}%</span>}
<span className="text-slate-700 shrink-0 ml-1" title="raw prediction_json">{r.rawPred}</span>
</div>
))}
{debugInfo && (
<div className="mt-2 pt-2 border-t border-slate-800/40 text-slate-600">
{debugInfo.error
? <span className="text-red-500">Erreur: {debugInfo.error}</span>
: <span>Refresh: {debugInfo.refreshed} ok / {debugInfo.failed} err / {debugInfo.total} total</span>
}
{debugInfo.details?.map((d: any, i: number) => (
<div key={i} className="mt-1 border-t border-slate-800/40 pt-1">
<span className={d.result === 'ok' ? 'text-emerald-500' : 'text-red-500'}>{d.result}</span>
<span className="text-slate-500 ml-1">#{d.event_id} {(d.event_name ?? '').slice(0, 20)}</span>
{d.run_error && <span className="text-red-400 ml-1">err:{d.run_error.slice(0, 40)}</span>}
<div className="ml-2 text-slate-600">
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 ?? {})}
</div>
</div>
))}
{/* DB state: show rows not in refresh (non-empty pred/actual but still 'vide' in snapshot) */}
{debugInfo.db_state?.length > 0 && (
<div className="mt-2 pt-1 border-t border-slate-800/40">
<div className="text-slate-600 mb-0.5">DB state ({debugInfo.db_state.length} analyses)</div>
{debugInfo.db_state.map((r: any, i: number) => (
<div key={i} className="text-slate-700">
#{r.event_id} {(r.event_name ?? '').slice(0, 18)}
<span className={r.pred_empty ? 'text-red-800' : 'text-emerald-800'}> pred:{r.pred_empty ? '∅' : `[${(r.pred_keys ?? []).join(',')}]`}</span>
<span className={r.actual_empty ? 'text-red-800' : 'text-emerald-800'}> act:{r.actual_empty ? '∅' : JSON.stringify(r.actual_keys)}</span>
</div>
))}
</div>
)}
</div>
)}
{/* Refresh result panel */}
{debugInfo && (
<div className="rounded-lg border border-slate-700/30 bg-dark-900/60 p-3 text-[10px] font-mono text-slate-600">
{debugInfo.error
? <span className="text-red-500">Erreur: {debugInfo.error}</span>
: <span>Refresh: {debugInfo.refreshed} ok / {debugInfo.failed} err / {debugInfo.total} total</span>
}
</div>
)}
</div>
@@ -1666,7 +1619,6 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
chartDateToX={chartDateToXRef.current ?? undefined}
chartCanvasLeft={chartCanvasLeftRef.current}
chartReady={chartReady}
causalScores={causalScores}
/>
</div>
@@ -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}