feat: instrument analysis
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user