import { useEffect, useRef, useState, useCallback } from "react"; import { createChart, CandlestickSeries, LineSeries, createSeriesMarkers, } from "lightweight-charts"; const TIMEFRAMES = [ { label: "1m", value: 1 }, { label: "3m", value: 3 }, { label: "5m", value: 5 }, { label: "15m", value: 15 }, { label: "30m", value: 30 }, { label: "1H", value: 60 }, ]; const COLORS = { bg: "#0a0a12", surface: "#12121e", surfaceLight: "#1a1a2e", border: "#1e1e35", borderLight: "#2a2a45", text: "#c8c8d4", textDim: "#6a6a80", textBright: "#eaeaf0", accent: "#6366f1", accentDim: "rgba(99, 102, 241, 0.15)", bullish: "#22c55e", bullishDim: "rgba(34, 197, 94, 0.12)", bearish: "#ef4444", bearishDim: "rgba(239, 68, 68, 0.12)", ob: "#3b82f6", obBearish: "#f59e0b", fvg: "#a855f7", liquidity: "#06b6d4", }; function App() { const chartContainerRef = useRef(null); const chartRef = useRef(null); const candleSeriesRef = useRef(null); const markersRef = useRef(null); const [timeframe, setTimeframe] = useState(5); const [indicators, setIndicators] = useState({ structure: true, orderBlocks: true, fvg: false, liquidity: false, }); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const toggleIndicator = (key) => { setIndicators((prev) => ({ ...prev, [key]: !prev[key] })); }; const loadData = useCallback(async () => { setLoading(true); try { const [candleRes, indicatorRes] = await Promise.all([ fetch(`http://localhost:8000/api/candles?timeframe=${timeframe}`), fetch(`http://localhost:8000/api/indicators?timeframe=${timeframe}`), ]); const candleData = await candleRes.json(); const indicatorData = await indicatorRes.json(); const formatted = candleData.candles.map((c) => ({ time: Math.floor(new Date(c.time).getTime() / 1000), open: c.open, high: c.high, low: c.low, close: c.close, })); if (candleSeriesRef.current) { candleSeriesRef.current.setData(formatted); } // Build markers based on active indicators const times = indicatorData.candle_times; const markers = []; if (indicators.structure) { indicatorData.structure.forEach((s) => { if (s.index < times.length) { markers.push({ time: Math.floor(new Date(times[s.index]).getTime() / 1000), position: s.type === "high" ? "aboveBar" : "belowBar", color: s.label === "HH" || s.label === "HL" ? COLORS.bullish : COLORS.bearish, shape: s.type === "high" ? "arrowDown" : "arrowUp", text: s.label, }); } }); } if (indicators.orderBlocks) { indicatorData.order_blocks.forEach((ob) => { if (ob.index < times.length) { markers.push({ time: Math.floor(new Date(times[ob.index]).getTime() / 1000), position: ob.type === "bullish" ? "belowBar" : "aboveBar", color: ob.type === "bullish" ? COLORS.ob : COLORS.obBearish, shape: "square", text: "OB", }); } }); } if (indicators.fvg) { indicatorData.fvgs.forEach((f) => { if (f.index < times.length) { markers.push({ time: Math.floor(new Date(times[f.index]).getTime() / 1000), position: f.type === "bullish" ? "belowBar" : "aboveBar", color: COLORS.fvg, shape: "circle", text: "FVG", }); } }); } if (indicators.liquidity) { indicatorData.liquidity.forEach((l) => { l.indexes.forEach((idx) => { if (idx < times.length) { markers.push({ time: Math.floor(new Date(times[idx]).getTime() / 1000), position: l.type === "equal_highs" ? "aboveBar" : "belowBar", color: COLORS.liquidity, shape: "circle", text: l.type === "equal_highs" ? "EQH" : "EQL", }); } }); }); } markers.sort((a, b) => a.time - b.time); // Remove old markers if (markersRef.current) { markersRef.current.setMarkers([]); } markersRef.current = createSeriesMarkers(candleSeriesRef.current, markers); chartRef.current.timeScale().fitContent(); // Stats const bullishOB = indicatorData.order_blocks.filter( (o) => o.type === "bullish" ).length; const bearishOB = indicatorData.order_blocks.filter( (o) => o.type === "bearish" ).length; const bullishFVG = indicatorData.fvgs.filter( (f) => f.type === "bullish" ).length; const bearishFVG = indicatorData.fvgs.filter( (f) => f.type === "bearish" ).length; setStats({ candles: candleData.candles.length, swings: indicatorData.swings.length, structure: indicatorData.structure.length, orderBlocks: indicatorData.order_blocks.length, bullishOB, bearishOB, fvgs: indicatorData.fvgs.length, bullishFVG, bearishFVG, liquidity: indicatorData.liquidity.length, }); } catch (err) { console.error("Failed to load data:", err); } setLoading(false); }, [timeframe, indicators]); // Create chart once useEffect(() => { if (!chartContainerRef.current) return; const chart = createChart(chartContainerRef.current, { width: chartContainerRef.current.clientWidth, height: 560, layout: { background: { color: COLORS.surface }, textColor: COLORS.textDim, fontFamily: "'IBM Plex', 'Fira Code', monospace", fontSize: 11, }, grid: { vertLines: { color: COLORS.border }, horzLines: { color: COLORS.border }, }, crosshair: { vertLine: { color: "rgba(99, 102, 241, 0.3)", labelBackgroundColor: COLORS.accent, }, horzLine: { color: "rgba(99, 102, 241, 0.3)", labelBackgroundColor: COLORS.accent, }, }, rightPriceScale: { borderColor: COLORS.border, textColor: COLORS.textDim, }, timeScale: { borderColor: COLORS.border, timeVisible: true, secondsVisible: false, }, }); const candleSeries = chart.addSeries(CandlestickSeries, { upColor: COLORS.bullish, downColor: COLORS.bearish, borderVisible: false, wickUpColor: COLORS.bullish, wickDownColor: COLORS.bearish, }); chartRef.current = chart; candleSeriesRef.current = candleSeries; const handleResize = () => { if (chartContainerRef.current) { chart.applyOptions({ width: chartContainerRef.current.clientWidth, }); } }; window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize", handleResize); chart.remove(); }; }, []); // Load data when timeframe or indicators change useEffect(() => { if (chartRef.current && candleSeriesRef.current) { loadData(); } }, [loadData]); return (