fix: compute surprise_pct from actual/expected when NULL for auto-analysis scoring
Events with surprise_pct=NULL (bootstrap or older events) got inputs={} → evaluate_graph
returned {} → prediction_json='{}' → all scores showed 'En attente'.
Three-step fallback in _run_auto_analysis:
1. Use event.surprise_pct if set (existing behaviour)
2. Compute from actual_value / expected_value stored in market_events
3. Look up most recent ff_calendar release by currency + date
Frontend diagnostic now shows surprise_pct per event and exposes actual/expected
in the refresh result panel to make the source visible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1182,27 +1182,56 @@ def _run_auto_analysis(event: dict, template_id: int) -> bool:
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
elif src == "surprise_bps" and event.get("surprise_pct") is not None:
|
elif src in ("surprise_bps", "surprise"):
|
||||||
raw = float(event["surprise_pct"])
|
# Step 1: use stored surprise_pct
|
||||||
node_range = cfg.get("range")
|
sp = event.get("surprise_pct")
|
||||||
if node_range and len(node_range) == 2:
|
# Step 2: compute from actual_value / expected_value
|
||||||
lo, hi = float(node_range[0]), float(node_range[1])
|
if sp is None:
|
||||||
inputs[input_id] = round(max(lo, min(hi, raw)), 4)
|
try:
|
||||||
else:
|
act_s = str(event.get("actual_value") or "").replace(",", ".").strip()
|
||||||
inputs[input_id] = round(raw, 4)
|
exp_s = str(event.get("expected_value") or "").replace(",", ".").strip()
|
||||||
|
if act_s and exp_s:
|
||||||
elif src == "surprise" and event.get("surprise_pct") is not None:
|
act_f = float(act_s)
|
||||||
raw = float(event["surprise_pct"])
|
exp_f = float(exp_s)
|
||||||
node_range = cfg.get("range")
|
sp = (act_f - exp_f) / abs(exp_f) * 100 if abs(exp_f) > 1e-9 else (act_f - exp_f)
|
||||||
if node_range and len(node_range) == 2:
|
except (ValueError, TypeError):
|
||||||
lo, hi = float(node_range[0]), float(node_range[1])
|
pass
|
||||||
bound = max(abs(lo), abs(hi))
|
# Step 3: look up ff_calendar by currency + date
|
||||||
if bound <= 10:
|
if sp is None:
|
||||||
inputs[input_id] = round(max(lo, min(hi, raw / 100 * bound)), 4)
|
try:
|
||||||
|
currency = (event.get("sub_type") or "").upper()
|
||||||
|
if currency:
|
||||||
|
row_ff = conn.execute("""
|
||||||
|
SELECT actual_value, forecast_value FROM ff_calendar
|
||||||
|
WHERE currency = ? AND event_date <= ? AND actual_value IS NOT NULL
|
||||||
|
AND forecast_value IS NOT NULL
|
||||||
|
ORDER BY event_date DESC LIMIT 1
|
||||||
|
""", (currency, edate_str)).fetchone()
|
||||||
|
if row_ff:
|
||||||
|
a_f = float(row_ff["actual_value"])
|
||||||
|
e_f = float(row_ff["forecast_value"])
|
||||||
|
sp = (a_f - e_f) / abs(e_f) * 100 if abs(e_f) > 1e-9 else (a_f - e_f)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
if sp is not None:
|
||||||
|
raw = float(sp)
|
||||||
|
node_range = cfg.get("range")
|
||||||
|
if src == "surprise_bps":
|
||||||
|
if node_range and len(node_range) == 2:
|
||||||
|
lo, hi = float(node_range[0]), float(node_range[1])
|
||||||
|
inputs[input_id] = round(max(lo, min(hi, raw)), 4)
|
||||||
|
else:
|
||||||
|
inputs[input_id] = round(raw, 4)
|
||||||
else:
|
else:
|
||||||
inputs[input_id] = round(max(lo, min(hi, raw)), 4)
|
if node_range and len(node_range) == 2:
|
||||||
else:
|
lo, hi = float(node_range[0]), float(node_range[1])
|
||||||
inputs[input_id] = round(raw / 100, 4)
|
bound = max(abs(lo), abs(hi))
|
||||||
|
if bound <= 10:
|
||||||
|
inputs[input_id] = round(max(lo, min(hi, raw / 100 * bound)), 4)
|
||||||
|
else:
|
||||||
|
inputs[input_id] = round(max(lo, min(hi, raw)), 4)
|
||||||
|
else:
|
||||||
|
inputs[input_id] = round(raw / 100, 4)
|
||||||
|
|
||||||
elif src == "impact_score_scaled" and event.get("impact_score") is not None:
|
elif src == "impact_score_scaled" and event.get("impact_score") is not None:
|
||||||
inputs[input_id] = float(event["impact_score"]) / 10.0
|
inputs[input_id] = float(event["impact_score"]) / 10.0
|
||||||
@@ -1240,7 +1269,8 @@ def _run_auto_analysis(event: dict, template_id: int) -> bool:
|
|||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[auto_analysis] event #{event_id} → tmpl #{template_id} | "
|
f"[auto_analysis] event #{event_id} → tmpl #{template_id} | "
|
||||||
f"inputs={inputs} | instruments={all_instruments_csv} | actual_moves={actual_moves}"
|
f"surprise_pct={event.get('surprise_pct')} inputs={inputs} | "
|
||||||
|
f"nodes={len(node_values)} | instruments={all_instruments_csv} | actual_moves={actual_moves}"
|
||||||
)
|
)
|
||||||
|
|
||||||
existing = conn.execute(
|
existing = conn.execute(
|
||||||
@@ -1443,8 +1473,10 @@ def refresh_auto_analyses():
|
|||||||
details.append(detail)
|
details.append(detail)
|
||||||
continue
|
continue
|
||||||
ev = dict(ev_row)
|
ev = dict(ev_row)
|
||||||
detail["surprise_pct"] = ev.get("surprise_pct")
|
detail["surprise_pct"] = ev.get("surprise_pct")
|
||||||
detail["start_date"] = ev.get("start_date")
|
detail["actual_value"] = ev.get("actual_value")
|
||||||
|
detail["expected_value"] = ev.get("expected_value")
|
||||||
|
detail["start_date"] = ev.get("start_date")
|
||||||
ok = _run_auto_analysis(ev, row["template_id"])
|
ok = _run_auto_analysis(ev, row["template_id"])
|
||||||
detail["result"] = "ok" if ok else "failed"
|
detail["result"] = "ok" if ok else "failed"
|
||||||
if ok:
|
if ok:
|
||||||
|
|||||||
@@ -1028,7 +1028,7 @@ function ExplanationScore({
|
|||||||
// 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}[] = []
|
const debugRows: {id?: number; name: string; pred: string; actual: string; score: number | null; reason: string; surprisePct?: number | null}[] = []
|
||||||
|
|
||||||
for (const ev of events) {
|
for (const ev of events) {
|
||||||
if (!ev.template_id) continue
|
if (!ev.template_id) continue
|
||||||
@@ -1062,6 +1062,7 @@ function ExplanationScore({
|
|||||||
pred: hasPred ? '✓' : '✗',
|
pred: hasPred ? '✓' : '✗',
|
||||||
actual: hasActual ? '✓' : '✗',
|
actual: hasActual ? '✓' : '✗',
|
||||||
score: null, reason,
|
score: null, reason,
|
||||||
|
surprisePct: ev.surprise_pct ?? null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const s = comprehensionScore(ev, tmpl, inst)
|
const s = comprehensionScore(ev, tmpl, inst)
|
||||||
@@ -1136,9 +1137,12 @@ function ExplanationScore({
|
|||||||
<div key={i} className="flex gap-2 py-0.5 border-b border-slate-800/40 last:border-0">
|
<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.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={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="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>
|
<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>}
|
{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>}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{debugInfo && (
|
{debugInfo && (
|
||||||
@@ -1149,8 +1153,12 @@ function ExplanationScore({
|
|||||||
}
|
}
|
||||||
{debugInfo.details?.map((d: any, i: number) => (
|
{debugInfo.details?.map((d: any, i: number) => (
|
||||||
<div key={i} className="mt-0.5">
|
<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.event_id} {(d.event_name ?? '').slice(0, 22)} → <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>}
|
{d.surprise_pct != null
|
||||||
|
? <span className="text-cyan-700 ml-1">surp:{Number(d.surprise_pct).toFixed(1)}%</span>
|
||||||
|
: d.actual_value != null
|
||||||
|
? <span className="text-amber-700 ml-1">act:{d.actual_value} exp:{d.expected_value ?? '?'}</span>
|
||||||
|
: <span className="text-red-800 ml-1">surp=null</span>}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user