/** * CandlestickChartWithML Component * Lightweight Charts with ML prediction overlays * Displays: Order Blocks, Fair Value Gaps, Range Predictions, Signal Markers */ import { useEffect, useRef, useCallback, useState } from 'react'; import { createChart, IChartApi, ISeriesApi, CandlestickData, HistogramData, Time, ColorType, CrosshairMode, LineStyle, PriceLineOptions, SeriesMarker, } from 'lightweight-charts'; import type { Candle, CandlestickChartProps } from '../../../types/trading.types'; import type { MLSignal, RangePrediction, AMDPhase } from '../../../services/mlService'; import { getLatestSignal, getRangePrediction, getAMDPhase } from '../../../services/mlService'; // ============================================================================ // Types // ============================================================================ interface OrderBlock { id: string; type: 'bullish' | 'bearish'; priceHigh: number; priceLow: number; timeStart: number; timeEnd?: number; strength: number; tested: boolean; } interface FairValueGap { id: string; type: 'bullish' | 'bearish'; priceHigh: number; priceLow: number; time: number; filled: boolean; fillPercentage: number; } interface MLOverlays { signal?: MLSignal | null; rangePrediction?: RangePrediction | null; amdPhase?: AMDPhase | null; orderBlocks?: OrderBlock[]; fairValueGaps?: FairValueGap[]; } interface CandlestickChartWithMLProps extends CandlestickChartProps { enableMLOverlays?: boolean; showSignalLevels?: boolean; showRangePrediction?: boolean; showOrderBlocks?: boolean; showFairValueGaps?: boolean; showAMDPhase?: boolean; autoRefreshML?: boolean; refreshInterval?: number; } // ============================================================================ // Theme Configuration // ============================================================================ interface ChartTheme { backgroundColor: string; textColor: string; gridColor: string; upColor: string; downColor: string; borderUpColor: string; borderDownColor: string; wickUpColor: string; wickDownColor: string; volumeUpColor: string; volumeDownColor: string; } const THEMES: Record<'dark' | 'light', ChartTheme> = { dark: { backgroundColor: '#1a1a2e', textColor: '#d1d4dc', gridColor: '#2B2B43', upColor: '#10b981', downColor: '#ef4444', borderUpColor: '#10b981', borderDownColor: '#ef4444', wickUpColor: '#10b981', wickDownColor: '#ef4444', volumeUpColor: 'rgba(16, 185, 129, 0.5)', volumeDownColor: 'rgba(239, 68, 68, 0.5)', }, light: { backgroundColor: '#ffffff', textColor: '#131722', gridColor: '#e1e1e1', upColor: '#10b981', downColor: '#ef4444', borderUpColor: '#10b981', borderDownColor: '#ef4444', wickUpColor: '#10b981', wickDownColor: '#ef4444', volumeUpColor: 'rgba(16, 185, 129, 0.5)', volumeDownColor: 'rgba(239, 68, 68, 0.5)', }, }; // ML Overlay Colors const ML_COLORS = { entryLine: '#3b82f6', stopLoss: '#ef4444', takeProfit: '#10b981', rangePredictionHigh: 'rgba(16, 185, 129, 0.3)', rangePredictionLow: 'rgba(239, 68, 68, 0.3)', orderBlockBullish: 'rgba(16, 185, 129, 0.15)', orderBlockBearish: 'rgba(239, 68, 68, 0.15)', fvgBullish: 'rgba(59, 130, 246, 0.2)', fvgBearish: 'rgba(249, 115, 22, 0.2)', amdAccumulation: 'rgba(59, 130, 246, 0.1)', amdManipulation: 'rgba(249, 115, 22, 0.1)', amdDistribution: 'rgba(139, 92, 246, 0.1)', }; // ============================================================================ // Component // ============================================================================ export const CandlestickChartWithML: React.FC = ({ symbol, interval = '1h', height = 500, theme = 'dark', showVolume = true, onCrosshairMove, enableMLOverlays = true, showSignalLevels = true, showRangePrediction = true, showOrderBlocks = false, showFairValueGaps = false, showAMDPhase = true, autoRefreshML = true, refreshInterval = 30000, }) => { const containerRef = useRef(null); const chartRef = useRef(null); const candleSeriesRef = useRef | null>(null); const volumeSeriesRef = useRef | null>(null); const rangeHighSeriesRef = useRef | null>(null); const rangeLowSeriesRef = useRef | null>(null); const resizeObserverRef = useRef(null); const priceLinesRef = useRef['createPriceLine']>[]>([]); const [mlOverlays, setMlOverlays] = useState({}); const [isLoadingML, setIsLoadingML] = useState(false); const chartTheme = THEMES[theme]; // Fetch ML data const fetchMLData = useCallback(async () => { if (!enableMLOverlays || !symbol) return; setIsLoadingML(true); try { const [signal, rangePred, amd] = await Promise.all([ showSignalLevels ? getLatestSignal(symbol) : Promise.resolve(null), showRangePrediction ? getRangePrediction(symbol, interval) : Promise.resolve(null), showAMDPhase ? getAMDPhase(symbol) : Promise.resolve(null), ]); setMlOverlays({ signal, rangePrediction: rangePred, amdPhase: amd, }); } catch (error) { console.error('Error fetching ML data:', error); } finally { setIsLoadingML(false); } }, [symbol, interval, enableMLOverlays, showSignalLevels, showRangePrediction, showAMDPhase]); // Auto-refresh ML data useEffect(() => { if (enableMLOverlays && autoRefreshML) { fetchMLData(); const intervalId = setInterval(fetchMLData, refreshInterval); return () => clearInterval(intervalId); } }, [fetchMLData, autoRefreshML, refreshInterval, enableMLOverlays]); // Initialize chart useEffect(() => { if (!containerRef.current) return; const chart = createChart(containerRef.current, { width: containerRef.current.clientWidth, height, layout: { background: { type: ColorType.Solid, color: chartTheme.backgroundColor }, textColor: chartTheme.textColor, }, grid: { vertLines: { color: chartTheme.gridColor }, horzLines: { color: chartTheme.gridColor }, }, crosshair: { mode: CrosshairMode.Normal, vertLine: { width: 1, color: 'rgba(224, 227, 235, 0.4)', style: LineStyle.Solid, labelBackgroundColor: chartTheme.backgroundColor, }, horzLine: { width: 1, color: 'rgba(224, 227, 235, 0.4)', style: LineStyle.Solid, labelBackgroundColor: chartTheme.backgroundColor, }, }, rightPriceScale: { borderColor: chartTheme.gridColor, }, timeScale: { borderColor: chartTheme.gridColor, timeVisible: true, secondsVisible: false, }, }); // Add candlestick series const candleSeries = chart.addCandlestickSeries({ upColor: chartTheme.upColor, downColor: chartTheme.downColor, borderUpColor: chartTheme.borderUpColor, borderDownColor: chartTheme.borderDownColor, wickUpColor: chartTheme.wickUpColor, wickDownColor: chartTheme.wickDownColor, }); // Add volume series let volumeSeries: ISeriesApi<'Histogram'> | null = null; if (showVolume) { volumeSeries = chart.addHistogramSeries({ color: chartTheme.volumeUpColor, priceFormat: { type: 'volume' }, priceScaleId: '', }); volumeSeries.priceScale().applyOptions({ scaleMargins: { top: 0.8, bottom: 0 }, }); } // Add range prediction lines (invisible initially) const rangeHighSeries = chart.addLineSeries({ color: ML_COLORS.rangePredictionHigh, lineWidth: 1, lineStyle: LineStyle.Dashed, priceLineVisible: false, lastValueVisible: false, crosshairMarkerVisible: false, }); const rangeLowSeries = chart.addLineSeries({ color: ML_COLORS.rangePredictionLow, lineWidth: 1, lineStyle: LineStyle.Dashed, priceLineVisible: false, lastValueVisible: false, crosshairMarkerVisible: false, }); // Handle crosshair movement chart.subscribeCrosshairMove((param) => { if (!param || !param.time || !param.seriesData) { onCrosshairMove?.(null); return; } const candleData = param.seriesData.get(candleSeries) as CandlestickData; if (candleData) { onCrosshairMove?.({ time: param.time, price: candleData.close as number, }); } }); chartRef.current = chart; candleSeriesRef.current = candleSeries; volumeSeriesRef.current = volumeSeries; rangeHighSeriesRef.current = rangeHighSeries; rangeLowSeriesRef.current = rangeLowSeries; // Setup ResizeObserver const resizeObserver = new ResizeObserver((entries) => { if (entries[0] && chartRef.current) { const { width } = entries[0].contentRect; chartRef.current.applyOptions({ width }); } }); resizeObserver.observe(containerRef.current); resizeObserverRef.current = resizeObserver; return () => { resizeObserver.disconnect(); chart.remove(); }; }, [height, showVolume, chartTheme, onCrosshairMove]); // Update price lines when ML signal changes useEffect(() => { if (!candleSeriesRef.current || !showSignalLevels) return; // Remove old price lines priceLinesRef.current.forEach((line) => { candleSeriesRef.current?.removePriceLine(line); }); priceLinesRef.current = []; // Add new price lines for signal if (mlOverlays.signal) { const signal = mlOverlays.signal; // Entry line const entryLine = candleSeriesRef.current.createPriceLine({ price: signal.entry_price, color: ML_COLORS.entryLine, lineWidth: 2, lineStyle: LineStyle.Solid, axisLabelVisible: true, title: `Entry ${signal.direction.toUpperCase()}`, } as PriceLineOptions); priceLinesRef.current.push(entryLine); // Stop Loss line const slLine = candleSeriesRef.current.createPriceLine({ price: signal.stop_loss, color: ML_COLORS.stopLoss, lineWidth: 1, lineStyle: LineStyle.Dashed, axisLabelVisible: true, title: 'SL', } as PriceLineOptions); priceLinesRef.current.push(slLine); // Take Profit line const tpLine = candleSeriesRef.current.createPriceLine({ price: signal.take_profit, color: ML_COLORS.takeProfit, lineWidth: 1, lineStyle: LineStyle.Dashed, axisLabelVisible: true, title: 'TP', } as PriceLineOptions); priceLinesRef.current.push(tpLine); } // Add range prediction lines if (mlOverlays.rangePrediction && rangeHighSeriesRef.current && rangeLowSeriesRef.current) { const pred = mlOverlays.rangePrediction; // Support/Resistance from AMD if (mlOverlays.amdPhase?.key_levels) { const supportLine = candleSeriesRef.current.createPriceLine({ price: mlOverlays.amdPhase.key_levels.support, color: 'rgba(59, 130, 246, 0.5)', lineWidth: 1, lineStyle: LineStyle.Dotted, axisLabelVisible: true, title: 'Support', } as PriceLineOptions); priceLinesRef.current.push(supportLine); const resistanceLine = candleSeriesRef.current.createPriceLine({ price: mlOverlays.amdPhase.key_levels.resistance, color: 'rgba(249, 115, 22, 0.5)', lineWidth: 1, lineStyle: LineStyle.Dotted, axisLabelVisible: true, title: 'Resistance', } as PriceLineOptions); priceLinesRef.current.push(resistanceLine); } // Predicted range lines const predHighLine = candleSeriesRef.current.createPriceLine({ price: pred.predicted_high, color: ML_COLORS.rangePredictionHigh.replace('0.3', '0.7'), lineWidth: 1, lineStyle: LineStyle.LargeDashed, axisLabelVisible: true, title: `Pred High (${(pred.prediction_confidence * 100).toFixed(0)}%)`, } as PriceLineOptions); priceLinesRef.current.push(predHighLine); const predLowLine = candleSeriesRef.current.createPriceLine({ price: pred.predicted_low, color: ML_COLORS.rangePredictionLow.replace('0.3', '0.7'), lineWidth: 1, lineStyle: LineStyle.LargeDashed, axisLabelVisible: true, title: `Pred Low`, } as PriceLineOptions); priceLinesRef.current.push(predLowLine); } }, [mlOverlays, showSignalLevels, showRangePrediction]); // Update data method const updateData = useCallback( (candles: Candle[]) => { if (!candleSeriesRef.current || candles.length === 0) return; // Transform data for candlestick series const candleData: CandlestickData[] = candles.map((c) => ({ time: (c.time / 1000) as Time, open: c.open, high: c.high, low: c.low, close: c.close, })); candleSeriesRef.current.setData(candleData); // Add signal markers if (mlOverlays.signal) { const signal = mlOverlays.signal; const signalTime = new Date(signal.created_at).getTime() / 1000; // Find the candle closest to signal time const closestCandle = candleData.reduce((prev, curr) => { return Math.abs((curr.time as number) - signalTime) < Math.abs((prev.time as number) - signalTime) ? curr : prev; }); const markers: SeriesMarker