feat: instrument analysis
This commit is contained in:
@@ -741,7 +741,7 @@ const FRISE_POPUP_W = 220 // largeur du popup
|
|||||||
|
|
||||||
function CausalFrise({
|
function CausalFrise({
|
||||||
events, templates, priceData, selectedDate, causalInsts,
|
events, templates, priceData, selectedDate, causalInsts,
|
||||||
chartDateToX, chartCanvasLeft, chartReady, causalScores,
|
chartDateToX, chartCanvasLeft, chartReady,
|
||||||
}: {
|
}: {
|
||||||
events: SnapshotEvent[]
|
events: SnapshotEvent[]
|
||||||
templates: CausalTemplate[]
|
templates: CausalTemplate[]
|
||||||
@@ -751,7 +751,6 @@ function CausalFrise({
|
|||||||
chartDateToX?: (d: string) => number | null
|
chartDateToX?: (d: string) => number | null
|
||||||
chartCanvasLeft?: number
|
chartCanvasLeft?: number
|
||||||
chartReady?: number
|
chartReady?: number
|
||||||
causalScores: Record<number, number | null>
|
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
@@ -1062,13 +1061,29 @@ function CausalFrise({
|
|||||||
|
|
||||||
// ── ExplanationScore ──────────────────────────────────────────────────────────
|
// ── 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({
|
function ExplanationScore({
|
||||||
events, templates, causalInsts, onRefreshDone, onDebugResult, debugInfo, causalScores,
|
events, templates, causalInsts, onRefreshDone, onDebugResult, debugInfo,
|
||||||
}: {
|
}: {
|
||||||
events: SnapshotEvent[]
|
events: SnapshotEvent[]
|
||||||
templates: CausalTemplate[]
|
templates: CausalTemplate[]
|
||||||
causalInsts: string[]
|
causalInsts: string[]
|
||||||
causalScores: Record<number, number | null>
|
|
||||||
onRefreshDone?: () => void
|
onRefreshDone?: () => void
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
onDebugResult?: (data: any) => void
|
onDebugResult?: (data: any) => void
|
||||||
@@ -1077,51 +1092,33 @@ function ExplanationScore({
|
|||||||
}) {
|
}) {
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
const [refreshDone, setRefreshDone] = 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 scored: number[] = []
|
||||||
const totalLinked: 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) {
|
for (const ev of events) {
|
||||||
if (!ev.template_id) continue
|
if (!ev.template_id) continue
|
||||||
const tmpl = templates.find(t => t.id === ev.template_id)
|
const tmpl = templates.find(t => t.id === ev.template_id)
|
||||||
if (!tmpl) continue
|
if (!tmpl) continue
|
||||||
const inst = causalInsts.find(ci => tmpl.instruments.includes(ci))
|
if (!causalInsts.some(ci => tmpl.instruments.includes(ci))) continue
|
||||||
if (!inst) continue
|
|
||||||
totalLinked.push(ev.id ?? 0)
|
totalLinked.push(ev.id ?? 0)
|
||||||
|
const s = ev.id != null ? liveScores[ev.id] : null
|
||||||
// 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
|
|
||||||
if (s != null) scored.push(s)
|
if (s != null) scored.push(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1184,57 +1181,13 @@ function ExplanationScore({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Debug panel — score diagnostics per event */}
|
{/* Refresh result panel */}
|
||||||
{debugRows.length > 0 && scored.length === 0 && (
|
{debugInfo && (
|
||||||
<div className="rounded-lg border border-slate-700/30 bg-dark-900/60 p-3 text-[10px] font-mono">
|
<div className="rounded-lg border border-slate-700/30 bg-dark-900/60 p-3 text-[10px] font-mono text-slate-600">
|
||||||
<div className="text-slate-500 mb-1.5 uppercase tracking-wide">Diagnostic scores ({debugRows.length} graphes)</div>
|
{debugInfo.error
|
||||||
{debugRows.map((r, i) => (
|
? <span className="text-red-500">Erreur: {debugInfo.error}</span>
|
||||||
<div key={i} className="flex gap-2 py-0.5 border-b border-slate-800/40 last:border-0">
|
: <span>Refresh: {debugInfo.refreshed} ok / {debugInfo.failed} err / {debugInfo.total} total</span>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1666,7 +1619,6 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
|
|||||||
chartDateToX={chartDateToXRef.current ?? undefined}
|
chartDateToX={chartDateToXRef.current ?? undefined}
|
||||||
chartCanvasLeft={chartCanvasLeftRef.current}
|
chartCanvasLeft={chartCanvasLeftRef.current}
|
||||||
chartReady={chartReady}
|
chartReady={chartReady}
|
||||||
causalScores={causalScores}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1713,7 +1665,6 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
|
|||||||
events={snapshot.events}
|
events={snapshot.events}
|
||||||
templates={templates}
|
templates={templates}
|
||||||
causalInsts={causalInsts}
|
causalInsts={causalInsts}
|
||||||
causalScores={causalScores}
|
|
||||||
onRefreshDone={fetchSnapshotSilent}
|
onRefreshDone={fetchSnapshotSilent}
|
||||||
onDebugResult={setRefreshDebug}
|
onDebugResult={setRefreshDebug}
|
||||||
debugInfo={refreshDebug}
|
debugInfo={refreshDebug}
|
||||||
|
|||||||
Reference in New Issue
Block a user