// ── AccuracyView ─────────────────────────────────────────────────────────────
// Accuratezza delle previsioni (misurata su outcome già verificati vs prezzo
// reale) + pannello salute dati. Cuore della validazione: niente è "affidabile"
// finché non è misurato.
// Legenda numero tool → nome (Tool2 rimosso: i suoi output li produce Tool3).
const TOOL_LEGEND = [
{ n: "Tool0", name: "Rischio", desc: "regime di rischio di mercato (allerta crolli)" },
{ n: "Tool1", name: "Collector", desc: "scarica i prezzi (OHLCV)" },
{ n: "Tool3", name: "Tecnico", desc: "analisi tecnica → segnale, forecast, target/stop" },
{ n: "Tool4", name: "News", desc: "news + sentiment → segnale combinato" },
{ n: "Tool5", name: "Calendario", desc: "eventi finanziari / earnings" },
{ n: "Tool6", name: "Telegram", desc: "notifiche" },
{ n: "Tool7", name: "Settori", desc: "rotazione settoriale (11 settori SPDR)" },
{ n: "Tool8", name: "Pre-market", desc: "gap / momentum pre-apertura" },
{ n: "Tool9", name: "Top Picks", desc: "scoperta titoli promettenti" },
{ n: "Tool10", name: "Chain", desc: "catene lead-lag (A sale → B segue)" },
{ n: "Tool11", name: "Simulatore", desc: "paper-trading + Portfolio Manager AI" },
];
function AccuracyView() {
const [acc, setAcc] = useState(null);
const [health, setHealth] = useState(null);
const [sim, setSim] = useState(null);
const [pm, setPm] = useState(null);
const [sc, setSc] = useState(null); // pagella (admin-only; 401 → null)
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [help, setHelp] = useState(false);
const [win, setWin] = useState(30); // finestra previsioni in giorni (0 = tutto)
const load = async (spin) => {
if (spin) setLoading(true);
try {
const [a, h, s, p, scd] = await Promise.all([
fetch("/api/accuracy?days=" + (win || 0)).then(r => r.json()),
fetch("/api/health").then(r => r.json()),
fetch("/api/simulator").then(r => r.json()).catch(() => null),
fetch("/api/pm").then(r => r.json()).catch(() => null),
fetch("/api/scorecard?days=" + (win || 0)).then(r => r.json()).catch(() => null),
]);
setAcc(a || {}); setHealth(h.health || []); setSim(s); setPm(p);
setSc(scd && scd.tools ? scd : null);
setError(a?.error || h?.error || null);
} catch (e) { setError(String(e)); }
finally { if (spin) setLoading(false); }
};
useEffect(() => {
load(true);
const t = setInterval(() => load(false), 5 * 60 * 1000);
return () => clearInterval(t);
}, [win]);
const HORIZONS = ["1h", "3h", "6h", "24h"];
const num = (v, d = 2) => (v == null ? "—" : (v >= 0 && d ? "" : "") + v.toFixed(d));
const dirColor = (v) => v == null ? "var(--text-muted)" : v >= 55 ? "var(--green)" : v >= 50 ? "var(--amber)" : "var(--red)";
const biasColor = (v) => v == null ? "var(--text-muted)" : Math.abs(v) <= 0.3 ? "var(--green)" : Math.abs(v) <= 0.8 ? "var(--amber)" : "var(--red)";
const totalN = (stats) => HORIZONS.reduce((s, h) => s + ((stats?.[h]?.n) || 0), 0);
const ForecastCard = ({ title, color, stats }) => {
const n = totalN(stats);
return (
{title}
{n} outcome
{n === 0 ? (
Nessun outcome ancora. Maturano ~25h dopo ogni previsione (poi compaiono qui).
) : (
Oriz.
n
Dir.Acc
MBE
MAE
RMSE
{HORIZONS.map(h => {
const s = stats?.[h] || {};
return (
{h}
{s.n || 0}
{s.dir_acc == null ? "—" : s.dir_acc + "%"}
{s.mbe == null ? "—" : (s.mbe >= 0 ? "+" : "") + s.mbe}
{num(s.mae)}
{num(s.rmse)}
);
})}
)}
);
};
const MiniStat = ({ title, children, hint }) => (
);
const t9 = acc?.tool9 || {};
const t8 = acc?.tool8 || {};
const t10 = acc?.tool10 || {};
const signed = (v, suf = "%") => v == null ? "—" : `${v >= 0 ? "+" : ""}${v}${suf}`;
const greenRed = (v) => v == null ? "var(--text-muted)" : v >= 0 ? "var(--green)" : "var(--red)";
return (
Accuratezza previsioni
setHelp(h => !h)} title="Legenda: numero tool → nome"
style={help ? { color: "var(--accent)" } : {}}>
load(true)} title="Ricarica">
Aggiorna
Misurata su previsioni già verificate vs prezzo reale. Direction-accuracy > 55% = segnale; MBE ≈ bias sistematico.
{/* Finestra temporale: di default RECENTE (non a vita) così riflette lo stato
attuale dei modelli dopo i miglioramenti settimanali. */}
Finestra previsioni (Tool3/4):
{[{ v: 7, l: "7 giorni" }, { v: 30, l: "30 giorni" }, { v: 0, l: "Tutto" }].map(o => (
setWin(o.v)}
style={win === o.v ? { borderColor: "var(--accent)", color: "var(--accent)" } : {}}>{o.l}
))}
{win ? `· solo le previsioni degli ultimi ${win} giorni` : "· media di sempre (diluisce i miglioramenti)"}
{help && (
Legenda tool — numero → nome
setHelp(false)}>Chiudi
{TOOL_LEGEND.map(t => (
{t.n}
{t.name}
{t.desc}
))}
Tool2 è stato rimosso: i suoi output (target/stop/probabilità) ora li produce il Tool3 .
)}
{loading && !acc &&
Caricamento…
}
{error &&
Errore: {error}
}
{acc && <>
{/* Pagella tool — costo vs valore vs soldi (admin-only) */}
{sc && sc.tools && sc.tools.length > 0 && (() => {
const VLABEL = {
earns_its_keep: { t: "Si ripaga", c: "var(--green)" },
costs_no_value: { t: "Costa, 0 valore", c: "var(--red)" },
unproven: { t: "Da dimostrare", c: "var(--amber)" },
free_informative: { t: "Gratis · utile", c: "#6ea8fe" },
free_low_value: { t: "Gratis · poco valore", c: "var(--text-muted)" },
};
return (
Pagella tool — costo vs valore vs soldi
“mi fa guadagnare?”
Money = proxy di triage (long-only, una posizione per volta, niente concorrenza/slippage) → sovrastima : serve solo a capire quali tool meritano un backtest serio. Costo per trade ~{sc.assumptions?.cost_per_trade_pp}pp.
Tool
€/mese
Valore
Money net% (n)
Verdetto
{sc.tools.map(t => {
const v = t.value || {}, m = t.money || {};
const val = v.dir_acc != null ? v.dir_acc + "%" : (v.alpha != null ? signed(v.alpha) : "—");
const valCol = v.dir_acc != null ? dirColor(v.dir_acc) : greenRed(v.alpha);
const vlab = VLABEL[t.verdict] || { t: t.verdict, c: "var(--text-muted)" };
return (
{t.label}
0 ? "var(--text)" : "var(--text-faint)" }}>
{t.cost_month_eur > 0 ? "€" + t.cost_month_eur.toFixed(2) : "—"}
{val}
{m.net_ret_pct == null ? "—" : signed(m.net_ret_pct) + (m.n_signals ? ` (${m.n_signals})` : "")}
{vlab.t}
);
})}
);
})()}
{/* Forecast Tool3 / Tool4 */}
{/* Discovery / pre-market / chain */}
{t9.n_validated ? (
{signed(t9.avg_alpha)}
{t9.n_validated} pick · hit {t9.hit_rate ?? "—"}%
) : In maturazione (30gg)
}
{t8.n ? (
{t8.hit_rate}%
{t8.n} verdetti
) : Nessun dato
}
{t10.n ? (
{t10.pct_stable}%
{t10.n} edge · score medio {t10.avg_edge ?? "—"}
) : Nessun dato
}
{/* Paper-trading — il "bottom line": quanto rendono davvero le decisioni */}
Paper-trading (settimana)
denaro fittizio
{sim?.active && sim?.summary ? (
{signed(sim.summary.return_pct)}
Δ buy&hold {signed((sim.summary.return_pct || 0) - (sim.summary.bh_return_pct || 0))}
) : Non attivo
}
{pm?.active && pm?.summary ? (
{signed(pm.summary.return_pct)}
Δ b&h {signed((pm.summary.return_pct || 0) - (pm.summary.bh_return_pct || 0))} · {pm.summary.n_pos || 0} pos
) : Parte lunedì
}
{pm?.active && pm?.summary ? (
{signed((pm.summary.return_pct || 0) - (pm.summary.spy_return_pct || 0))}
) : —
}
{/* Salute dati */}
Salute dati
{(health || []).map(h => (
{h.label}
{h.age_min == null ? "nessun dato" : h.age_min < 90 ? `${h.age_min} min fa` : `${(h.age_min / 60).toFixed(1)}h fa`}
{h.rows_today == null ? "" : `${h.rows_today} righe oggi`}
{(h.anomalies || []).map((a, i) => (
{a}
))}
{h.ok && ok }
))}
>}
);
}
window.AccuracyView = AccuracyView;