/* ═══════════════════════════════════════════════════════════════
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}".`}
window.location.hash="#/signaux"}>
Aller voir les Signaux
) : (
{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 => (
setTf(t)} style={{ padding:"4px 10px", fontSize:11 }}>{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 */}
FERMER 100%
FERMER 50%
BREAK-EVEN
{ window.location.hash="#/charts"; emitToast({ type:"info", msg:`Naviguer vers ${p.pair} sur Charts` }); }}>
OUVRIR CHART
);
}
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 });