/* ═══════════════════════════════════════════════════════════════ FOXBOT PRO — MOBILE DASHBOARD Hero P&L · KPI 2x2 · Positions snap · Signaux · Activité ═══════════════════════════════════════════════════════════════ */ const { useState: useStateMD, useEffect: useEffectMD, useRef: useRefMD, useMemo: useMemoMD } = React; function MobileDashboard({ onAction, onRoute }) { const { KPI, POSITIONS, ACTIVITY, EQUITY, SIGNALS } = window.MOCK; const top3 = useMemoMD(() => [...SIGNALS].filter(s => s.state === "active").sort((a, b) => b.score - a.score).slice(0, 3), [SIGNALS] ); return (
{/* ─── Hero P&L ─── */} p.v)}/> {/* ─── KPI Grid 2x2 ─── */}
{KPI.winRate.val.toFixed(1)}%} foot={ <>
{KPI.winRate.wins}W·{KPI.winRate.losses}L } /> {KPI.trades.d}/ jour} foot={ 7J {KPI.trades.w} · 30J {KPI.trades.m} } sparkline={makeWaveSpark(KPI.trades.w)} /> {fmtPct(KPI.drawdown.current)}} foot={ <>
max {fmtPct(KPI.drawdown.max)} } /> {POSITIONS.length}actives} foot={ Exposition €2 420 } stack={POSITIONS.slice(0, 3).map(p => p.pair)} />
{/* ─── Positions actives (horizontal snap) ─── */} {POSITIONS.length > 0 && (

Positions {POSITIONS.length}

)} {/* ─── Top opportunités ─── */}

Opportunités

{top3.map(s => )}
{/* ─── Activité ─── */}

Activité

1H
{ACTIVITY.slice(0, 5).map((a, i) => )}
); } /* ─── Hero P&L card ─── */ function MHero({ kpi, positions, sparkData }) { const animated = useCountUp(kpi.pnlTotal.eur, 1100); const animPct = useCountUp(kpi.pnlTotal.pct, 1100); const up = kpi.pnlTotal.eur >= 0; const sparkPath = useMemoMD(() => { if (!sparkData || sparkData.length < 2) return ""; const min = Math.min(...sparkData), max = Math.max(...sparkData); const range = max - min || 1; const w = 380, h = 60; return sparkData.map((v, i) => { const x = (i / (sparkData.length - 1)) * w; const y = h - 4 - ((v - min) / range) * (h - 8); return (i === 0 ? "M" : "L") + x.toFixed(1) + "," + y.toFixed(1); }).join(" "); }, [sparkData]); return (
P&L Total · 30J LIVE
{up ? "+" : "−"}{Math.abs(animated).toLocaleString("fr-FR", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
{up ? : } {fmtPct(animPct)} {positions} positions
{/* background sparkline */}
); } /* ─── KPI tile ─── */ function MKpi({ label, value, foot, sparkline, stack }) { return (
{label}
{value}
{sparkline && (
)} {stack && (
{stack.map((s, i) => ( {s.slice(0, 1)} ))}
)}
{foot}
); } /* ─── Solde live (USDC réel ou capital paper selon mode) ─── */ function MBalancePill() { const store = window.useStore ? window.useStore() : (window.MOCK || {}); const st = store.STATUS || {}; const mode = store.MODE || "PAPER"; const isLive = mode === "LIVE"; const value = isLive ? Number(st.usdc || 0) : Number(st.capital || 200); const animVal = useCountUp(value, 600, isLive ? 2 : 0); return ( {isLive ? "Solde" : "Capital"} {isLive ? "$" + animVal.toFixed(2) : "€" + Math.round(animVal)} ); } function makeWaveSpark(seed) { const pts = []; for (let i = 0; i < 12; i++) { pts.push(8 + Math.sin(i * 0.7 + seed * 0.1) * 4 + Math.cos(i * 0.4) * 2); } return pts.map((y, i) => (i === 0 ? "M" : "L") + (i * 10) + "," + y.toFixed(1)).join(" "); } /* ─── Positions horizontal scroll ─── */ function MPositionsScroll({ positions }) { const [active, setActive] = useStateMD(0); const ref = useRefMD(null); useEffectMD(() => { const el = ref.current; if (!el) return; const onScroll = () => { const cards = el.querySelectorAll(".m-pos-card"); const center = el.scrollLeft + el.clientWidth / 2; let best = 0, bestDist = Infinity; cards.forEach((c, i) => { const cx = c.offsetLeft + c.offsetWidth / 2; const d = Math.abs(cx - center); if (d < bestDist) { bestDist = d; best = i; } }); setActive(best); }; el.addEventListener("scroll", onScroll, { passive: true }); return () => el.removeEventListener("scroll", onScroll); }, []); return (
{positions.map(p => )}
{positions.length > 1 && (
{positions.map((_, i) => )}
)}
); } function MPositionCard({ p }) { const animPnl = useCountUp(p.pnlEur); const animPct = useCountUp(p.pnlPct); const up = p.pnlEur >= 0; const sparkData = useMemoMD(() => { // synthesize a short live shape from entry → current const out = []; const start = p.entry, end = p.current; for (let i = 0; i < 24; i++) { const t = i / 23; const wobble = Math.sin(i * 0.8 + p.id.charCodeAt(1)) * (Math.abs(end - start) * 0.4); out.push(start + (end - start) * t + wobble); } return out; }, [p.entry, p.current, p.id]); return (
{p.pair}/USDT {p.direction} · ouvert {p.opened}
×{p.lev}
{up ? "+" : "−"}€{Math.abs(animPnl).toFixed(2)} {fmtPct(animPct)}
Entry
{fmtNum(p.entry)}
SL
{fmtNum(p.sl)}
TP1
{fmtNum(p.tp1)}
); } function MiniLiveSpark({ data, up }) { const path = useMemoMD(() => { if (!data || data.length < 2) return ""; const min = Math.min(...data), max = Math.max(...data); const range = max - min || 1; const w = 300, h = 56; return data.map((v, i) => { const x = (i / (data.length - 1)) * w; const y = h - 3 - ((v - min) / range) * (h - 6); return (i === 0 ? "M" : "L") + x.toFixed(1) + "," + y.toFixed(1); }).join(" "); }, [data]); const c = up ? "#10B981" : "#EF4444"; const id = useMemoMD(() => "msp-" + Math.random().toString(36).slice(2, 8), []); return ( ); } /* ─── Compact signal row ─── */ function MSignalRow({ signal, onAction }) { const up = signal.direction === "LONG"; return (
{ HAPTIC.tap(); onAction && onAction("preview", signal); }}>
{signal.pair}/USDT
{signal.note}
{fmtPct(signal.chg24h)}
); } /* ─── Activity row ─── */ function MActivityRow({ item }) { 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.title}
{item.desc}
{item.t}
); } Object.assign(window, { MobileDashboard });