// ── SimulatorView (Tool11 — paper-trading settimanale) ─────────────────────── // Denaro FITTIZIO. Ogni lunedì riparte: €500/ticker, trading attivo Lun→Ven seguendo // Tool4 (primario). Forward-test, NON consulenza. Confronto vs buy-and-hold e SPY. function SimNavChart({ nav }) { const ref = useRef(null); useEffect(() => { if (!ref.current || !window.LightweightCharts || !nav || nav.length < 1) return; const el = ref.current; let chart; try { chart = LightweightCharts.createChart(el, { width: Math.max(100, el.clientWidth), height: Math.max(100, el.clientHeight), layout: { background: { color: "transparent" }, textColor: "#8892a4", fontFamily: "JetBrains Mono, monospace", fontSize: 11 }, grid: { vertLines: { color: "rgba(255,255,255,0.035)" }, horzLines: { color: "rgba(255,255,255,0.05)" } }, rightPriceScale: { borderVisible: false, scaleMargins: { top: 0.2, bottom: 0.12 } }, timeScale: { borderVisible: false, timeVisible: true, secondsVisible: false }, crosshair: { mode: LightweightCharts.CrosshairMode.Magnet, vertLine: { color: "#3a4252", width: 1, labelBackgroundColor: "#1c2230" }, horzLine: { color: "#3a4252", width: 1, labelBackgroundColor: "#1c2230" }, }, localization: { priceFormatter: (p) => "€" + Math.round(p) }, }); } catch (e) { console.warn("sim chart init", e); return; } const toPts = (key) => { const seen = new Set(); const out = []; for (const p of nav) { if (!p.timestamp || p[key] == null) continue; // Il backend invia GIÀ l'ora italiana (Europe/Rome). La tratto come fissa: // append "Z" → l'epoch è quel wall-clock interpretato come UTC, e // LightweightCharts (che rende in UTC) mostra esattamente l'ora di Roma. // Indipendente dal fuso del browser. Vedi CONVENZIONE FUSO ORARIO in CLAUDE.md. const t = Math.floor(new Date(p.timestamp + "Z").getTime() / 1000); if (!t || isNaN(t) || seen.has(t)) continue; seen.add(t); out.push({ time: t, value: p[key] }); } return out; }; // Strategia come AREA con riempimento sfumato → molto più leggibile di una linea sottile. const sBh = chart.addLineSeries({ color: "#8892a4", lineWidth: 1, lineStyle: LightweightCharts.LineStyle.Dashed, priceLineVisible: false, lastValueVisible: true, title: "Buy&Hold" }); const sSpy = chart.addLineSeries({ color: "#6ea8fe", lineWidth: 1, lineStyle: LightweightCharts.LineStyle.Dotted, priceLineVisible: false, lastValueVisible: true, title: "SPY" }); const sStrat = chart.addAreaSeries({ lineColor: "#22c55e", lineWidth: 2, topColor: "rgba(34,197,94,0.30)", bottomColor: "rgba(34,197,94,0.01)", priceLineVisible: false, lastValueVisible: true, title: "Strategia", crosshairMarkerRadius: 3, }); sBh.setData(toPts("bh_nav")); sSpy.setData(toPts("spy_nav")); sStrat.setData(toPts("nav")); chart.timeScale().fitContent(); const ro = new ResizeObserver(() => chart.applyOptions({ width: el.clientWidth, height: el.clientHeight })); ro.observe(el); return () => { ro.disconnect(); chart.remove(); }; }, [nav]); return
; } // Donut SVG (no librerie). slices: [{label, value>=0, color, display}]. function SimDonut({ slices, size = 158, thickness = 30, center, centerSub, centerColor }) { const pos = (slices || []).filter(s => s.value > 0); const total = pos.reduce((a, s) => a + s.value, 0); const r = size / 2, ir = r - thickness; const cx = (txt, color, dy, fs, fw) => txt ? ( {txt} ) : null; if (total <= 0) { // Nessuna fetta (tutto a 0): mostra comunque un anello grigio col valore al // centro — coerente con l'altra torta, invece del solo trattino. return ( {cx(center, "var(--text-muted)", -1, 15, 700)} {cx(centerSub, "var(--text-faint)", 15, 10, 400)} ); } let body; if (pos.length === 1) { body = ; } else { let acc = 0; body = pos.map((s, i) => { const f = s.value / total; const a0 = acc * 2 * Math.PI - Math.PI / 2; acc += f; const a1 = acc * 2 * Math.PI - Math.PI / 2; const large = f > 0.5 ? 1 : 0; const x0 = r + r * Math.cos(a0), y0 = r + r * Math.sin(a0); const x1 = r + r * Math.cos(a1), y1 = r + r * Math.sin(a1); const xi1 = r + ir * Math.cos(a1), yi1 = r + ir * Math.sin(a1); const xi0 = r + ir * Math.cos(a0), yi0 = r + ir * Math.sin(a0); const d = `M ${x0} ${y0} A ${r} ${r} 0 ${large} 1 ${x1} ${y1} L ${xi1} ${yi1} A ${ir} ${ir} 0 ${large} 0 ${xi0} ${yi0} Z`; return {s.label}: {s.display}; }); } return ( {body} {cx(center, centerColor || "var(--text)", -1, 15, 700)} {cx(centerSub, "var(--text-muted)", 15, 10, 400)} ); } function SimLegend({ items }) { return (
{(items || []).filter(x => x.value > 0).map((x, i) => (
{x.label} {x.display}
))}
); } // Grafico a barre: per ogni ticker investito (blu) e guadagno (verde su, rosso giù). // STESSA scala per entrambi (magnitudini corrette): l'investito è grande, il guadagno // Due barre per ticker su SCALE DIVERSE (investito su €budget, guadagno su max|guadagno|) // così il guadagno — piccolo in assoluto — resta visibile. Entrambe vanno verso l'ALTO; // il guadagno è verde, la perdita rossa (il segno è dato dal colore e dall'etichetta). // Larghezza piena: misura il contenitore e distribuisce le colonne su tutta la larghezza. function SimBars({ positions, budget }) { const ref = useRef(null); const [w, setW] = useState(700); useEffect(() => { if (!ref.current) return; const upd = () => { if (ref.current) setW(ref.current.clientWidth || 700); }; upd(); const ro = new ResizeObserver(upd); ro.observe(ref.current); return () => ro.disconnect(); }, []); if (!positions || !positions.length) return
; const data = positions.map(p => ({ sym: p.symbol, inv: Math.max(0, p.invested || 0), gain: (p.value || 0) - (p.cost_basis != null ? p.cost_basis : budget) })); const maxInv = Math.max(1, budget, ...data.map(d => d.inv)); const maxGain = Math.max(0.01, ...data.map(d => Math.abs(d.gain))); const H = 160, top = 18, base = top + H, n = data.length; const colW = w / n; // colonne distribuite su TUTTA la larghezza const barW = Math.max(8, Math.min(22, colW * 0.26)), gap = Math.max(4, colW * 0.06); return (
{data.map((d, i) => { const cx = i * colW + colW / 2; const x1 = cx - barW - gap / 2, x2 = cx + gap / 2; const invH = (d.inv / maxInv) * H; const gH = (Math.abs(d.gain) / maxGain) * H; const up = d.gain >= 0; const gc = up ? "#3fb950" : "#f0556b"; return ( {d.sym} · investito €{d.inv.toFixed(0)} {d.sym} · {up ? "+" : ""}€{d.gain.toFixed(1)} {d.inv.toFixed(0)} {up ? "+" : ""}{d.gain.toFixed(1)} {d.sym} ); })}
); } // Formatta il timestamp dell'ultima valutazione. `iso` è GIÀ ora italiana (Europe/Rome, // convertita dal backend) → uso i componenti senza riconversione del fuso del browser. function _simFmtEval(iso) { if (!iso) return "—"; const m = String(iso).match(/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/); if (!m) return String(iso); const [, y, mo, d, hh, mm] = m; const now = new Date(); const today = now.getFullYear() === +y && (now.getMonth() + 1) === +mo && now.getDate() === +d; return today ? `oggi alle ${hh}:${mm}` : `${d}/${mo} alle ${hh}:${mm}`; } // Scheda Portfolio Manager LLM (sleeve separata) — stesso stile ricco del simulatore function PMPanel({ onBack }) { const [d, setD] = useState(null); const [err, setErr] = useState(null); const [isMobile, setIsMobile] = useState(typeof window !== "undefined" && window.innerWidth < 760); useEffect(() => { const onR = () => setIsMobile(window.innerWidth < 760); window.addEventListener("resize", onR); return () => window.removeEventListener("resize", onR); }, []); const load = () => fetch("/api/pm").then(r => r.json()).then(j => { setD(j); setErr(j?.error || null); }).catch(e => setErr(String(e))); useEffect(() => { load(); const t = setInterval(load, 60 * 1000); return () => clearInterval(t); }, []); const pct = (v) => v == null ? "—" : `${v >= 0 ? "+" : ""}${v.toFixed(2)}%`; const pc = (v) => v == null ? "var(--text-muted)" : v >= 0 ? "var(--green)" : "var(--red)"; const PALETTE = ["#6ea8fe", "#56cfe1", "#9d8df1", "#5fd0ac", "#7aa2f7", "#c08cf0", "#74c0fc", "#80b8a0", "#a3b1cf", "#88d3c0"]; const s = d?.summary || {}; const cash = d?.cash || 0; const dec = d?.decisions || []; const wl = d?.watchlist || []; const rawPos = d?.positions || []; // Universo = watchlist seguita (così torte/barre mostrano ANCHE le aziende a zero); // fallback alle sole posizioni se la watchlist non è disponibile. const universe = wl.length ? wl.map(w => w.symbol) : rawPos.map(p => p.symbol); const colorOf = {}; universe.forEach((sym, i) => { colorOf[sym] = PALETTE[i % PALETTE.length]; }); const posBySym = {}; rawPos.forEach(p => { posBySym[p.symbol] = { ...p, _cost: (p.shares || 0) * (p.avg_cost || 0) }; }); const positions = universe.map(sym => posBySym[sym]).filter(Boolean); // solo quelle in posizione (tabella) const heldSet = new Set(rawPos.map(p => p.symbol)); const allocSlices = universe.map(sym => { const v = posBySym[sym] ? (posBySym[sym].value || 0) : 0; return { label: sym, value: Math.max(0, v), color: colorOf[sym], display: "$" + v.toFixed(0) }; }); if (cash > 0) allocSlices.push({ label: "Cash", value: cash, color: "#2d3340", display: "$" + cash.toFixed(0) }); const gainSlices = universe.map(sym => { const p = posBySym[sym]; const g = p ? ((p.value || 0) - p._cost) : 0; return { label: sym, value: Math.abs(g), color: colorOf[sym], display: (g >= 0 ? "+" : "-") + "$" + Math.abs(g).toFixed(1), _g: g }; }); const netGain = rawPos.reduce((a, p) => a + ((p.value || 0) - (p.shares || 0) * (p.avg_cost || 0)), 0); const barPos = universe.map(sym => { const p = posBySym[sym]; return { symbol: sym, invested: p ? p._cost : 0, value: p ? (p.value || 0) : 0, cost_basis: p ? p._cost : 0 }; }); // Legenda che mostra ANCHE le voci a zero (dimmate); per i guadagni colora per segno. const fullLegend = (items, signed) => (
{items.map((x, i) => (
0 ? 1 : 0.42 }}> {x.label} = 0 ? "var(--green)" : "var(--red)") : "var(--text-muted)" }}>{x.display}
))}
); return (

Portfolio Manager AI

denaro fittizio · paper
Trader AI a cassa unica $10k: decide come una persona usando tutti i tool, sizing per rischio (1%/trade), stop/target. Si attiva solo sugli eventi. Confronto vs comprare-e-tenere e SPY.
{d?.active && d?.last_run && (
Ultima esecuzione: {_simFmtEval(d.last_run)} · gira ad ogni ciclo a mercato aperto
)} {err &&
Errore: {err}
} {d && !d.active && (
Il Portfolio Manager non ha ancora una settimana attiva — parte al reset di lunedì e inizia a tradare con dati reali.
)} {d?.active && <> {/* Summary */}
NAV · settimana
{pct(s.return_pct)}
${(s.nav || 0).toFixed(0)} su ${(d.capital || 0).toFixed(0)} · {s.n_pos || 0} pos · {s.n_trades || 0} trade
vs Buy&Hold
{pct(s.bh_return_pct)}
Δ {pct((s.return_pct || 0) - (s.bh_return_pct || 0))}
vs SPY
{pct(s.spy_return_pct)}
cash ${(cash).toFixed(0)}
{/* Torte */}
Capitale per posizione
{fullLegend(allocSlices)}
Guadagni per posizione
= 0 ? "+" : "-") + "$" + Math.abs(netGain).toFixed(0)} centerSub="netto" centerColor={netGain >= 0 ? "#3fb950" : "#f0556b"} /> {fullLegend(gainSlices, true)}
{/* Curva NAV */}

Andamento del valore

— PM · — compra-e-tieni · — SPY
{(d.nav && d.nav.length > 1) ? :
Curva in costruzione (servono più punti durante la settimana).
}
{/* Barre */} {positions.length > 0 && (
Investito e guadagno per posizione
▮ investito · ▮ guadagno / perdita · valore sopra ogni barra
)} {/* Posizioni */}

Posizioni aperte

{positions.length === 0 ? (
Nessuna posizione aperta.
) : isMobile ? (
{positions.map(p => (
{p.symbol}
{p.shares} az @ ${p.avg_cost} · stop {p.stop} · tgt {p.target}
${(p.value || 0).toFixed(0)}
{pct(p.pnl_pct)}
))}
) : (
{positions.map(p => ( ))}
Ticker Azioni Costo Prezzo Valore P&L Stop Target
{p.symbol} {p.shares} ${p.avg_cost} ${p.last_price} ${(p.value || 0).toFixed(0)} {pct(p.pnl_pct)} {p.stop} {p.target}
)} {/* Watchlist dinamica — aziende analizzate */}

Aziende seguite dal PM ({wl.length})

Universo analizzato questa settimana. ● in posizione ·{" "} 🃏 Top Pick = subentrata da Tool9 in settimana.
{wl.length === 0 ? : wl.map(w => { const held = heldSet.has(w.symbol); const pick = w.source === "tool9"; return ( {held && } {w.symbol} {pick && 🃏 Top Pick} ); })}
{/* Settimane passate (chiuse) — storico pm_weeks */}

Settimane passate

{(!d?.weeks || d.weeks.length === 0) ?
Nessuna settimana finalizzata ancora.
:
{d.weeks.map((w, i) => ( ))}
Settimana Valore Rendimento vs B&H vs SPY Trade
{w.week_start} {w.end_value != null ? "$" + w.end_value.toFixed(0) : "—"} {pct(w.return_pct)} {pct(w.bh_return_pct)} {pct(w.spy_return_pct)} {w.n_trades != null ? w.n_trades : "—"}
} {/* Decisioni / reasoning */}

Decisioni del PM (reasoning)

{dec.length === 0 ?
Nessuna decisione registrata.
: dec.map((x, i) => (
{x.symbol} · {x.action} · {x.event} · {(x.timestamp || "").slice(5, 16).replace("T", " ")}
{x.reasoning}
))} }
); } function SimulatorView() { const [mode, setMode] = useState("strategy"); const [data, setData] = useState(null); const [hist, setHist] = useState({ weeks: [] }); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [status, setStatus] = useState(null); // null | running | ok | err const [openWeek, setOpenWeek] = useState(null); const [detail, setDetail] = useState([]); const [updatedAt, setUpdatedAt] = useState(null); const [help, setHelp] = useState(false); const [isMobile, setIsMobile] = useState(typeof window !== "undefined" && window.innerWidth < 760); useEffect(() => { const onR = () => setIsMobile(window.innerWidth < 760); window.addEventListener("resize", onR); return () => window.removeEventListener("resize", onR); }, []); const load = async (spin) => { if (spin) setLoading(true); try { const [d, h] = await Promise.all([ fetch("/api/simulator").then(r => r.json()), fetch("/api/simulator/history").then(r => r.json()), ]); setData(d); setHist(h || { weeks: [] }); setError(d?.error || null); setUpdatedAt(new Date()); } catch (e) { setError(String(e)); } finally { if (spin) setLoading(false); } }; useEffect(() => { load(true); const t = setInterval(() => load(false), 60 * 1000); // auto-refresh ogni 60s return () => clearInterval(t); }, []); const act = async (path) => { setStatus("running"); try { await fetch(path, { method: "POST" }); // Il trade/reset gira in background sul server: ricarico a 2s e di nuovo a 6s // (così vedi l'esito senza F5, anche se il ciclo è un po' lento). setTimeout(() => load(false), 2000); setTimeout(() => { load(false); setStatus("ok"); setTimeout(() => setStatus(null), 3500); }, 6000); } catch { setStatus("err"); setTimeout(() => setStatus(null), 3000); } }; const openDetail = async (wk) => { if (openWeek === wk) { setOpenWeek(null); return; } setOpenWeek(wk); try { const h = await fetch("/api/simulator/history?week=" + encodeURIComponent(wk)).then(r => r.json()); setDetail(h.detail || []); } catch { setDetail([]); } }; const pctC = (v) => v == null ? "var(--text-muted)" : v >= 0 ? "var(--green)" : "var(--red)"; const pct = (v) => v == null ? "—" : `${v >= 0 ? "+" : ""}${v.toFixed(2)}%`; const sigColor = (s) => { if (!s) return "var(--text-muted)"; if (s.includes("BUY")) return "var(--green)"; if (s.includes("SELL")) return "var(--red)"; return "var(--amber)"; }; const s = data?.summary || {}; const positions = data?.positions || []; const trades = data?.trades || []; // Torte: (1) capitale per azione + cash da investire, (2) guadagni per azione const budget = data?.budget || 500; // Palette armoniosa (toni blu/teal/viola in linea con l'app), non arcobaleno. const PALETTE = ["#6ea8fe", "#56cfe1", "#9d8df1", "#5fd0ac", "#7aa2f7", "#c08cf0", "#74c0fc", "#80b8a0", "#a3b1cf", "#88d3c0"]; const allocSlices = positions.map((p, i) => ({ label: p.symbol, value: Math.max(0, p.invested || 0), color: PALETTE[i % PALETTE.length], display: "€" + (p.invested || 0).toFixed(0), })); const totalCash = positions.reduce((a, p) => a + (p.cash || 0), 0); if (totalCash > 0) allocSlices.push({ label: "Da investire", value: totalCash, color: "#2d3340", display: "€" + totalCash.toFixed(0) }); // Stessi colori per-ticker della torta "capitale" (così DELL e MU si distinguono); // il segno guadagno/perdita è dato dal +/- nelle etichette e dal valore netto al centro. const gainSlices = positions.map((p, i) => { const g = (p.value || 0) - budget; return { label: p.symbol, value: Math.abs(g), color: PALETTE[i % PALETTE.length], display: (g >= 0 ? "+" : "-") + "€" + Math.abs(g).toFixed(1) }; }); const netGain = positions.reduce((a, p) => a + ((p.value || 0) - budget), 0); if (mode === "pm") return setMode("strategy")} />; return (

Simulatore

denaro fittizio · non è consulenza
{status === "running" && eseguo…} {status === "ok" && ✓ aggiornato} {updatedAt && status !== "running" && status !== "ok" && ( agg. {updatedAt.toLocaleTimeString("it-IT")} )}
Ogni lunedì riparte: €{data?.budget || 500}/ticker, trading attivo Lun→Ven seguendo Tool4. Confronto vs comprare-e-tenere e SPY.
{data?.active && data?.last_eval && (
Ultima valutazione: {_simFmtEval(data.last_eval)} · nessun trade se i segnali sono HOLD
)} {help && (
Guida: cosa stai guardando

Cos'è. Un gioco a soldi finti: ogni lunedì parti con €{data?.budget || 500} per ciascuna azione della tua watchlist, e durante la settimana il sistema compra/vende seguendo i segnali di Tool4. Serve a vedere se "dare retta" ai segnali avrebbe fatto guadagnare — senza rischiare soldi veri.

Le 3 card in alto

  • Settimana: guadagno/perdita totale di questa settimana.
  • vs Buy&Hold: confronto col "compra e tieni" (se lunedì avessi comprato e non toccato più niente). Δ verde = la strategia sta facendo meglio.
  • vs SPY: confronto con l'indice S&P 500 (il mercato USA nel suo insieme).

Le 2 torte

  • Capitale per azione: come sono divisi i tuoi soldi adesso — quanto in ogni azione + quanto ancora in contanti ("Da investire").
  • Guadagni per azione: quanto pesa il guadagno/perdita di ciascuna azione (stessi colori dell'altra torta). Il + o e il valore al centro dicono se sei in positivo o negativo.

Il grafico a linee (curva)

È il valore del tuo portafoglio nel tempo, durante la settimana, con 3 linee a confronto: ▬ verde = la tua strategia, ▬ grigio = compra-e-tieni, ▬ blu = indice SPY. Se il verde sta sopra le altre, il trading sta convenendo; se sta sotto, faresti meglio a non fare nulla. (I numeri sull'asse sono euro totali del portafoglio.)

Posizioni: per ogni azione il segnale attuale, quante ne hai, quanto vale ora e il guadagno/perdita.

Trade recenti: le operazioni (compra/vendi) fatte questa settimana. Tutto è denaro fittizio.

)} {loading && !data &&
Caricamento…
} {error &&
Errore: {error}
} {data && !data.active && (
Simulatore non ancora avviato. Si attiva dal toggle in Configurazione e parte il lunedì, oppure premi Reset settimana per avviarlo subito.
)} {data && data.active && <> {/* Summary settimana corrente */}
Settimana {s.week_start}
{pct(s.return_pct)}
€{(s.nav || 0).toFixed(0)} su €{(s.total_start || 0).toFixed(0)} · {s.n_trades || 0} trade
vs Buy&Hold
{pct(s.bh_return_pct)}
{(s.return_pct != null && s.bh_return_pct != null) ? `Δ ${pct((s.return_pct || 0) - (s.bh_return_pct || 0))}` : ""}
vs SPY
{pct(s.spy_return_pct)}
{/* Torte: capitale per azione + guadagni per azione */}
Capitale per azione
Guadagni per azione
= 0 ? "+" : "-") + "€" + Math.abs(netGain).toFixed(0)} centerSub="netto" centerColor={netGain >= 0 ? "#3fb950" : "#f0556b"} />
{/* Curva NAV */}

Andamento del valore

— strategia ·{" "} — compra-e-tieni ·{" "} — SPY
{(data.nav && data.nav.length > 1) ? :
Curva in costruzione (servono più punti durante la settimana).
}
Valore del tuo portafoglio (in €) nel tempo. Se la linea verde sta sopra le altre, seguire i segnali sta convenendo rispetto a tenere fermo o all'indice.
{/* Barre: investito + guadagno per azione */}
Investito e guadagno per azione
▮ investito · ▮ guadagno / perdita · scale diverse (la perdita va comunque in alto, in rosso) · valore sopra ogni barra
{/* Posizioni — tabella chiara su desktop, card su mobile */}

Posizioni

{isMobile ? (
{positions.map(p => (
{p.symbol} {p.t4_signal || "—"}
investito €{p.invested} · cash €{p.cash} · {p.shares} az{p.n_trades ? ` · ${p.n_trades} trade` : ""}
€{p.value}
{pct(p.pnl_pct)}
))}
) : (
{positions.map(p => ( ))}
Ticker Segnale Azioni Investito Cash Valore P&L
{p.symbol} {p.t4_signal || "—"} {p.shares} €{p.invested} €{p.cash} €{p.value} {pct(p.pnl_pct)}
)} {/* Trade recenti — righe responsive */} {trades.length > 0 && <>

Trade recenti

{trades.map((t, i) => (
{t.side}
{t.symbol} @{(t.price || 0).toFixed(2)}
{(t.timestamp || "").slice(5, 16).replace("T", " ")} · {t.reason}
€{(t.value || 0).toFixed(0)}
))}
} } {/* Settimane passate */}

Settimane passate

{(!hist.weeks || hist.weeks.length === 0) ?
Nessuna settimana finalizzata ancora.
:
{hist.weeks.map(w => ( openDetail(w.week_start)}> {openWeek === w.week_start && ( )} ))}
Settimana RendimentoBuy&HoldSPYTrade
{w.week_start} {pct(w.return_pct)} {pct(w.bh_return_pct)} {pct(w.spy_return_pct)} {w.n_trades}
{detail.map(d => ( ))} {detail.length === 0 && }
{d.symbol} {pct(d.return_pct)} buy-hold {pct(d.bh_return_pct)} {d.n_trades} trade
}
); } window.SimulatorView = SimulatorView;