/* 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 && (
Pos Lato Entry Ultimo P&L Uscita
{s.positions.map(p => (
{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.
:
Quando Azione Prezzo Quote P&L
{trades.map((t, i) => (
{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.
:
Quando Ordine Stato Fill
{orders.map((o, i) => (
{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)
{running ? "In esecuzione…" : "▶ Run now"}
{data.sleeves.map(s => )}
{weekKeys.length > 0 && (
Storico settimanale
Settimana
{data.sleeves.map(s => {s.key} )}TOT Target cum
{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 (
{wk}
{data.sleeves.map(s => {
const r = get(s.key);
const v = r ? r.pnl_eur : null;
return = 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.
);
};
})();