From 26302c2d3068020b3a2cbf74ba81a55d84d15dde Mon Sep 17 00:00:00 2001 From: moen0 Date: Fri, 10 Apr 2026 20:11:56 +0200 Subject: [PATCH] fvg and ob indicators Front-end start --- backend/api/routes.py | 4 +- frontend/src/App.jsx | 602 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 575 insertions(+), 31 deletions(-) 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 ( -
-

noteQuant Backtester

-
+
+ + + {/* Header */} +
+
+
+
nQ
+
+
noteQuant
+
ICT/SMC Backtester
+
+
+
+
+
+ GBP/JPY + Forex +
+ {loading &&
} +
+
+ + {/* Controls */} +
+
+ Timeframe +
+ {TIMEFRAMES.map((tf) => ( + + ))} +
+
+ +
+ Indicators +
+ {[ + { key: "structure", label: "Structure", color: COLORS.bullish }, + { key: "orderBlocks", label: "Order Blocks", color: COLORS.ob }, + { key: "fvg", label: "FVG", color: COLORS.fvg }, + { key: "liquidity", label: "Liquidity", color: COLORS.liquidity }, + ].map((ind) => ( + + ))} +
+
+
+ + {/* Chart */} +
+
+
+ + {/* Stats */} + {stats && ( +
+
+ {stats.candles.toLocaleString()} + Candles +
+
+
+ {stats.swings} + Swings +
+
+
+ + {stats.bullishOB} + + Bull OB +
+
+ + {stats.bearishOB} + + Bear OB +
+
+
+ + {stats.fvgs} + + FVGs +
+
+
+ + {stats.liquidity} + + Liq Levels +
+
+ )}
); } +const styles = { + root: { + minHeight: "100vh", + background: COLORS.bg, + fontFamily: "'Outfit', sans-serif", + color: COLORS.text, + padding: "0", + }, + header: { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + padding: "16px 24px", + borderBottom: `1px solid ${COLORS.border}`, + }, + headerLeft: { + display: "flex", + alignItems: "center", + gap: "16px", + }, + headerRight: { + display: "flex", + alignItems: "center", + gap: "12px", + }, + logo: { + display: "flex", + alignItems: "center", + gap: "12px", + }, + logoIcon: { + width: "36px", + height: "36px", + borderRadius: "8px", + background: `linear-gradient(135deg, ${COLORS.accent}, #818cf8)`, + display: "flex", + alignItems: "center", + justifyContent: "center", + fontFamily: "'IBM Plex', monospace", + fontWeight: "700", + fontSize: "13px", + color: "#fff", + letterSpacing: "-0.5px", + }, + logoTitle: { + fontSize: "16px", + fontWeight: "600", + color: COLORS.textBright, + letterSpacing: "-0.3px", + }, + logoSub: { + fontSize: "11px", + color: COLORS.textDim, + fontFamily: "'IBM Plex', monospace", + letterSpacing: "0.5px", + textTransform: "uppercase", + }, + pairBadge: { + display: "flex", + alignItems: "center", + gap: "8px", + padding: "6px 12px", + background: COLORS.surfaceLight, + borderRadius: "6px", + border: `1px solid ${COLORS.border}`, + }, + pairFlag: { + fontFamily: "'IBM Plex', monospace", + fontWeight: "600", + fontSize: "13px", + color: COLORS.textBright, + }, + pairLabel: { + fontSize: "10px", + color: COLORS.textDim, + textTransform: "uppercase", + letterSpacing: "1px", + }, + loadingDot: { + width: "8px", + height: "8px", + borderRadius: "50%", + background: COLORS.accent, + animation: "pulse 1.5s infinite", + }, + controls: { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + padding: "12px 24px", + borderBottom: `1px solid ${COLORS.border}`, + flexWrap: "wrap", + gap: "12px", + }, + controlGroup: { + display: "flex", + alignItems: "center", + gap: "10px", + }, + controlLabel: { + fontSize: "10px", + fontWeight: "500", + color: COLORS.textDim, + textTransform: "uppercase", + letterSpacing: "1.2px", + fontFamily: "'IBM Plex', monospace", + }, + tfGroup: { + display: "flex", + gap: "2px", + background: COLORS.surface, + borderRadius: "6px", + padding: "2px", + border: `1px solid ${COLORS.border}`, + }, + tfBtn: { + padding: "6px 12px", + fontSize: "12px", + fontWeight: "500", + fontFamily: "'IBM Plex', monospace", + color: COLORS.textDim, + background: "transparent", + border: "none", + borderRadius: "4px", + cursor: "pointer", + transition: "all 0.15s ease", + }, + tfBtnActive: { + background: COLORS.accent, + color: "#fff", + boxShadow: `0 0 12px ${COLORS.accentDim}`, + }, + indicatorGroup: { + display: "flex", + gap: "6px", + }, + indBtn: { + display: "flex", + alignItems: "center", + gap: "6px", + padding: "6px 12px", + fontSize: "11px", + fontWeight: "500", + fontFamily: "'Outfit', sans-serif", + color: COLORS.textDim, + background: COLORS.surface, + border: `1px solid ${COLORS.border}`, + borderRadius: "6px", + cursor: "pointer", + transition: "all 0.15s ease", + }, + indDot: { + width: "6px", + height: "6px", + borderRadius: "50%", + }, + chartWrapper: { + padding: "16px 24px", + }, + chart: { + borderRadius: "8px", + overflow: "hidden", + border: `1px solid ${COLORS.border}`, + }, + statsBar: { + display: "flex", + alignItems: "center", + gap: "20px", + padding: "14px 24px", + margin: "0 24px 24px", + background: COLORS.surface, + borderRadius: "8px", + border: `1px solid ${COLORS.border}`, + flexWrap: "wrap", + }, + statItem: { + display: "flex", + flexDirection: "column", + alignItems: "center", + gap: "2px", + }, + statValue: { + fontFamily: "'IBM Plex', monospace", + fontSize: "15px", + fontWeight: "600", + color: COLORS.textBright, + }, + statLabel: { + fontSize: "9px", + fontWeight: "500", + color: COLORS.textDim, + textTransform: "uppercase", + letterSpacing: "1px", + }, + statDivider: { + width: "1px", + height: "28px", + background: COLORS.border, + }, +}; + export default App; \ No newline at end of file