// Calendar, Report, Evaluate, Config, Logs, Admin views // Note: FilterSelect, FilterDateChips, FilterDateInput are defined in shell.jsx (loads first) // ────────── Calendar ────────── function sentimentArrows(s) { if (s >= 3) return { text: "↑↑↑", color: "var(--green)" }; if (s === 2) return { text: "↑↑", color: "var(--green)" }; if (s === 1) return { text: "↑", color: "var(--green)" }; if (s === 0) return { text: "—", color: "var(--text-faint)" }; if (s === -1) return { text: "↓", color: "var(--red)" }; if (s === -2) return { text: "↓↓", color: "var(--red)" }; return { text: "↓↓↓", color: "var(--red)" }; } // Posiziona il tooltip evitando sidebar fissa (240px o 64px collassata) // e fuori-schermo. Se nessun lato ha spazio (caso agenda full-width), // posiziona SOTTO la riga centrato e clampato. function positionTooltip(rect, tipW = 290, tipH = 240) { const vw = window.innerWidth; const vh = window.innerHeight; const sidebarEl = document.querySelector(".sidebar"); const sidebarW = (sidebarEl && vw > 760) ? (sidebarEl.offsetWidth || 240) : 0; const margin = 12; // Prova: a destra della riga if (rect.right + 8 + tipW <= vw - margin) { return { x: rect.right + 8, y: Math.min(rect.top, vh - tipH - 16) }; } // Prova: a sinistra (rispettando sidebar) if (rect.left - 8 - tipW >= sidebarW + margin) { return { x: rect.left - tipW - 8, y: Math.min(rect.top, vh - tipH - 16) }; } // Fallback: sotto la riga, centrato e clampato const xCentered = rect.left + rect.width / 2 - tipW / 2; const xClamped = Math.max(sidebarW + margin, Math.min(xCentered, vw - tipW - margin)); const yBelow = rect.bottom + 8; // Se sotto non c'è spazio verticale, mettilo sopra const y = yBelow + tipH > vh - 16 ? Math.max(16, rect.top - tipH - 8) : yBelow; return { x: xClamped, y }; } function CalendarView() { const today = new Date(); const [view, setView] = useState({ y: today.getFullYear(), m: today.getMonth() }); const [clock, setClock] = useState(new Date()); const [selectedDay, setSelectedDay] = useState(null); // { label, key, evs } const [tooltip, setTooltip] = useState(null); // { event, dateKey, x, y } const [calEvents, setCalEvents] = useState(window.CALENDAR_EVENTS || {}); const [refreshStatus, setRefreshStatus] = useState(null); // null | "loading" | "ok" | "err" const [showDiscovery, setShowDiscovery] = useState(false); // mostra notizie IPO/Spinoff const refreshCalendar = async () => { setRefreshStatus("loading"); try { // 1. Avvia re-fetch Tool5 in background (IPO/spinoff news + eventi ticker) await fetch("/api/calendar/run-now", { method: "POST" }); // 2. Aspetta ~35s poi ricarica i dati dal DB setTimeout(async () => { try { const r = await fetch("/api/calendar-events").then(r => r.json()); if (r.events) { window.CALENDAR_EVENTS = r.events; setCalEvents(r.events); } setRefreshStatus("ok"); setTimeout(() => setRefreshStatus(null), 3000); } catch { setRefreshStatus("err"); } }, 35000); } catch { setRefreshStatus("err"); } }; // Vista: Griglia mensile (overview) o Agenda cronologica (lista). // Sotto i 1200px la griglia diventa illeggibile → forziamo agenda automaticamente. const [viewMode, setViewMode] = useState(() => { try { const saved = localStorage.getItem("calViewMode"); return saved === "agenda" ? "agenda" : "grid"; } catch { return "grid"; } }); const [forcedAgenda, setForcedAgenda] = useState( typeof window !== "undefined" && window.innerWidth < 1500 ); useEffect(() => { const t = setInterval(() => setClock(new Date()), 1000); return () => clearInterval(t); }, []); useEffect(() => { const onResize = () => setForcedAgenda(window.innerWidth < 1500); window.addEventListener("resize", onResize); return () => window.removeEventListener("resize", onResize); }, []); const effectiveMode = forcedAgenda ? "agenda" : viewMode; const onSetViewMode = (m) => { setViewMode(m); try { localStorage.setItem("calViewMode", m); } catch {} }; // Close day panel when month changes useEffect(() => { setSelectedDay(null); }, [view]); const firstDay = new Date(view.y, view.m, 1); const lastDay = new Date(view.y, view.m + 1, 0); const startWd = (firstDay.getDay() + 6) % 7; const monthName = firstDay.toLocaleDateString("it-IT", { month: "long", year: "numeric" }); const cells = []; const prevLast = new Date(view.y, view.m, 0).getDate(); for (let i = startWd - 1; i >= 0; i--) cells.push({ d: prevLast - i, dim: true, y: view.y, m: view.m - 1 }); for (let d = 1; d <= lastDay.getDate(); d++) cells.push({ d, dim: false, y: view.y, m: view.m }); while (cells.length % 7 !== 0) cells.push({ d: cells.length - (startWd + lastDay.getDate()) + 1, dim: true, y: view.y, m: view.m + 1 }); const dayKey = c => { const rm = ((c.m % 12) + 12) % 12; const ry = c.y + Math.floor(c.m / 12); return `${ry}-${String(rm+1).padStart(2,"0")}-${String(c.d).padStart(2,"0")}`; }; const isToday = c => !c.dim && c.d === today.getDate() && c.m === today.getMonth() && c.y === today.getFullYear(); const rawEvents = calEvents; // Filtra eventi: se showDiscovery=false, nasconde notizie IPO/Spinoff (sim. senza data reale) const _DISCOVERY_SYMS = new Set(["IPO", "SPINOFF"]); const events = {}; for (const [k, evs] of Object.entries(rawEvents)) { const filtered = evs.filter(e => showDiscovery || !_DISCOVERY_SYMS.has(e.sym)); if (filtered.length) events[k] = filtered; } const TYPE_CONFIG = { earnings: { color: "var(--teal)", label: "Risultati" }, ex_dividend: { color: "var(--amber)", label: "Stacco Div." }, dividend: { color: "var(--green)", label: "Dividendo" }, split: { color: "var(--violet)", label: "Frazionamento" }, ipo: { color: "var(--red)", label: "IPO" }, spinoff: { color: "#f97316", label: "Spin-off" }, macro: { color: "#8b9cf4", label: "Macro/Fed" }, other: { color: "var(--text-faint)", label: "Altro" }, }; const typeColor = (t) => (TYPE_CONFIG[t] || TYPE_CONFIG.other).color; const openDay = (c) => { const key = dayKey(c); const evs = events[key] || []; if (!evs.length) return; const rm = ((c.m % 12) + 12) % 12; const ry = c.y + Math.floor(c.m / 12); const label = new Date(ry, rm, c.d).toLocaleDateString("it-IT", { weekday: "long", day: "numeric", month: "long" }); setSelectedDay({ label, key, evs }); }; return (
{/* ── Top bar ── */}
BVF Financial Calendar
{!showDiscovery && (
{monthName}
)}
{!forcedAgenda && !showDiscovery && (
)} {/* Toggle notizie senza data specifica (IPO / Spin-off discovery) */} {clock.toLocaleTimeString("it-IT")}
{/* ── Vista Discovery (IPO / Spin-off) ── */} {showDiscovery && (() => { const discoveryEvs = Object.values(calEvents).flat() .filter(e => _DISCOVERY_SYMS.has(e.sym)); const ipoEvs = discoveryEvs.filter(e => e.type === "ipo"); const spinoffEvs = discoveryEvs.filter(e => e.type === "spinoff"); const renderGroup = (label, color, evs) => evs.length === 0 ? null : (
{label} ({evs.length})
{evs.map((e, i) => { let detail = {}; try { detail = JSON.parse(e.detail || "{}"); } catch {} return (
{e.title}
{detail.publisher && {detail.publisher}} {detail.url && ( ev.stopPropagation()}> Leggi articolo → )}
); })}
); return (

Notizie recenti su IPO imminenti e spin-off — aggiornate giornalmente da Yahoo Finance. Senza data precisa di quotazione.

{discoveryEvs.length === 0 ? (
Nessuna notizia disponibile. Premi Aggiorna per caricare.
) : ( <> {renderGroup("IPO imminenti", TYPE_CONFIG.ipo.color, ipoEvs)} {renderGroup("Spin-off annunciati", TYPE_CONFIG.spinoff.color, spinoffEvs)} )}
); })()} {/* ── Calendario classico ── */} {!showDiscovery && <> {/* ── Legend ── */}
{Object.entries(TYPE_CONFIG).map(([t, cfg]) => ( {cfg.label} ))}
{/* ── Calendar grid (headers + cells in one grid) — solo in modalità grid ── */} {effectiveMode === "grid" && (
{[["Lun","L"],["Mar","M"],["Mer","M"],["Gio","G"],["Ven","V"],["Sab","S"],["Dom","D"]].map(([full, short], i)=>(
{full} {short}
))} {cells.map((c, i) => { const key = dayKey(c); const evs = events[key] || []; const hasEvs = evs.length > 0; return (
!c.dim && window.innerWidth <= 760 && openDay(c)} > {!c.dim && {c.d}} {/* Mobile: colored dots only (not for dim cells) */} {!c.dim && hasEvs && (
{evs.slice(0, 8).map((e, j) => ( ))}
)} {/* Desktop: text rows (not for dim cells) */} {!c.dim && evs.map((e, j) => { const sa = e.arrows ? { text: e.arrows, color: e.arrows.startsWith("↑") ? "var(--green)" : "var(--red)" } : sentimentArrows(e.sentiment); return (
{ const rect = ev.currentTarget.getBoundingClientRect(); setTooltip({ event: e, dateKey: key, ...positionTooltip(rect) }); }} onMouseLeave={() => setTooltip(null)} > {sa.text && sa.text !== "—" && {sa.text}{" "}} {e.sym} {" "}{TYPE_CONFIG[e.type]?.label || e.type} {e.time && {" · "}{e.time}}
); })}
); })}
)} {/* ── Event list (below grid only, chronological) ── */} {effectiveMode === "grid" && (() => { const monthPrefix = `${view.y}-${String(view.m+1).padStart(2,"0")}`; const list = Object.entries(events) .filter(([date]) => date.startsWith(monthPrefix)) .flatMap(([date, evs]) => evs.map(e => ({ ...e, date }))) .sort((a, b) => { const dc = a.date.localeCompare(b.date); return dc !== 0 ? dc : (a.time || "99:99").localeCompare(b.time || "99:99"); }); if (!list.length) return (
Nessun evento in {monthName}
); return (
{list.length} eventi · {monthName}
{list.map((e, i) => { const cfg = TYPE_CONFIG[e.type] || TYPE_CONFIG.other; const d = new Date(e.date + "T12:00:00"); const dLabel = d.toLocaleDateString("it-IT", { weekday:"short", day:"2-digit", month:"short" }); const isUpcoming = e.date >= `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,"0")}-${String(today.getDate()).padStart(2,"0")}`; return (
{e.company || e.sym} {e.sym} {cfg.label} {e.confirmed && ✓✓}
{dLabel} {e.time ? · {e.time} (Roma) : · orario n/d } {e.arrows && e.arrows !== "—" && ( {e.arrows} {e.analyst_label || ""} {e.analyst_count != null && ({e.analyst_count})} )}
); })}
); })()} {/* ── Agenda view (lista cronologica giorno per giorno, full-width) ── */} {effectiveMode === "agenda" && (() => { const monthPrefix = `${view.y}-${String(view.m+1).padStart(2,"0")}`; const monthEvents = Object.entries(events) .filter(([date]) => date.startsWith(monthPrefix)) .sort(([a], [b]) => a.localeCompare(b)); if (!monthEvents.length) return (
Nessun evento in {monthName}
); const todayKey = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,"0")}-${String(today.getDate()).padStart(2,"0")}`; return (
{monthEvents.map(([date, evs]) => { const d = new Date(date + "T12:00:00"); const weekday = d.toLocaleDateString("it-IT", { weekday: "long" }); const dateLabel = d.toLocaleDateString("it-IT", { day: "numeric", month: "long" }); const sortedEvs = [...evs].sort((a, b) => (a.time || "99:99").localeCompare(b.time || "99:99") ); const isPast = date < todayKey; const isToday_ = date === todayKey; return (
{weekday} {dateLabel} {isToday_ && Oggi} {sortedEvs.length} event{sortedEvs.length === 1 ? "o" : "i"}
{sortedEvs.map((e, j) => { const cfg = TYPE_CONFIG[e.type] || TYPE_CONFIG.other; const sa = e.arrows ? { text: e.arrows, color: e.arrows.startsWith("↑") ? "var(--green)" : "var(--red)" } : sentimentArrows(e.sentiment); return (
{ const rect = ev.currentTarget.getBoundingClientRect(); setTooltip({ event: e, dateKey: date, ...positionTooltip(rect) }); }} onMouseLeave={() => setTooltip(null)} onClick={() => window.innerWidth <= 760 && openDay({ d: d.getDate(), dim: false, y: d.getFullYear(), m: d.getMonth() })} > {sa.text && sa.text !== "—" ? sa.text : ""} {e.company || e.sym} {cfg.label} {e.confirmed && ✓✓} {e.time || "—"}
); })}
); })}
); })()} {/* ── Tooltip (desktop hover) ── */} {tooltip && (() => { const e = tooltip.event; const sa = e.arrows ? { text: e.arrows, color: e.arrows.startsWith("↑") ? "var(--green)" : "var(--red)" } : sentimentArrows(e.sentiment); const sentLabel = e.analyst_label || (sa.color === "var(--green)" ? "Strong Buy" : "Strong Sell"); return (
{e.title || e.company || `${TYPE_CONFIG[e.type]?.label || e.type} ${e.sym}`}
Data {tooltip.dateKey} Simbolo {e.sym} {e.company && e.company !== e.sym && <> Società {e.company} } {e.time && <> Orario Roma {e.time} } Tipo {TYPE_CONFIG[e.type]?.label || e.type} {e.eps_est != null && <> EPS stimato ${e.eps_est} } {sa.text && sa.text !== "—" && <> Analisti {sa.text} {e.analyst_label || sentLabel} {e.analyst_count != null && ({e.analyst_count})} } {e.sources && e.sources.length > 0 && <> Fonte {e.sources.join(" + ")} {e.confirmed ? ✓ confermato (2 fonti) : · singola fonte} }
); })()} } {/* fine !showDiscovery */} {/* ── Day detail panel (slide-in) ── */} {selectedDay && (

{selectedDay.label}

{selectedDay.evs.map((e, i) => { const sa = e.arrows ? { text: e.arrows, color: e.arrows.startsWith("↑") ? "var(--green)" : "var(--red)" } : sentimentArrows(e.sentiment); return (
{e.company || e.sym} ({e.sym}) {sa.text && sa.text !== "—" && ( {sa.text} )}
{TYPE_CONFIG[e.type]?.label || e.type} {e.time && · {e.time} (Roma)}
{e.eps_est != null && (
EPS stimato: ${e.eps_est}
)} {sa.text && sa.text !== "—" && (
Analisti: {sa.text} {e.analyst_label ? ` ${e.analyst_label}` : ""} {e.analyst_count != null && ({e.analyst_count})}
)}
); })}
)}
); } // ────────── Report ────────── const rThStyle = { padding:"9px 12px", textAlign:"left", fontSize:11, fontWeight:600, color:"var(--text-muted)", textTransform:"uppercase", letterSpacing:".06em", background:"var(--bg)", borderBottom:"1px solid var(--border)", whiteSpace:"nowrap", position:"sticky", top:0, zIndex:2, }; const rTdStyle = { padding:"8px 12px", fontSize:12.5, borderBottom:"1px solid var(--border-soft)", whiteSpace:"nowrap", verticalAlign:"middle", }; function PctCell({ v }) { if (v === undefined || v === null) return —; const n = parseFloat(v); if (isNaN(n)) return —; const c = n > 0 ? "var(--green)" : n < 0 ? "var(--red)" : "var(--text-muted)"; return {n > 0 ? "+" : ""}{n.toFixed(2)}% ; } function PriceCell({ v }) { if (v === undefined || v === null) return —; const n = parseFloat(v); return {isNaN(n) ? "—" : n.toFixed(2)}; } function SignalBadge({ sig }) { if (!sig) return ; const s = sig.toUpperCase().replace(/ /g,"_"); const map = { BUY: ["var(--green)", "var(--green-soft)"], STRONG_BUY: ["var(--green)", "var(--green-soft)"], SELL: ["var(--red)", "var(--red-soft)"], STRONG_SELL: ["var(--red)", "var(--red-soft)"], HOLD: ["var(--amber)", "var(--amber-soft)"], }; const [c,bg] = map[s] || ["var(--text-muted)","var(--surface)"]; return ( {sig.replace(/_/g," ")} ); } function ConfCell({ v }) { if (v === undefined || v === null) return —; const n = parseInt(v); const c = n >= 70 ? "var(--green)" : n >= 50 ? "var(--text)" : "var(--red)"; return {n}%; } function TrendCell({ v }) { if (!v) return —; const up = v.toUpperCase().includes("BULL"); const dn = v.toUpperCase().includes("BEAR"); return ( {up ? "▲ BULL" : dn ? "▼ BEAR" : v} ); } // ts helper: "2026-05-14 09:20:31" → "09:20" const _ts2time = ts => (ts || "").slice(11,16) || "—"; function ReportView() { const [tool, setTool] = useState("tool2"); // Real data from API (date-keyed). Falls back to empty if not loaded yet. const byDate = window.REPORT_DATA_REAL || {}; const allDates = Object.keys(byDate).sort().reverse(); // newest first // Default: yesterday const _yesterday = (() => { const d = new Date(); d.setDate(d.getDate()-1); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`; })(); const [dateFilter, setDateFilter] = useState(allDates[0] || _yesterday); const [tickerFilter, setTickerFilter] = useState("Tutti"); // All tickers in the selected day (across all tools) const dayData = byDate[dateFilter] || { tool2:[], tool3:[], tool4:[] }; const allSymbols = [...new Set([ ...((dayData.tool2)||[]).map(r=>r.symbol), ...((dayData.tool3)||[]).map(r=>r.symbol), ...((dayData.tool4)||[]).map(r=>r.symbol), ])].sort(); const tickers = ["Tutti", ...allSymbols]; const applyFilter = rows => (rows||[]).filter(r => tickerFilter === "Tutti" || r.symbol === tickerFilter ); const rows2 = applyFilter(dayData.tool2); const rows3 = applyFilter(dayData.tool3); const rows4 = applyFilter(dayData.tool4); // CSV export const exportCsv = () => { const rows = tool === "tool2" ? rows2 : tool === "tool3" ? rows3 : rows4; if (!rows.length) return; const headers = Object.keys(rows[0]); const esc = v => { const s = String(v??''); return (s.includes(',')||s.includes('"')||s.includes('\n')) ? `"${s.replace(/"/g,'""')}"` : s; }; const lines = [headers.join(','), ...rows.map(r => headers.map(h => esc(r[h])).join(','))]; const a = document.createElement('a'); a.href = 'data:text/csv;charset=utf-8,' + encodeURIComponent(lines.join('\r\n')); a.download = `report_${tool}_${dateFilter}.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); }; const counts = `${rows2.length} AI | ${rows3.length} TA | ${rows4.length} News`; const noData = allDates.length === 0; return (
{/* Toolbar */}
({ value: t, label: t }))} placeholder="Tutti" />
{noData ? "Nessun dato — avvia il daemon" : `${dateFilter} — ${counts}`}
{noData ? (
Nessun dato nel database. Avvia il daemon e attendi il primo ciclo completo.
) : ( <> {/* Tool tabs */}
{[ { id:"tool2", label:"Tool2 — AI Agent" }, { id:"tool3", label:"Tool3 — TA Analyzer" }, { id:"tool4", label:"Tool4 — News" }, ].map(t => ( ))}
{/* Table */}
{tool === "tool2" && ( rows2.length === 0 ? : {["GIORNO","SYMBOL","ORA","MODE","SIGNAL","CONF%","CONF★%","TREND 1H","CLOSE","+1H","+3H","+6H","+24H","+2W","TARGET","STOP"].map(h=>)} {rows2.map((r,i)=>( ))}
{h}
{(r.timestamp||"").slice(0,10)} {r.symbol} {_ts2time(r.timestamp)} {r.mode}
)} {tool === "tool3" && ( rows3.length === 0 ? : {["GIORNO","SYMBOL","ORA","SIGNAL","SCORE","METODO","N.","+1H","+3H","+6H","+24H","+2W"].map(h=>)} {rows3.map((r,i)=>( ))}
{h}
{(r.timestamp||"").slice(0,10)} {r.symbol} {_ts2time(r.timestamp)} =65?"var(--green)":r.score_total<50?"var(--red)":"var(--text)",fontWeight:600}}>{r.score_total ?? "—"} {r.forecast_method} {r.sample_size ?? "—"}
)} {tool === "tool4" && ( rows4.length === 0 ? : {["GIORNO","SYMBOL","ORA","SIGNAL","IMPACT","SENT.","ALERT","+1H%","p(1h)","+3H%","p(3h)","+6H%","p(6h)","+24H%","p(24h)"].map(h=>)} {rows4.map((r,i)=>{ const sent = parseFloat(r.overall_sentiment); const imp = (r.news_impact||"").toUpperCase(); const impColor = imp==="HIGH"||imp==="CRITICAL" ? "var(--red)" : imp==="MEDIUM" ? "var(--amber)" : "var(--text-muted)"; return ( ); })}
{h}
{(r.timestamp||"").slice(0,10)} {r.symbol} {_ts2time(r.timestamp)} {r.news_impact||"—"} 0?"var(--green)":!isNaN(sent)&&sent<0?"var(--red)":"var(--text-muted)",fontWeight:500}}> {isNaN(sent) ? "—" : (sent>0?"+":"")+sent.toFixed(2)} {parseInt(r.breaking_alert)===1 ? "⚡ SI" : "no"} {r.fc_1h_prob != null ? parseFloat(r.fc_1h_prob).toFixed(2) : "—"} {r.fc_3h_prob != null ? parseFloat(r.fc_3h_prob).toFixed(2) : "—"} {r.fc_6h_prob != null ? parseFloat(r.fc_6h_prob).toFixed(2) : "—"} {r.fc_24h_prob != null ? parseFloat(r.fc_24h_prob).toFixed(2) : "—"}
)}
)}
); } function ReportEmpty() { return (
Nessun dato per la data/ticker selezionati.
); } // ────────── Evaluate ────────── function EvaluateView() { const [fileObjs, setFileObjs] = useState({ tool2: null, tool3: null, tool4: null }); const [status, setStatus] = useState(""); const [loading, setLoading] = useState(false); const [results, setResults] = useState(null); const setFile = (k, file) => setFileObjs(f => ({ ...f, [k]: file })); const ready = Object.values(fileObjs).some(Boolean); const ZONES = [ { k: "tool2", icon: "brain", name: "Tool 2 — AI Agent", emoji: "🤖" }, { k: "tool3", icon: "trending", name: "Tool 3 — Tecnico", emoji: "📐" }, { k: "tool4", icon: "newspaper", name: "Tool 4 — News", emoji: "📰" }, ]; const handleDrop = (k) => (e) => { e.preventDefault(); const file = e.dataTransfer?.files[0]; if (file && file.name.endsWith(".csv")) setFile(k, file); }; const handleChange = (k) => (e) => { const file = e.target.files[0]; if (file) setFile(k, file); }; const handleClear = (k, e) => { e.stopPropagation(); setFile(k, null); }; const runEvaluate = async () => { setLoading(true); setStatus("⏳ Elaborazione in corso..."); setResults(null); const fd = new FormData(); let count = 0; for (const { k } of ZONES) { if (fileObjs[k]) { fd.append("csv_" + k, fileObjs[k], fileObjs[k].name); count++; } } if (!count) { setLoading(false); return; } try { const r = await fetch("/api/evaluate", { method: "POST", body: fd }); const d = await r.json(); if (d.error) throw new Error(d.error); setResults(d.results); const n = d.results.filter(x => !x.error).length; setStatus(`✓ Completato — ${n} tool analizzati`); } catch (e) { setStatus("✗ Errore: " + e.message); } finally { setLoading(false); } }; return (

Valutazione previsioni

Carica i CSV esportati dalla sezione Report (idealmente 24–48h dopo l'esecuzione). Il sistema arricchisce ogni riga con le variazioni reali di prezzo e genera un report con istruzioni specifiche per Claude Code.
{ZONES.map(z => { const f = fileObjs[z.k]; return ( ); })}
{status || (ready ? `${Object.values(fileObjs).filter(Boolean).length} file pronto/i` : "Nessun file selezionato")}
{results && results.map((r, idx) => )}
); } function EvaluateResult({ r, idx, allResults }) { const [copied, setCopied] = useState(false); const toolNames = { tool2: "🤖 Tool 2 — AI Agent", tool3: "📐 Tool 3 — Tecnico", tool4: "📰 Tool 4 — News" }; if (r.error) { return (

⚠ Errore — {r.filename}

{r.error}
); } const toolName = toolNames[r.tool] || r.tool; const validMsg = r.n_valid === 0 ? ⚠ 0 prezzi reali (DB vecchio o tutto pending) : ✓ {r.n_valid}/{r.n_total} record con prezzi reali; const horizons = (r.analysis || {}).horizons || {}; const priorityColor = { ALTA: "var(--red)", MEDIA: "var(--amber)", BASSA: "var(--green)" }; const copyReport = () => { navigator.clipboard.writeText(r.report_md || "") .then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); }) .catch(() => {}); }; const downloadCsv = () => { const blob = new Blob([r.enriched_csv], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "eval_enriched_" + r.filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; const downloadMd = () => { const blob = new Blob([r.report_md || ""], { type: "text/markdown;charset=utf-8;" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "eval_report_" + r.filename.replace(/\.csv$/i, "") + ".md"; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; return (

{toolName}

{validMsg} · {r.filename}

Accuracy Summary

{["1h","3h","6h","24h"].map(label => { const h = horizons[label] || {}; const mbe = h.mbe != null ? = 0 ? "var(--red)" : "var(--green)"}}>{h.mbe >= 0 ? "+" : ""}{h.mbe.toFixed(3)}% : "—"; return ( ); })}
OrizzontenMAEBias (MBE)Dir.AccRMSE
+{label} {h.n ?? 0} {h.mae != null ? h.mae.toFixed(3) + "%" : "—"} {mbe} {h.dir_acc != null ? h.dir_acc.toFixed(1) + "%" : "—"} {h.rmse != null ? h.rmse.toFixed(3) : "—"}

Pattern Identificati

{(!r.patterns || r.patterns.length === 0) ? (

✅ Nessun pattern critico (dati insufficienti o accuratezza già buona)

) : r.patterns.map((p, i) => (
{p.priority} {p.title}
{p.evidence || ""}
))}

Report per Claude Code

Copia e incolla direttamente in Claude Code
{r.report_md || ""}
); } // ────────── Config ────────── function ConfigView({ user }) { const [configJson, setConfigJson] = useState(""); const [loading, setLoading] = useState(true); const [saveStatus, setSaveStatus] = useState(null); // null | "ok" | "error" const [parsed, setParsed] = useState(null); const [newTicker, setNewTicker] = useState(""); const [restartStatus, setRestartStatus] = useState(null); // null | "loading" | "ok" | "err" const [starState, setStarState] = useState({}); // {symbol: bool} — stellina Tool4 const isAdmin = user?.is_admin || false; const loadStars = () => { fetch("/api/watchlist/star").then(r => r.json()) .then(d => { if (d.state) setStarState(d.state); }).catch(() => {}); }; useEffect(loadStars, []); const toggleStar = (sym) => { const isOn = starState[sym] !== false; // default true (stellato) const want = !isOn; setStarState(s => ({ ...s, [sym]: want })); // ottimistico fetch("/api/watchlist/star", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ symbol: sym, starred: want }), }).then(r => r.json()).then(loadStars).catch(loadStars); }; const restartAnalyzer = async () => { setRestartStatus("loading"); try { const r = await fetch("/api/restart-analyzer", { method: "POST" }).then(r => r.json()); setRestartStatus(r.ok ? "ok" : "err"); setTimeout(() => setRestartStatus(null), 4000); } catch { setRestartStatus("err"); setTimeout(() => setRestartStatus(null), 4000); } }; const fetchConfig = () => { setLoading(true); fetch("/api/config") .then(r => r.json()) .then(data => { const pretty = JSON.stringify(data, null, 2); setConfigJson(pretty); setParsed(data); setLoading(false); }) .catch(() => setLoading(false)); }; useEffect(() => { fetchConfig(); }, []); const toggleInConfig = (path, value) => { try { const obj = JSON.parse(configJson); const keys = path.split("."); let ref = obj; for (let i = 0; i < keys.length - 1; i++) ref = ref[keys[i]]; ref[keys[keys.length - 1]] = value; const pretty = JSON.stringify(obj, null, 2); setConfigJson(pretty); setParsed(obj); } catch {} }; const saveConfig = async () => { try { const body = JSON.parse(configJson); const res = await fetch("/api/config", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const data = await res.json(); setSaveStatus(data.ok ? "ok" : "error"); if (data.ok) window.refreshDashboard?.(); setTimeout(() => setSaveStatus(null), 3000); } catch { setSaveStatus("error"); setTimeout(() => setSaveStatus(null), 3000); } }; const addTicker = () => { const sym = newTicker.trim().toUpperCase(); if (!sym) return; const list = parsed?.watchlist || []; if (list.includes(sym)) { setNewTicker(""); return; } toggleInConfig("watchlist", [...list, sym]); setNewTicker(""); }; const sched = parsed?.schedule || {}; const toggles = [ { key: "schedule.tool1.enabled", name: "Tool 1 — Collector", desc: "Fetch OHLCV 5m, ogni 5 min", val: sched?.tool1?.enabled }, { key: "schedule.tool3.enabled", name: "Tool 3 — Tecnico", desc: "EMA, RSI, MACD · 10 min", val: sched?.tool3?.enabled }, { key: "schedule.tool4.enabled", name: "Tool 4 — News", desc: "Finnhub + Perplexity", val: sched?.tool4?.enabled }, { key: "schedule.tool5.enabled", name: "Tool 5 — Calendario", desc: "Earnings + eventi · digest mattutino", val: sched?.tool5?.enabled }, { key: "schedule.tool7.enabled", name: "Tool 7 — Settori", desc: "Rotazione settori + temi · 60 min", val: sched?.tool7?.enabled }, { key: "schedule.tool8.enabled", name: "Tool 8 — Pre-market", desc: "Gap scanner pre-apertura", val: sched?.tool8?.enabled }, { key: "schedule.tool9.enabled", name: "Tool 9 — Top Picks", desc: "Discovery daily 03:00 · Claude Haiku", val: sched?.tool9?.enabled }, { key: "schedule.tool10.enabled", name: "Tool 10 — Chain", desc: "Lead-lag discovery daily 04:00 · gratis", val: sched?.tool10?.enabled }, { key: "schedule.tool11.enabled", name: "Tool 11 — Simulatore", desc: "Paper-trading settimanale · denaro fittizio", val: sched?.tool11?.enabled }, ]; const dash = parsed?.dashboard || {}; const dashToggles = [ { key: "dashboard.show_tool2", name: "Linea Tool 2 — AI (blu)", val: dash.show_tool2 !== false }, { key: "dashboard.show_tool3", name: "Linea Tool 3 — Tecnico (arancio)", val: dash.show_tool3 !== false }, { key: "dashboard.show_tool4", name: "Linea Tool 4 — News (viola)", val: dash.show_tool4 !== false }, ]; const forcePx = (parsed?.tool4?.perplexity_force) === true; return (

Attiva / disattiva tool

{isAdmin && ( )}
Controlla quali tool girano nello scheduler del daemon.
{toggles.map(t => (
{t.name}{t.desc}
))}

Visibilità nei grafici

Nasconde la linea del tool nei grafici della Dashboard.
{dashToggles.map(t => (
{t.name}
))}

Opzioni avanzate Tool 4 (News)

Diagnostica. Le modifiche si applicano dopo Salva configurazione (hot-reload, niente restart).
Forza Perplexity su tutti i ticker Bypassa breaking-gate e cache: 1 chiamata Perplexity per ticker a ogni ciclo Tool 4. {forcePx ? " ⚠️ ATTIVO — costoso, ricordati di rispegnerlo dopo i test." : " Solo per test/diagnostica."}

Watchlist

Aggiungi o rimuovi ticker dal watchlist principale. La stellina ★ attiva l'analisi news con IA (Tool4): i ticker stellati pagano l'AI sulle news; quelli senza stella si fermano al Tool3 (Tool4 derivato, costo zero). Salva per applicare le altre modifiche.
{(parsed?.watchlist || []).map(sym => { const on = starState[sym] !== false; // default stellato return ( {sym} ); })} {(parsed?.watchlist || []).length === 0 && ( Nessun ticker nel watchlist. )}
setNewTicker(e.target.value.toUpperCase())} onKeyDown={e => e.key === "Enter" && addTicker()} disabled={loading} style={{ padding:"6px 10px", background:"var(--surface)", border:"1px solid var(--border)", borderRadius:"var(--radius-sm)", color:"var(--text)", fontSize:12.5, width:120, outline:"none", transition:"border-color 120ms", }} onFocus={e => e.target.style.borderColor = "var(--accent)"} onBlur={e => e.target.style.borderColor = "var(--border)"} />

Editor JSON

Modifica diretta della configurazione. Salva per applicare al prossimo ciclo del daemon.