/* ═══════════════════════════════════════════════════════════════ TRADING screen — cockpit positions actives Chart lightweight par position + actions rapides + KPIs live ═══════════════════════════════════════════════════════════════ */ const { useEffect: useEffectT, useRef: useRefT, useState: useStateT, useMemo: useMemoT } = React; const TF_BAR_T = { M1:"1m", M5:"5m", M15:"15m", H1:"1H", H4:"4H" }; function TradingScreen() { const store = window.useStore ? window.useStore() : {}; const positions = store.POSITIONS || []; const [filter, setFilter] = useStateT("all"); const filtered = positions.filter(p => { if (filter === "all") return true; if (filter === "paper") return !p.isReal; if (filter === "real") return p.isReal; if (filter === "long") return p.direction === "LONG"; if (filter === "short") return p.direction === "SHORT"; return true; }); // KPIs live const totalPnl = positions.reduce((s, p) => s + (p.pnlEur || 0), 0); const longCount = positions.filter(p => p.direction === "LONG").length; const shortCount = positions.filter(p => p.direction === "SHORT").length; const capital = store.STATUS?.capital || 200; const expoSum = positions.reduce((s, p) => s + (p.entry * (p.qty || 1)), 0); const expoPct = capital > 0 ? (expoSum / (capital * 10)) * 100 : 0; const now = Date.now(); const avgDur = positions.length === 0 ? 0 : positions.reduce((s, p) => s + (p.raw?.entry_ts_ms ? (now - p.raw.entry_ts_ms) : 0), 0) / positions.length; return (

Trading Center {positions.length} actives

Cockpit · gestion positions ouvertes · WS live 300ms
{[ { id:"all", l:"Toutes", n: positions.length }, { id:"paper", l:"PAPER", n: positions.filter(p => !p.isReal).length }, { id:"real", l:"RÉEL", n: positions.filter(p => p.isReal).length }, { id:"long", l:"LONG", n: longCount }, { id:"short", l:"SHORT", n: shortCount }, ].map(c => (
setFilter(c.id)}> {c.l} {c.n}
))}
{/* KPIs LIVE */}
P&L OUVERT
= 0 ? "var(--bull)":"var(--bear)", letterSpacing:"-0.03em" }}> {totalPnl >= 0 ? "+" : ""}{totalPnl.toFixed(2)}€
non réalisé · live
POSITIONS
{positions.length}
▲ {longCount} LONG · ▼ {shortCount} SHORT
EXPOSITION
{expoPct.toFixed(0)}%
capital engagé · {expoSum.toFixed(0)}€
TEMPS MOYEN
{fmtDuration(avgDur)}
durée ouverte moyenne
{/* POSITIONS */} {filtered.length === 0 ? (

Aucune position active

{filter === "all" ? "Aucun trade ouvert pour l'instant." : `Aucune position dans le filtre "${filter}".`}

) : (
{filtered.map(p => )}
)}
); } /* ─── Position card avec chart embarqué + actions ───────────────── */ function TradingPositionCard({ position: p }) { const containerRef = useRefT(null); const seriesRef = useRefT(null); const [tf, setTf] = useStateT("M15"); const [candles, setCandles] = useStateT([]); const store = window.useStore ? window.useStore() : {}; const livePx = (store.PRICES && store.PRICES[p.pair]) || p.current; const KEY = new URLSearchParams(location.search).get("key"); const [now, setNow] = useStateT(Date.now()); useEffectT(() => { const t = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(t); }, []); const fullPair = (p.pair + "/USDT:USDT"); const duration = p.raw?.entry_ts_ms ? (now - p.raw.entry_ts_ms) : 0; // % distances const isLong = p.direction === "LONG"; const slDist = p.sl ? ((livePx - p.sl) / livePx * 100 * (isLong ? 1 : -1)) : 0; const tp1Dist = p.tp1 ? ((p.tp1 - livePx) / livePx * 100 * (isLong ? 1 : -1)) : 0; const tp2Dist = p.tp2 ? ((p.tp2 - livePx) / livePx * 100 * (isLong ? 1 : -1)) : 0; // attention : slDist négative en LONG si prix proche du SL (à corriger) // formule directe : pct distance from livePx to target const dist = (target) => target ? ((target - livePx) / livePx * 100) : 0; const slPct = dist(p.sl); const tp1Pct = dist(p.tp1); const tp2Pct = dist(p.tp2); // Fetch candles useEffectT(() => { const bar = TF_BAR_T[tf] || "15m"; fetch(`/api/candles?pair=${encodeURIComponent(fullPair)}&bar=${bar}&limit=120`, { credentials: "include" }) .then(r => r.json()) .then(j => { const arr = (j.candles || []).map(c => ({ time: Math.floor(c.t / 1000), open: c.open, high: c.high, low: c.low, close: c.close, })); setCandles(arr); }).catch(() => {}); }, [tf, p.pair]); // Chart init useEffectT(() => { if (!containerRef.current || !window.LightweightCharts || candles.length < 2) return; const { createChart } = window.LightweightCharts; const chart = createChart(containerRef.current, { width: containerRef.current.clientWidth, height: 260, layout: { background:{ type:"solid", color:"transparent" }, textColor:"#6B678F", fontFamily:"Geist Mono, monospace" }, grid: { vertLines:{ color:"#1F1F3A" }, horzLines:{ color:"#1F1F3A" } }, crosshair: { mode:0 }, rightPriceScale: { borderColor:"#2A2A4D", textColor:"#6B678F" }, timeScale: { borderColor:"#2A2A4D", timeVisible:true, secondsVisible:false }, }); const cs = chart.addCandlestickSeries({ upColor:"#10B981", downColor:"#EF4444", wickUpColor:"#10B981", wickDownColor:"#EF4444", borderVisible:false, }); cs.setData(candles); seriesRef.current = cs; // Lignes ENTRY / SL / TP1 / TP2 if (p.entry) cs.createPriceLine({ price:p.entry, color:"#A855F7", lineWidth:2, lineStyle:0, title:"ENTRY", axisLabelVisible:true }); if (p.sl) cs.createPriceLine({ price:p.sl, color:"#EF4444", lineWidth:1, lineStyle:2, title:"SL", axisLabelVisible:true }); if (p.tp1) cs.createPriceLine({ price:p.tp1, color:"#10B981", lineWidth:1, lineStyle:2, title:"TP1", axisLabelVisible:true }); if (p.tp2) cs.createPriceLine({ price:p.tp2, color:"#10B981", lineWidth:1, lineStyle:2, title:"TP2", axisLabelVisible:true }); // Marker entrée if (p.raw?.entry_ts_ms) { const entrySec = Math.floor(p.raw.entry_ts_ms / 1000); const closeCandle = candles.find(c => c.time >= entrySec) || candles[candles.length - 1]; if (closeCandle) { cs.setMarkers([{ time: closeCandle.time, position: isLong ? "belowBar" : "aboveBar", color: isLong ? "#10B981" : "#EF4444", shape: isLong ? "arrowUp" : "arrowDown", text: `${p.direction} @${fmtNum(p.entry)}`, }]); } } chart.timeScale().fitContent(); const ro = new ResizeObserver(() => { if (containerRef.current) chart.applyOptions({ width: containerRef.current.clientWidth, height: 260 }); }); ro.observe(containerRef.current); return () => { ro.disconnect(); seriesRef.current = null; chart.remove(); }; }, [candles, tf]); // Live tick → update dernière bougie useEffectT(() => { if (!livePx || !seriesRef.current || candles.length === 0) return; const last = candles[candles.length - 1]; seriesRef.current.update({ time: last.time, open: last.open, high: Math.max(last.high, livePx), low: Math.min(last.low, livePx), close: livePx, }); }, [livePx]); // Actions async function closeAll() { if (!confirm(`Fermer 100% ${p.pair} ${p.direction} ?`)) return; await window.fbClosePos(p); emitToast({ type:"success", msg:`Position ${p.pair} fermée` }); } async function closeHalf() { if (!p.isReal) { emitToast({ type:"warn", msg:"Clôture partielle dispo uniquement sur positions RÉELLES" }); return; } if (!confirm(`Fermer 50% de ${p.pair} ${p.direction} ?`)) return; try { await fetch(`/api/close_real_partial/${p.raw.instId}/50`, { method:"POST", credentials:"include" }); emitToast({ type:"success", msg:"50% fermé" }); } catch (e) { emitToast({ type:"error", msg:e.message }); } } async function moveBreakEven() { if (p.isReal) { emitToast({ type:"warn", msg:"Break-even auto dispo uniquement sur PAPER pour l'instant" }); return; } try { const r = await fetch(`/api/move_sl_be/${p.id}`, { method:"POST", credentials:"include" }).then(r => r.json()); emitToast({ type: r.ok?"success":"error", msg: r.ok?`SL → BE @${r.new_trail_sl}` : (r.msg||"erreur") }); } catch (e) { emitToast({ type:"error", msg:e.message }); } } const pnlUp = p.pnlEur >= 0; const pnlColor = pnlUp ? "var(--bull)" : "var(--bear)"; return (
{/* HEADER */}
{p.pair}/USDT x{p.lev} {p.isReal ? "RÉEL" : "PAPER"}
Entrée @{fmtNum(p.entry)} · ouvert depuis {fmtDuration(duration)}
{pnlUp ? "+" : ""}{p.pnlEur.toFixed(2)}€ {pnlUp ? "+" : ""}{p.pnlPct.toFixed(2)}%
{/* TF selector + chart */}
{Object.keys(TF_BAR_T).map(t => ( ))}
{/* INFOS LIVE */}
= 0 ? "+" : ""}${slPct.toFixed(2)}%`} cls={Math.abs(slPct) < 0.3 ? "bear" : isLong ? (slPct < 0 ? "bear" : "muted") : (slPct > 0 ? "bear" : "muted")}/> = 0 ? "+" : ""}${tp1Pct.toFixed(2)}%`} cls={Math.abs(tp1Pct) < 0.3 ? "bull" : isLong ? (tp1Pct > 0 ? "bull" : "muted") : (tp1Pct < 0 ? "bull" : "muted")}/> = 0 ? "+" : ""}${tp2Pct.toFixed(2)}%`} cls={isLong ? (tp2Pct > 0 ? "bull" : "muted") : (tp2Pct < 0 ? "bull" : "muted")}/>
{/* ACTIONS */}
); } function InfoCell({ label, val, cls }) { return (
{label} {val}
); } function fmtDuration(ms) { if (!ms || ms <= 0) return "—"; const s = Math.floor(ms / 1000); if (s < 60) return `${s}s`; if (s < 3600) return `${Math.floor(s/60)}m ${s%60}s`; if (s < 86400) return `${Math.floor(s/3600)}h ${Math.floor((s%3600)/60)}m`; return `${Math.floor(s/86400)}j ${Math.floor((s%86400)/3600)}h`; } Object.assign(window, { TradingScreen });