// ── ChainView (Tool10 — Lead-Lag) ──────────────────────────────────────────── // "Quando il leader A sale, i follower B/C tendono a seguire nei giorni dopo." // Esplorativo, NON un backtest: la correlazione lead-lag è associazione // statistica, il movimento del leader può essere già prezzato. function ChainView() { const [chains, setChains] = useState([]); const [date, setDate] = useState(""); const [generatedAt, setGeneratedAt] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [expanded, setExpanded] = useState(null); const [runStatus, setRunStatus] = useState(null); const [showLegend, setShowLegend] = useState(false); const [filterText, setFilterText] = useState(""); const load = async (showSpinner) => { if (showSpinner) setLoading(true); try { const r = await fetch("/api/chain").then(r => r.json()); setChains(r.chains || []); setDate(r.date || ""); setGeneratedAt(r.generated_at || null); setError(r.error || null); } catch (e) { setError(String(e)); } finally { if (showSpinner) setLoading(false); } }; useEffect(() => { load(true); const t = setInterval(() => load(false), 5 * 60 * 1000); return () => clearInterval(t); }, []); const runNow = async () => { setRunStatus("running"); try { await fetch("/api/chain/run-now", { method: "POST" }); // Dopo il lancio, poll ogni 30s finché il timestamp cambia (max 15 min) const prevTs = generatedAt; let attempts = 0; const poll = setInterval(async () => { attempts++; try { const r = await fetch("/api/chain").then(r => r.json()); if (r.generated_at && r.generated_at !== prevTs) { clearInterval(poll); setChains(r.chains || []); setDate(r.date || ""); setGeneratedAt(r.generated_at); setRunStatus("ok"); setTimeout(() => setRunStatus(null), 5000); } } catch {} if (attempts >= 30) { clearInterval(poll); setRunStatus("ok"); setTimeout(() => setRunStatus(null), 3000); } }, 30000); } catch { setRunStatus("err"); } }; const pct = (v) => v == null ? "—" : `${v > 0 ? "+" : ""}${(v * 100).toFixed(2)}%`; const prob = (v) => v == null ? "—" : `${(v * 100).toFixed(0)}%`; const scoreColor = (v) => { if (v == null) return "var(--text-muted)"; if (v >= 70) return "var(--green)"; if (v >= 55) return "var(--amber)"; return "var(--text-muted)"; }; const liftColor = (v) => v == null ? "var(--text-muted)" : (v > 0 ? "var(--green)" : "var(--red)"); // Mini-sparkline della corr per lag 1..5 const Sparkline = ({ byLag, best }) => { if (!byLag) return null; const lags = Object.keys(byLag).sort((a, b) => +a - +b); if (!lags.length) return null; const vals = lags.map(k => byLag[k]); const maxAbs = Math.max(0.05, ...vals.map(Math.abs)); const W = 54, H = 18, bw = W / lags.length; return ( {vals.map((v, i) => { const h = Math.abs(v) / maxAbs * (H/2 - 1); const up = v >= 0; const isBest = (+lags[i]) === best; return ( ); })} ); }; const LEGEND = [ { name: "Leader", desc: "Titolo che si muove per primo." }, { name: "Follower", desc: "Titolo che tende a seguire il leader." }, { name: "Lag", desc: "Ritardo ottimale in giorni di borsa: il follower segue dopo N giorni (best_lag tra 1 e 5)." }, { name: "Corr", desc: "Correlazione ritardata sui RENDIMENTI (non sui prezzi) al lag ottimale. Segno = direzione." }, { name: "Lag chart", desc: "Barre della correlazione per ciascun lag 1→5; la barra evidenziata è il lag ottimale." }, { name: "P(su)", desc: "Probabilità che il follower salga nei giorni successivi quando il leader sale ≥ soglia (default +2%)." }, { name: "Lift", desc: "Extra-rendimento medio del follower rispetto al suo drift naturale (baseline). Metrica chiave: lift>0 = il leader anticipa davvero." }, { name: "Hit", desc: "Frazione di eventi in cui il follower batte la sua baseline (il leader aggiunge segnale)." }, { name: "Granger", desc: "p-value del test di causalità di Granger sui rendimenti. Più basso = relazione statisticamente più solida (gate p≤0.01)." }, { name: "N", desc: "Numero di eventi (giorni in cui il leader è salito sopra soglia) usati per le statistiche." }, { name: "Stab ✓", desc: "Validazione out-of-sample: la relazione regge in ENTRAMBE le metà temporali della storia (stesso segno e magnitudo). ✓ = pattern stabile, non una coincidenza statistica. Passa il mouse per vedere la corr nelle due metà." }, { name: "Score", desc: "Punteggio 0-100 dell'edge: fonde |corr|, P(su), lift e bonus Granger." }, ]; // Filtra per leader o per qualsiasi follower const q = filterText.trim().toUpperCase(); const visibleChains = q ? chains.filter(c => c.leader.includes(q) || (c.followers || []).some(f => f.follower.includes(q)) ).map(c => { // se il filtro matcha solo un follower, espandi automaticamente il leader if (!c.leader.includes(q)) return { ...c, _autoExpand: true }; return c; }) : chains; if (loading) return (
Caricamento chain…
); if (error && !chains.length) return (
Errore: {error}
); if (!chains.length) return (

Nessuno snapshot lead-lag disponibile.

Avvia il primo run (richiede qualche minuto) oppure da CLI con python -m tool10_chain.chain

); const renderChain = (c) => { const isOpen = expanded === c.leader || !!c._forceExpand; const followers = c.followers || []; return ( setExpanded(isOpen ? null : c.leader)} style={{cursor: "pointer"}}> {c.leader} {c.leader_sector || "—"} {c.n_followers} {c.chain_score != null ? c.chain_score.toFixed(0) : "—"} {followers.slice(0, 4).map(f => `${f.follower}(+${f.best_lag}g)`).join(" · ") || "—"} {isOpen && (
{followers.map(f => ( ))}
Follower Lag Corr Lag chart P(su) Lift Hit Granger N Stab Score
{f.follower} {f.follower_sector ? {f.follower_sector} : null} +{f.best_lag}g = 0 ? "var(--green)" : "var(--red)"}}>{f.corr != null ? f.corr.toFixed(2) : "—"} {prob(f.cond_prob_up)} {pct(f.lift)} {prob(f.hit_rate)} {f.granger_p == null ? "—" : f.granger_p < 0.001 ? "<0.001" : f.granger_p.toFixed(3)} {f.sample_size} {f.stable ? : f.stable === false ? : ·} {f.edge_score != null ? f.edge_score.toFixed(0) : "—"}
)}
); }; return (
Snapshot: {date || "—"}
{generatedAt && (
Generato: {generatedAt.replace("T", " ").slice(0, 16)}
)}
setFilterText(e.target.value)} style={{ fontFamily: "var(--font-mono, monospace)", fontSize: 13, padding: "4px 28px 4px 8px", borderRadius: 6, border: "1px solid var(--border)", background: "var(--surface2)", color: "var(--text)", width: 140, outline: "none", }} /> {filterText && ( )}
{q && ( {visibleChains.length} risultat{visibleChains.length === 1 ? "o" : "i"} )}
{showLegend && ( <>
setShowLegend(false)} style={{position:"fixed", inset:0, zIndex:199}}/>
Legenda
{LEGEND.map(c => (
{c.name} {c.desc}
))}
)}
Come leggerlo: ogni riga è un leader; espandila per vedere i follower che storicamente salgono N giorni dopo. Correlazioni sui rendimenti (non sui prezzi), filtrate per causalità di Granger e validate out-of-sample (✓ Stab = il pattern regge in entrambe le metà della storia, quindi non è una coincidenza). Strumento esplorativo, non un backtest tradabile.
{visibleChains.length === 0 && q ? ( ) : visibleChains.map(c => renderChain({ ...c, // auto-espandi se il filtro ha trovato solo un follower (non il leader) _forceExpand: c._autoExpand }))}
Leader Settore #Foll Score Follower (lag)
Nessuna chain trovata per «{q}»
); } window.ChainView = ChainView;