/* sleeves.jsx — Tool12: forward-test pre-registrato a 4 sleeve meccaniche. Card per sleeve (NAV, cum vs target, posizioni, badge KILLED) + sparkline SVG, tabelle ordini/trade, storico settimanale. Refresh 5 min + run-now. */ (function () { const { useState, useEffect, useRef } = React; function Spark({ curve, capital, width = 260, height = 56 }) { if (!curve || curve.length < 2) return
NAV in costruzione…
; const vals = curve.map(p => p.nav); const min = Math.min(...vals, capital), max = Math.max(...vals, capital); const span = (max - min) || 1; const pts = vals.map((v, i) => `${(i / (vals.length - 1)) * width},${height - ((v - min) / span) * height}`).join(" "); const capY = height - ((capital - min) / span) * height; const up = vals[vals.length - 1] >= capital; return ( ); } function SleeveCard({ s, target }) { const [open, setOpen] = useState(false); const killed = s.status === "KILLED"; const cumOk = s.cum_pnl >= 0; const trades = s.trades || []; const orders = s.orders || []; const green = "var(--green, #22c55e)", red = "var(--red, #ef4444)"; const shortTs = (ts) => (ts || "").slice(5, 16).replace("T", " "); // MM-DD HH:MM const shortDay = (ts) => (ts || "").slice(5, 10); // MM-DD const actLabel = (t) => t.action === "EXIT" ? "Chiudi" : (t.side === "SHORT" ? "Apri short" : "Apri long"); const stColor = (st) => st === "FILLED" ? green : st === "REJECTED" ? red : "var(--amber, #f59e0b)"; return (
setOpen(v => !v)} title="Clicca per trade e ordini" style={{ cursor: "pointer", display: "flex", justifyContent: "space-between", alignItems: "baseline" }}> {open ? "▾ " : "▸ "}{s.key} · {s.name} {killed ? ☠️ KILLED : {s.positions.length} pos}
€{s.nav.toFixed(0)} {s.cum_pnl >= 0 ? "+" : ""}{s.cum_pnl.toFixed(2)}€ {s.consec_below > 0 && !killed && sotto soglia {s.consec_below}/8}
{killed && s.kill_reason &&
{s.kill_reason}
} {s.positions.length > 0 && ( {s.positions.map(p => ( ))}
PosLatoEntryUltimoP&LUscita
{p.symbol}{p.leverage > 1 ? ` ×${p.leverage}` : ""} {p.side === "SHORT" ? "▼ short" : "▲ long"} {p.entry} {p.last} = 0 ? green : red }}> {p.pnl_pct >= 0 ? "+" : ""}{p.pnl_pct}% {shortDay(p.planned_exit) || "—"}
)} {s.pending_orders.length > 0 && (
⏳ {s.pending_orders.length} ordini in attesa di fill: {s.pending_orders.map(o => `${o.action === "EXIT" ? "chiudi" : (o.side === "SHORT" ? "short" : "compra")} ${o.symbol}`).join(", ")}
)} {!open && (
setOpen(true)}> ▸ {trades.length} trade · {orders.length} ordini — clicca per il dettaglio
)} {open && (
Trade eseguiti (a che prezzo)
{trades.length === 0 ?
Nessun trade ancora.
: {trades.map((t, i) => ( ))}
QuandoAzionePrezzoQuoteP&L
{shortTs(t.timestamp)} {t.symbol} {actLabel(t)} {t.price != null ? t.price.toFixed(2) : "—"} {t.shares} = 0 ? green : red) }}> {t.pnl_eur == null ? "—" : (t.pnl_eur >= 0 ? "+" : "") + t.pnl_eur.toFixed(2) + "€"}
}
Ordini
{orders.length === 0 ?
Nessun ordine.
: {orders.map((o, i) => ( ))}
QuandoOrdineStatoFill
{shortTs(o.signal_ts)} {o.symbol} {o.action === "EXIT" ? "chiudi" : (o.side === "SHORT" ? "short" : "long")} {o.status} {o.fill_price != null ? o.fill_price.toFixed(2) : (o.reject_reason ? "✗" : "—")}
}
Sleeve meccaniche: l'uscita è per tempo/segnale (colonna "Uscita"), non con stop-loss. Fill al primo prezzo DOPO il segnale. ✗ = ordine scartato (vedi tooltip).
)}
); } window.SleevesView = function SleevesView() { const [data, setData] = useState(null); const [weeks, setWeeks] = useState([]); const [err, setErr] = useState(null); const [running, setRunning] = useState(false); const timer = useRef(null); const load = () => { fetch("/api/sleeves").then(r => r.json()).then(d => { if (d.error) setErr(d.error); else { setData(d); setErr(null); } }).catch(e => setErr(String(e))); fetch("/api/sleeves/history").then(r => r.json()) .then(d => setWeeks(d.weeks || [])).catch(() => {}); }; useEffect(() => { load(); timer.current = setInterval(load, 5 * 60 * 1000); return () => clearInterval(timer.current); }, []); const runNow = () => { setRunning(true); fetch("/api/sleeves/run-now", { method: "POST" }) .then(() => setTimeout(() => { setRunning(false); load(); }, 4000)) .catch(() => setRunning(false)); }; if (err) return
Errore: {err}
; if (!data) return
Caricamento sleeve…
; const target = (data.weekly_target_total || 90) / Math.max(1, data.sleeves.length); const totNav = data.sleeves.reduce((a, s) => a + s.nav, 0); const totCum = data.sleeves.reduce((a, s) => a + s.cum_pnl, 0); const byWeek = {}; weeks.forEach(w => { (byWeek[w.week_start] = byWeek[w.week_start] || []).push(w); }); const weekKeys = Object.keys(byWeek).sort().reverse(); return (
{data.blocked && (
🚫 MOTORE BLOCCATO: config/sleeves.json modificato senza bump di versione. Il forward-test non sta girando.
)}
Forward-test v{data.version} ({data.hash}) registrato: {data.registered_at || "—"} NAV €{totNav.toFixed(0)} = 0 ? "var(--green, #22c55e)" : "var(--red, #ef4444)" }}> cum {totCum >= 0 ? "+" : ""}{totCum.toFixed(2)}€ target €{data.weekly_target_total}/sett (€{target.toFixed(2)}/sleeve)
{data.sleeves.map(s => )}
{weekKeys.length > 0 && (
Storico settimanale {data.sleeves.map(s => )} {weekKeys.map(wk => { const rows = byWeek[wk]; const get = k => rows.find(r => r.sleeve === k); const tot = rows.reduce((a, r) => a + (r.pnl_eur || 0), 0); const cumT = rows.reduce((a, r) => Math.max(a, r.cum_target_eur || 0), 0) * data.sleeves.length / 1; return ( {data.sleeves.map(s => { const r = get(s.key); const v = r ? r.pnl_eur : null; return ; })} ); })}
Settimana{s.key}TOTTarget cum
{wk}= 0 ? "var(--green, #22c55e)" : "var(--red, #ef4444)") }}> {v == null ? "—" : (v >= 0 ? "+" : "") + v.toFixed(1)}{r && r.status === "KILLED" ? " ☠️" : ""}= 0 ? "var(--green, #22c55e)" : "var(--red, #ef4444)" }}> {(tot >= 0 ? "+" : "") + tot.toFixed(1)} {rows[0] ? (rows.reduce((a, r) => a + (r.cum_target_eur || 0), 0)).toFixed(0) : "—"}
)}
Regole pre-registrate in config/sleeves.json (hash verificato a ogni ciclo) · fill al primo prezzo disponibile DOPO il segnale · costi €1 + 0.1% + 5bps per lato · kill automatico: DD >15% o 8 settimane sotto −50% del target pro-quota.
); }; })();