/* ═══════════════════════════════════════════════════════════════
Screens — Dashboard, Signaux, Volatile, Charts, Journal
═══════════════════════════════════════════════════════════════ */
const { useEffect: useEffectS, useState: useStateS, useRef: useRefS, useMemo: useMemoS } = React;
/* ────────────────────────────────────────────────────────────────
DASHBOARD
──────────────────────────────────────────────────────────────── */
function DashboardScreen({ onAction }) {
const { KPI, POSITIONS, ACTIVITY, EQUITY, SIGNALS } = window.MOCK;
const top3 = [...SIGNALS].filter(s => s.state === "active").sort((a,b)=>b.score-a.score).slice(0, 3);
return (
Dashboard
Lundi 13 mai 2026 · 14:42 · Connecté OKX
{/* KPI row */}
}
foot={SUR {KPI.pnlTotal.period} · CAPITAL €10K}
sparkline={EQUITY.map(p => p.v).slice(-30)}
/>
}
foot={{KPI.winRate.wins}W · {KPI.winRate.losses}L · {KPI.winRate.trades} trades}
/>
{KPI.trades.d}
aujourd'hui
}
foot={
7J {KPI.trades.w}
30J {KPI.trades.m}
}
/>
{fmtPct(KPI.drawdown.current)}
}
foot={
MAX {fmtPct(KPI.drawdown.max)}
·
SEUIL −15%
}
/>
{/* Crypto price chart full-width */}
{/* Positions + Top signals */}
Positions ouvertes {POSITIONS.length}
Top signaux actifs
SCORE ≥ 8
{top3.map(s => )}
{/* Activity timeline */}
Activité récente
DERNIÈRE HEURE
{ACTIVITY.map((a, i) =>
)}
);
}
function KpiCard({ label, main, foot, accent, sparkline }) {
return (
{label}
{accent && }
{main}
{sparkline && (
)}
{foot}
);
}
function WinRateGauge({ value }) {
const r = 26;
const c = 2 * Math.PI * r;
const offset = c * (1 - value / 100);
const animated = useCountUp(value, 900);
return (
{animated.toFixed(1)}%
RATIO W/L
);
}
function EquityCurve({ data }) {
const ref = useRefS(null);
const [hover, setHover] = useStateS(null);
const w = 1200; const h = 220;
const path = useMemoS(() => {
if (!data || !data.length) return { line:"", area:"" };
const vs = data.map(d=>d.v); const min=Math.min(...vs); const max=Math.max(...vs);
const range = max - min || 1;
const pad = 12;
const px = (i) => (i / (data.length - 1)) * (w - 2*pad) + pad;
const py = (v) => h - pad - ((v - min) / range) * (h - 2*pad);
let line = ""; data.forEach((d, i) => { line += (i===0?"M":"L") + px(i) + "," + py(d.v) + " "; });
const area = line + ` L${w-pad},${h} L${pad},${h} Z`;
return { line, area, px, py, min, max };
}, [data]);
const isPositive = data[data.length-1].v > data[0].v;
const color = isPositive ? "var(--bull)" : "var(--bear)";
function onMove(e) {
const r = ref.current.getBoundingClientRect();
const x = (e.clientX - r.left) / r.width * w;
const i = Math.round((x - 12) / (w - 24) * (data.length - 1));
if (i >= 0 && i < data.length) setHover({ i, x: path.px(i), y: path.py(data[i].v), v: data[i].v });
}
return (
setHover(null)}>
{hover && (
JOUR {hover.i}
€{hover.v.toLocaleString("fr-FR", { maximumFractionDigits:0 })}
)}
);
}
function PositionsTable({ positions }) {
const isMobile = typeof window !== "undefined" && window.innerWidth < 880;
if (positions.length === 0) {
return Aucune position ouverte
;
}
if (isMobile) {
return (
);
}
return (
| PAIR | DIR | ENTRY | CURRENT | P&L | SL/TP | |
{positions.map(p => )}
);
}
function PositionCardMobile({ p }) {
const animPnl = useCountUp(p.pnlEur);
const animPct = useCountUp(p.pnlPct);
const up = p.pnlEur >= 0;
return (
{p.pair}
x{p.lev} · {p.isReal?"RÉEL":"PAPER"}
{up?"+":"−"}€{Math.abs(animPnl).toFixed(2)}
{fmtPct(animPct)}
SL/TP1{fmtNum(p.sl)}
{fmtNum(p.tp1)}
);
}
function PositionRow({ p }) {
const animPnl = useCountUp(p.pnlEur);
const animPct = useCountUp(p.pnlPct);
const up = p.pnlEur >= 0;
return (
{p.pair}
x{p.lev} · {p.opened}
|
|
{fmtNum(p.entry)} |
{fmtNum(p.current)} |
{up?"+":"−"}€{Math.abs(animPnl).toFixed(2)}
{fmtPct(animPct)}
|
↓ {fmtNum(p.sl)}
↑ {fmtNum(p.tp1)}
|
|
);
}
function TopSignalRow({ signal }) {
return (
{signal.pair}/USDT
{signal.note}
);
}
function ActivityRow({ item }) {
const colorMap = { bull:"var(--bull)", bear:"var(--bear)", accent:"var(--accent-400)", muted:"var(--text-muted)" };
const iconMap = { "trend-up": Icon.TrendUp, "trend-down": Icon.TrendDown, "zap": Icon.Zap, "shield": Icon.Shield, "x": Icon.X };
const Ic = iconMap[item.icon] || Icon.Dot;
return (
{item.t}
{item.title}
{item.desc}
);
}
/* ─── Crypto chart card (replaces equity curve) ───────────────── */
function CryptoChartCard() {
const PAIR_OPTIONS = [
{ id: "BTC/USDT:USDT", sym: "BTC" },
{ id: "ETH/USDT:USDT", sym: "ETH" },
{ id: "SOL/USDT:USDT", sym: "SOL" },
{ id: "XAU/USDT:USDT", sym: "XAU" },
{ id: "DOGE/USDT:USDT", sym: "DOGE" },
];
const TF_OPTIONS = [
{ id: "1D", bar: "15m", limit: 96 },
{ id: "1W", bar: "1H", limit: 168 },
{ id: "1M", bar: "4H", limit: 180 },
{ id: "ALL", bar: "1D", limit: 365 },
];
const [pair, setPair] = useStateS(PAIR_OPTIONS[0]);
const [tf, setTf] = useStateS(TF_OPTIONS[1]);
const [data, setData] = useStateS([]);
const [loading, setLoad] = useStateS(true);
useEffectS(() => {
const key = new URLSearchParams(location.search).get("key");
const refresh = (initial) => {
if (initial) setLoad(true);
fetch(`/api/candles?pair=${encodeURIComponent(pair.id)}&bar=${tf.bar}&limit=${tf.limit}`, { credentials: "include" })
.then(r => r.json())
.then(j => {
const arr = (j.candles || []).map((c, i) => ({ t: c.t || i, v: parseFloat(c.close) }));
setData(arr);
if (initial) setLoad(false);
})
.catch(() => { if (initial) setLoad(false); });
};
refresh(true);
const t = setInterval(() => refresh(false), 30000); // refresh prix toutes les 30s
return () => clearInterval(t);
}, [pair.id, tf.id]);
// Lit le prix LIVE depuis le WS si dispo (sinon last close)
const store = window.useStore ? window.useStore() : {};
const livePx = store.PRICES && store.PRICES[pair.sym];
const last = livePx || (data.length ? data[data.length - 1].v : 0);
const first = data.length ? data[0].v : 0;
const chg = first > 0 ? ((last - first) / first) * 100 : 0;
const up = chg >= 0;
return (
{pair.sym}/USDT
{loading ? "—" : fmtNum(last)}
{fmtPct(chg)}
PRIX MARCHÉ · {tf.bar.toUpperCase()} · OKX
{PAIR_OPTIONS.map(p => (
setPair(p)}>
{p.sym}
))}
{TF_OPTIONS.map(t => (
setTf(t)}>
{t.id}
))}
{loading ? (
Chargement…
) : data.length < 2 ? (
Pas de données
) : (
)}
);
}
function PriceCurve({ data, up }) {
const ref = useRefS(null);
const [hover, setHover] = useStateS(null);
const w = 1200, h = 220;
const path = useMemoS(() => {
if (!data || data.length < 2) return { line:"", area:"" };
const vs = data.map(d=>d.v);
const min = Math.min(...vs), max = Math.max(...vs);
const range = max - min || 1;
const pad = 12;
const px = (i) => (i / (data.length - 1)) * (w - 2*pad) + pad;
const py = (v) => h - pad - ((v - min) / range) * (h - 2*pad);
let line = ""; data.forEach((d, i) => { line += (i===0?"M":"L") + px(i) + "," + py(d.v) + " "; });
const area = line + ` L${w-pad},${h} L${pad},${h} Z`;
return { line, area, px, py, min, max };
}, [data]);
function onMove(e) {
const r = ref.current.getBoundingClientRect();
const x = (e.clientX - r.left) / r.width * w;
const i = Math.round((x - 12) / (w - 24) * (data.length - 1));
if (i >= 0 && i < data.length) setHover({ i, x: path.px(i), y: path.py(data[i].v), v: data[i].v });
}
const stroke = up ? "var(--bull)" : "var(--bear)";
const fill = up ? "rgba(16,185,129,0.18)" : "rgba(239,68,68,0.18)";
return (
setHover(null)}>
{hover && (
POINT {hover.i + 1}/{data.length}
{fmtNum(hover.v)}
)}
);
}
Object.assign(window, { DashboardScreen });