/* ═══════════════════════════════════════════════════════════════ CHARTS screen — lightweight-charts integration · REAL OKX data ═══════════════════════════════════════════════════════════════ */ const { useEffect: useEffectC, useRef: useRefC, useState: useStateC } = React; const TF_BAR_MAP = { M1:"1m", M5:"5m", M15:"15m", H1:"1H", H4:"4H", D1:"1D", W1:"1W" }; const TF_LIMITS = { M1:300, M5:300, M15:300, H1:300, H4:300, D1:365, W1:200 }; function ChartsScreen() { const containerRef = useRefC(null); const candleSeriesRef = useRefC(null); const [pair, setPair] = useStateC("BTC"); const [tf, setTf] = useStateC("M15"); const [indicators, setIndicators] = useStateC({ rsi: false, macd: false, ema20: true, ema50: true, bb: false, vol: true }); const [analysis, setAnalysis] = useStateC(null); const [analysisLoading, setAnalysisLoading] = useStateC(false); const [candles, setCandles] = useStateC([]); const [info, setInfo] = useStateC({ price: 0, chg: 0, vol24h: 0, funding: null }); const [loading, setLoading] = useStateC(true); // Subscribe au STORE pour les prix WS live (300ms) const store = window.useStore ? window.useStore() : {}; const livePx = store.PRICES && store.PRICES[pair]; const fullPair = pair + "/USDT:USDT"; const KEY = new URLSearchParams(location.search).get("key"); // Fetch real candles useEffectC(() => { setLoading(true); const bar = TF_BAR_MAP[tf] || "15m"; const limit = TF_LIMITS[tf] || 300; fetch(`/api/candles?pair=${encodeURIComponent(fullPair)}&bar=${bar}&limit=${limit}`, { 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, volume: c.vol || 0, })); setCandles(arr); if (arr.length > 0) { const last = arr[arr.length - 1]; const secPerBar = bar==="1m"?60:bar==="5m"?300:bar==="15m"?900:bar==="1H"?3600:bar==="4H"?14400:86400; const idx24h = Math.max(0, arr.length - Math.floor(86400 / secPerBar)); const ref24h = arr[idx24h] || arr[0]; const chg = ((last.close - ref24h.open) / ref24h.open) * 100; const vol24h = arr.slice(idx24h).reduce((s, c) => s + (c.volume || 0), 0); setInfo(prev => ({ ...prev, price: last.close, chg, vol24h })); } setLoading(false); }) .catch(() => setLoading(false)); }, [pair, tf]); // Fetch funding rate from current signal useEffectC(() => { fetch(`/api/signal`, { credentials: "include" }).then(r => r.json()).then(j => { const sig = (j.signals || []).find(s => (s.paire || "").startsWith(pair + "/")); if (sig) setInfo(prev => ({ ...prev, funding: sig.funding_raw })); }).catch(() => {}); }, [pair]); // Render chart useEffectC(() => { if (!containerRef.current || !window.LightweightCharts || candles.length < 2) return; const { createChart } = window.LightweightCharts; const chart = createChart(containerRef.current, { width: containerRef.current.clientWidth, height: containerRef.current.clientHeight, layout: { background: { type: "solid", color: "transparent" }, textColor: "#6B678F", fontFamily: "Geist Mono, JetBrains 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 candleSeries = chart.addCandlestickSeries({ upColor: "#10B981", downColor: "#EF4444", wickUpColor: "#10B981", wickDownColor: "#EF4444", borderVisible: false, }); candleSeries.setData(candles); candleSeriesRef.current = candleSeries; if (indicators.vol) { const vol = chart.addHistogramSeries({ priceFormat: { type: "volume" }, priceScaleId: "", color: "#A855F7", }); vol.priceScale().applyOptions({ scaleMargins: { top: 0.85, bottom: 0 } }); vol.setData(candles.map(c => ({ time: c.time, value: c.volume, color: c.close >= c.open ? "rgba(16,185,129,0.4)" : "rgba(239,68,68,0.4)", }))); } if (indicators.ema20) { const ema20 = chart.addLineSeries({ color: "#C084FC", lineWidth: 1.5, priceLineVisible: false, lastValueVisible: false }); const out = []; let e = candles[0].close; candles.forEach(d => { e = e * 0.905 + d.close * 0.095; out.push({ time: d.time, value: e }); }); ema20.setData(out); } if (indicators.ema50) { const ema50 = chart.addLineSeries({ color: "#EC4899", lineWidth: 1.5, priceLineVisible: false, lastValueVisible: false }); const out = []; let e = candles[0].close; candles.forEach(d => { e = e * 0.961 + d.close * 0.039; out.push({ time: d.time, value: e }); }); ema50.setData(out); } if (indicators.bb) { const period = 20, mult = 2; const upper = [], mid = [], lower = []; for (let i = period - 1; i < candles.length; i++) { const slice = candles.slice(i - period + 1, i + 1).map(c => c.close); const m = slice.reduce((a,b) => a+b, 0) / period; const v = slice.reduce((a,b) => a + (b-m)*(b-m), 0) / period; const sd = Math.sqrt(v); const t = candles[i].time; mid.push({ time: t, value: m }); upper.push({ time: t, value: m + mult*sd }); lower.push({ time: t, value: m - mult*sd }); } chart.addLineSeries({ color: "rgba(168,85,247,0.4)", lineWidth: 1, priceLineVisible: false, lastValueVisible: false }).setData(upper); chart.addLineSeries({ color: "rgba(168,85,247,0.7)", lineWidth: 1, priceLineVisible: false, lastValueVisible: false }).setData(mid); chart.addLineSeries({ color: "rgba(168,85,247,0.4)", lineWidth: 1, priceLineVisible: false, lastValueVisible: false }).setData(lower); } // Position markers if open const store = window.STORE && window.STORE.data; if (store && store.POSITIONS) { const pos = store.POSITIONS.find(p => p.pair === pair); if (pos) { if (pos.entry) candleSeries.createPriceLine({ price: pos.entry, color: "#A855F7", lineWidth: 1, lineStyle: 2, title: "ENTRY", axisLabelVisible: true }); if (pos.sl) candleSeries.createPriceLine({ price: pos.sl, color: "#EF4444", lineWidth: 1, lineStyle: 2, title: "SL", axisLabelVisible: true }); if (pos.tp1) candleSeries.createPriceLine({ price: pos.tp1, color: "#10B981", lineWidth: 1, lineStyle: 2, title: "TP1", axisLabelVisible: true }); if (pos.tp2) candleSeries.createPriceLine({ price: pos.tp2, color: "#10B981", lineWidth: 1, lineStyle: 2, title: "TP2", axisLabelVisible: true }); } } chart.timeScale().fitContent(); const ro = new ResizeObserver(() => { if (containerRef.current) { chart.applyOptions({ width: containerRef.current.clientWidth, height: containerRef.current.clientHeight }); } }); ro.observe(containerRef.current); return () => { ro.disconnect(); candleSeriesRef.current = null; chart.remove(); }; }, [candles, indicators.ema20, indicators.ema50, indicators.vol, indicators.bb, pair]); // LIVE : à chaque tick WS, on update la dernière bougie du chart (close + high/low étirés) useEffectC(() => { if (!livePx || !candleSeriesRef.current || candles.length === 0) return; const last = candles[candles.length - 1]; candleSeriesRef.current.update({ time: last.time, open: last.open, high: Math.max(last.high, livePx), low: Math.min(last.low, livePx), close: livePx, }); // Met aussi à jour le prix + variation affichés en header setInfo(prev => { if (!candles.length) return prev; const ref24h = candles[0]; return { ...prev, price: livePx, chg: ((livePx - ref24h.open) / ref24h.open) * 100 }; }); }, [livePx]); async function runAiAnalysis() { setAnalysisLoading(true); setAnalysis(null); try { const j = await fetch(`/api/signal`, { credentials: "include" }).then(r => r.json()); const sig = (j.signals || []).find(s => (s.paire || "").startsWith(pair + "/")); if (!sig) { setAnalysis({ verdict: "Pas de signal disponible", conf: 0, points: [`${pair} n'est pas dans le scan en cours`], risk: "—" }); setAnalysisLoading(false); return; } const dir = sig.action === "WAIT" ? sig.bias : sig.action; setAnalysis({ verdict: `${dir} · ${sig.action === "WAIT" ? "biais en attente" : "setup validé"}`, conf: Math.round((sig.score || 0) * 10), points: [ `Score ${sig.score}/10 (raw ${sig.score_raw12 || "?"}/12) · Mode ${sig.scan_mode}`, `Multi-TF ${sig.tf_alignment || "?"} · ${sig.trend_4h}/${sig.trend_1h}/${sig.trend_15m}`, `Wyckoff : ${sig.wyckoff_phase || "—"}`, `R/R net ${sig.rr_net} (frais OKX inclus) · ATR ${sig.atr_pct}%`, `Volume ${sig.volume_pct || sig.volume_pct_moyenne}% · Funding ${sig.funding_rate}`, `BTC corr : ${sig.btc_correlation} · BTC.D ${sig.btc_dominance}`, ], risk: sig.quality_ok ? `Signal éligible. Risque ${sig.risque_max_eur}€ pour gain min ${sig.gain_min_eur}€.` : `Filtres non passés : ${sig.quality_msg}`, }); } catch (e) { setAnalysis({ verdict: "Erreur API", conf: 0, points: [String(e)], risk: "—" }); } setAnalysisLoading(false); } const up = info.chg >= 0; const pairsList = ["BTC","ETH","SOL","XAU","DOGE","XRP","ADA","SUI","LTC"]; const fundingPct = info.funding != null ? (info.funding * 100).toFixed(4) + "%" : "—"; return (