feat: instrument analysis

This commit is contained in:
OpenSquared
2026-06-29 22:38:50 +02:00
parent ce96e55314
commit 959c2825df
2 changed files with 135 additions and 30 deletions

View File

@@ -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))

View File

@@ -1023,10 +1023,12 @@ function ExplanationScore({
}) {
const [refreshing, setRefreshing] = useState(false)
const [refreshDone, setRefreshDone] = useState(false)
const [debugInfo, setDebugInfo] = useState<any>(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 (
<div className="flex items-center gap-3 px-4 py-2.5 rounded-xl border border-slate-700/30 bg-dark-800/50">
<span className="text-xs text-slate-500 whitespace-nowrap">Note globale</span>
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div className={clsx('h-full rounded-full transition-all', barCls)} style={{ width: `${globalScore ?? 0}%` }} />
<div className="space-y-2">
<div className="flex items-center gap-3 px-4 py-2.5 rounded-xl border border-slate-700/30 bg-dark-800/50">
<span className="text-xs text-slate-500 whitespace-nowrap">Note globale</span>
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div className={clsx('h-full rounded-full transition-all', barCls)} style={{ width: `${globalScore ?? 0}%` }} />
</div>
<span className={clsx('text-xs font-semibold px-2 py-1 rounded-lg border whitespace-nowrap', badgeCls)}>
{globalScore != null ? `${globalScore}% — ` : ''}{verdict}
</span>
<span className="text-[10px] text-slate-600 whitespace-nowrap">
{scored.length}/{totalLinked.length} graphes
</span>
{(needsRefresh || scored.length === 0) && !refreshDone && totalLinked.length > 0 && (
<button
onClick={handleRefresh}
disabled={refreshing}
className="text-[10px] text-violet-400 hover:text-violet-200 bg-violet-900/20 hover:bg-violet-900/40 border border-violet-800/40 rounded px-2 py-1 whitespace-nowrap disabled:opacity-50 transition-colors"
>
{refreshing ? '…' : 'Recalculer'}
</button>
)}
</div>
<span className={clsx('text-xs font-semibold px-2 py-1 rounded-lg border whitespace-nowrap', badgeCls)}>
{globalScore != null ? `${globalScore}% — ` : ''}{verdict}
</span>
<span className="text-[10px] text-slate-600 whitespace-nowrap">
{scored.length}/{totalLinked.length} graphes
</span>
{needsRefresh && (
<button
onClick={handleRefresh}
disabled={refreshing}
className="text-[10px] text-violet-400 hover:text-violet-200 bg-violet-900/20 hover:bg-violet-900/40 border border-violet-800/40 rounded px-2 py-1 whitespace-nowrap disabled:opacity-50 transition-colors"
>
{refreshing ? '…' : 'Recalculer'}
</button>
)}
{refreshDone && (
<span className="text-[10px] text-emerald-500 whitespace-nowrap"> Rechargez la page</span>
{/* 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-36 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.score != null && <span className="text-violet-400">{r.score}%</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-0.5">
#{d.event_id} {(d.event_name ?? '').slice(0, 25)} <span className={d.result === 'ok' ? 'text-emerald-600' : 'text-red-500'}>{d.result}</span>
{d.surprise_pct != null && <span className="text-slate-500 ml-1">surp:{d.surprise_pct}</span>}
</div>
))}
</div>
)}
</div>
)}
</div>
)