diff --git a/backend/api/routes.py b/backend/api/routes.py index 3cea13f..7ea0ec6 100644 --- a/backend/api/routes.py +++ b/backend/api/routes.py @@ -35,7 +35,6 @@ def get_candles(timeframe: int = 5): for c in candles ] } - @app.get("/api/indicators") def get_indicators(timeframe: int = 5): candles_1m = load_candles("data/data.csv") @@ -47,7 +46,10 @@ def get_indicators(timeframe: int = 5): fvgs = find_fvgs(candles) obs = find_order_blocks(candles, structure) + candle_times = [c.time_open.isoformat() for c in candles] + return { + "candle_times": candle_times, "swings": swings, "structure": structure, "liquidity": levels, diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 930e764..89be450 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,53 +1,595 @@ -import { useEffect, useRef } from "react"; -import { createChart } from "lightweight-charts"; +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(() => { - const chart = createChart(chartRef.current, { - width: window.innerWidth - 40, - height: 600, + if (!chartContainerRef.current) return; + + const chart = createChart(chartContainerRef.current, { + width: chartContainerRef.current.clientWidth, + height: 560, layout: { - background: { color: "#1a1a2e" }, - textColor: "#e0e0e0", + background: { color: COLORS.surface }, + textColor: COLORS.textDim, + fontFamily: "'IBM Plex', 'Fira Code', monospace", + fontSize: 11, }, grid: { - vertLines: { color: "#2a2a3e" }, - horzLines: { color: "#2a2a3e" }, + 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.addCandlestickSeries({ - upColor: "#26a69a", - downColor: "#ef5350", + const candleSeries = chart.addSeries(CandlestickSeries, { + upColor: COLORS.bullish, + downColor: COLORS.bearish, borderVisible: false, - wickUpColor: "#26a69a", - wickDownColor: "#ef5350", + wickUpColor: COLORS.bullish, + wickDownColor: COLORS.bearish, }); - fetch("http://localhost:8000/api/candles?timeframe=5") - .then((res) => res.json()) - .then((data) => { - const formatted = data.candles.map((c) => ({ - time: Math.floor(new Date(c.time).getTime() / 1000), - open: c.open, - high: c.high, - low: c.low, - close: c.close, - })); - candleSeries.setData(formatted); - }); + chartRef.current = chart; + candleSeriesRef.current = candleSeries; - return () => chart.remove(); + 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 ( -