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():
|
def refresh_auto_analyses():
|
||||||
"""
|
"""
|
||||||
Re-run _run_auto_analysis for all causal_event_analyses rows with empty actual_json.
|
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:
|
try:
|
||||||
from services.database import get_conn
|
from services.database import get_conn
|
||||||
conn = 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,
|
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
|
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()
|
""").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()
|
conn.close()
|
||||||
|
|
||||||
|
details = []
|
||||||
refreshed, failed = 0, 0
|
refreshed, failed = 0, 0
|
||||||
for row in rows:
|
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:
|
try:
|
||||||
conn2 = get_conn()
|
conn2 = get_conn()
|
||||||
ev_row = conn2.execute(
|
ev_row = conn2.execute(
|
||||||
@@ -1413,18 +1439,33 @@ def refresh_auto_analyses():
|
|||||||
).fetchone()
|
).fetchone()
|
||||||
conn2.close()
|
conn2.close()
|
||||||
if not ev_row:
|
if not ev_row:
|
||||||
|
detail["result"] = "event_not_found"
|
||||||
|
details.append(detail)
|
||||||
continue
|
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:
|
if ok:
|
||||||
refreshed += 1
|
refreshed += 1
|
||||||
else:
|
else:
|
||||||
failed += 1
|
failed += 1
|
||||||
|
detail["result"] = "run_returned_false"
|
||||||
except Exception as _e:
|
except Exception as _e:
|
||||||
logger.warning(f"[refresh_auto_analyses] cea#{row['cea_id']}: {_e}")
|
logger.warning(f"[refresh_auto_analyses] cea#{row['cea_id']}: {_e}")
|
||||||
|
detail["result"] = f"exception: {_e}"
|
||||||
failed += 1
|
failed += 1
|
||||||
|
details.append(detail)
|
||||||
|
|
||||||
logger.info(f"[refresh_auto_analyses] done: {refreshed} refreshed, {failed} failed")
|
print(f"[refresh] done: {refreshed} refreshed, {failed} failed", flush=True)
|
||||||
return {"refreshed": refreshed, "failed": failed, "total": len(rows)}
|
return {
|
||||||
|
"refreshed": refreshed,
|
||||||
|
"failed": failed,
|
||||||
|
"total": len(rows),
|
||||||
|
"db_state": db_state,
|
||||||
|
"details": details,
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[refresh_auto_analyses] {e}")
|
logger.error(f"[refresh_auto_analyses] {e}")
|
||||||
raise HTTPException(500, str(e))
|
raise HTTPException(500, str(e))
|
||||||
|
|||||||
@@ -1023,10 +1023,12 @@ function ExplanationScore({
|
|||||||
}) {
|
}) {
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
const [refreshDone, setRefreshDone] = 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
|
// 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}[] = []
|
||||||
|
|
||||||
for (const ev of events) {
|
for (const ev of events) {
|
||||||
if (!ev.template_id) continue
|
if (!ev.template_id) continue
|
||||||
@@ -1035,7 +1037,35 @@ function ExplanationScore({
|
|||||||
const inst = causalInsts.find(ci => tmpl.instruments.includes(ci))
|
const inst = causalInsts.find(ci => tmpl.instruments.includes(ci))
|
||||||
if (!inst) continue
|
if (!inst) continue
|
||||||
totalLinked.push(ev.id ?? 0)
|
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)
|
const s = comprehensionScore(ev, tmpl, inst)
|
||||||
|
if (debugRows.length > 0) debugRows[debugRows.length - 1].score = s
|
||||||
if (s != null) scored.push(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'
|
: globalScore >= 70 ? 'bg-emerald-500' : globalScore >= 40 ? 'bg-amber-500' : 'bg-red-500'
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
|
setDebugInfo(null)
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
try {
|
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)
|
setRefreshDone(true)
|
||||||
onRefreshDone?.()
|
onRefreshDone?.()
|
||||||
setTimeout(() => setRefreshDone(false), 3000)
|
setTimeout(() => setRefreshDone(false), 8000)
|
||||||
|
} catch (err: any) {
|
||||||
|
setDebugInfo({ error: String(err) })
|
||||||
} finally {
|
} finally {
|
||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
}
|
}
|
||||||
@@ -1070,28 +1105,57 @@ function ExplanationScore({
|
|||||||
const needsRefresh = totalLinked.length > 0 && scored.length === 0 && !refreshDone
|
const needsRefresh = totalLinked.length > 0 && scored.length === 0 && !refreshDone
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3 px-4 py-2.5 rounded-xl border border-slate-700/30 bg-dark-800/50">
|
<div className="space-y-2">
|
||||||
<span className="text-xs text-slate-500 whitespace-nowrap">Note globale</span>
|
<div className="flex items-center gap-3 px-4 py-2.5 rounded-xl border border-slate-700/30 bg-dark-800/50">
|
||||||
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
|
<span className="text-xs text-slate-500 whitespace-nowrap">Note globale</span>
|
||||||
<div className={clsx('h-full rounded-full transition-all', barCls)} style={{ width: `${globalScore ?? 0}%` }} />
|
<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>
|
</div>
|
||||||
<span className={clsx('text-xs font-semibold px-2 py-1 rounded-lg border whitespace-nowrap', badgeCls)}>
|
|
||||||
{globalScore != null ? `${globalScore}% — ` : ''}{verdict}
|
{/* Debug panel — score diagnostics per event */}
|
||||||
</span>
|
{debugRows.length > 0 && scored.length === 0 && (
|
||||||
<span className="text-[10px] text-slate-600 whitespace-nowrap">
|
<div className="rounded-lg border border-slate-700/30 bg-dark-900/60 p-3 text-[10px] font-mono">
|
||||||
{scored.length}/{totalLinked.length} graphes
|
<div className="text-slate-500 mb-1.5 uppercase tracking-wide">Diagnostic scores ({debugRows.length} graphes)</div>
|
||||||
</span>
|
{debugRows.map((r, i) => (
|
||||||
{needsRefresh && (
|
<div key={i} className="flex gap-2 py-0.5 border-b border-slate-800/40 last:border-0">
|
||||||
<button
|
<span className={clsx('shrink-0 w-4', r.pred === '✓' ? 'text-emerald-600' : 'text-red-600')}>{r.pred}</span>
|
||||||
onClick={handleRefresh}
|
<span className={clsx('shrink-0 w-4', r.actual === '✓' ? 'text-emerald-600' : 'text-red-600')}>{r.actual}</span>
|
||||||
disabled={refreshing}
|
<span className="text-slate-400 w-36 shrink-0 truncate" title={r.name}>{r.name}</span>
|
||||||
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"
|
<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>}
|
||||||
{refreshing ? '…' : 'Recalculer'}
|
</div>
|
||||||
</button>
|
))}
|
||||||
)}
|
{debugInfo && (
|
||||||
{refreshDone && (
|
<div className="mt-2 pt-2 border-t border-slate-800/40 text-slate-600">
|
||||||
<span className="text-[10px] text-emerald-500 whitespace-nowrap">✓ Rechargez la page</span>
|
{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>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user