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