From d3f4aa3385bb882aa7d4d98c172548a0565071ba Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Wed, 28 Jan 2026 12:28:04 -0600 Subject: [PATCH] feat: Add ML prediction overlay components for trading charts Add complete implementation of ML overlay components: - MLPredictionOverlay: Renders ML price predictions as line overlay - SignalMarkers: Displays BUY/SELL signal markers on chart - ICTConceptsOverlay: Renders Order Blocks, FVG, and Liquidity zones - useMlOverlayData: Custom hook with TanStack Query for data fetching - mlOverlay.types.ts: Type definitions for all ML overlay data Features: - Uses lightweight-charts API for efficient rendering - Automatic caching with 60s stale time - Configurable colors and visibility - Clean up on component unmount - Full TypeScript support Co-Authored-By: Claude Opus 4.5 --- src/hooks/charts/useMlOverlayData.ts | 91 +++++++++++ .../charts/overlays/ICTConceptsOverlay.tsx | 134 +++++++++++++++ .../charts/overlays/MLPredictionOverlay.tsx | 95 +++++++++++ .../components/charts/overlays/README.md | 154 ++++++++++++++++++ .../charts/overlays/SignalMarkers.tsx | 71 ++++++++ .../components/charts/overlays/index.ts | 13 ++ src/types/mlOverlay.types.ts | 106 ++++++++++++ 7 files changed, 664 insertions(+) create mode 100644 src/hooks/charts/useMlOverlayData.ts create mode 100644 src/modules/trading/components/charts/overlays/ICTConceptsOverlay.tsx create mode 100644 src/modules/trading/components/charts/overlays/MLPredictionOverlay.tsx create mode 100644 src/modules/trading/components/charts/overlays/README.md create mode 100644 src/modules/trading/components/charts/overlays/SignalMarkers.tsx create mode 100644 src/modules/trading/components/charts/overlays/index.ts create mode 100644 src/types/mlOverlay.types.ts diff --git a/src/hooks/charts/useMlOverlayData.ts b/src/hooks/charts/useMlOverlayData.ts new file mode 100644 index 0000000..8c05ee4 --- /dev/null +++ b/src/hooks/charts/useMlOverlayData.ts @@ -0,0 +1,91 @@ +/** + * useMlOverlayData Hook + * Custom hook for fetching and caching ML overlay data for trading charts + * Uses TanStack Query for efficient data fetching and caching + */ + +import { useQuery } from '@tanstack/react-query'; +import type { + MLPrediction, + SignalMarker, + ICTConcept, + MLPredictionResponse, +} from '../../types/mlOverlay.types'; + +// ============================================================================ +// Constants +// ============================================================================ + +const ML_API_URL = import.meta.env.VITE_ML_URL || 'http://localhost:3083'; +const STALE_TIME = 60000; // 60 seconds +const CACHE_TIME = 300000; // 5 minutes + +// ============================================================================ +// API Functions +// ============================================================================ + +async function fetchMLPredictions( + symbol: string, + timeframe: string +): Promise { + const response = await fetch( + `${ML_API_URL}/api/ml/predictions/${symbol}/${timeframe}` + ); + + if (!response.ok) { + throw new Error(`Failed to fetch ML predictions: ${response.statusText}`); + } + + return response.json(); +} + +// ============================================================================ +// Hook +// ============================================================================ + +export interface UseMlOverlayDataOptions { + symbol: string; + timeframe: string; + enabled?: boolean; +} + +export interface UseMlOverlayDataResult { + predictions: MLPrediction[]; + signals: SignalMarker[]; + ictConcepts: ICTConcept[]; + isLoading: boolean; + error: Error | null; + refetch: () => void; +} + +export function useMlOverlayData({ + symbol, + timeframe, + enabled = true, +}: UseMlOverlayDataOptions): UseMlOverlayDataResult { + const { + data, + isLoading, + error, + refetch, + } = useQuery({ + queryKey: ['ml-predictions', symbol, timeframe], + queryFn: () => fetchMLPredictions(symbol, timeframe), + staleTime: STALE_TIME, + gcTime: CACHE_TIME, + enabled, + retry: 2, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000), + }); + + return { + predictions: data?.predictions || [], + signals: data?.signals || [], + ictConcepts: data?.ictConcepts || [], + isLoading, + error: error as Error | null, + refetch, + }; +} + +export default useMlOverlayData; diff --git a/src/modules/trading/components/charts/overlays/ICTConceptsOverlay.tsx b/src/modules/trading/components/charts/overlays/ICTConceptsOverlay.tsx new file mode 100644 index 0000000..fe7bf2f --- /dev/null +++ b/src/modules/trading/components/charts/overlays/ICTConceptsOverlay.tsx @@ -0,0 +1,134 @@ +/** + * ICTConceptsOverlay Component + * Renders ICT concepts (Order Blocks, FVG, Liquidity) as rectangles on the chart + */ + +import { useEffect, useRef } from 'react'; +import type { IChartApi, ISeriesApi, Time } from 'lightweight-charts'; +import type { ICTConcept } from '../../../../../types/mlOverlay.types'; + +// ============================================================================ +// Types +// ============================================================================ + +export interface ICTConceptsOverlayProps { + chartRef: React.RefObject; + ictConcepts: ICTConcept[]; + visible?: boolean; + colors?: { + OrderBlock: string; + FVG: string; + Liquidity: string; + }; +} + +interface RectangleData { + time: Time; + value: number; +} + +// ============================================================================ +// Default Colors +// ============================================================================ + +const DEFAULT_COLORS = { + OrderBlock: 'rgba(59, 130, 246, 0.3)', // Blue + FVG: 'rgba(251, 191, 36, 0.3)', // Yellow + Liquidity: 'rgba(249, 115, 22, 0.3)', // Orange +}; + +// ============================================================================ +// Component +// ============================================================================ + +export const ICTConceptsOverlay: React.FC = ({ + chartRef, + ictConcepts, + visible = true, + colors = DEFAULT_COLORS, +}) => { + const conceptSeriesRef = useRef>>(new Map()); + + // Initialize series for each ICT concept + useEffect(() => { + if (!chartRef.current || !visible) return; + + const chart = chartRef.current; + const seriesMap = new Map>(); + + // Create series for each concept type + ictConcepts.forEach((concept, index) => { + const conceptId = `${concept.type}-${index}`; + + if (!conceptSeriesRef.current.has(conceptId)) { + const color = colors[concept.type] || DEFAULT_COLORS[concept.type]; + + const series = chart.addAreaSeries({ + topColor: color, + bottomColor: color, + lineColor: color.replace('0.3', '0.8'), + lineWidth: 1, + crosshairMarkerVisible: false, + lastValueVisible: false, + priceLineVisible: false, + title: `${concept.type}`, + }); + + seriesMap.set(conceptId, series); + } + }); + + conceptSeriesRef.current = seriesMap; + + return () => { + // Cleanup: remove all series + conceptSeriesRef.current.forEach((series) => { + chart.removeSeries(series); + }); + conceptSeriesRef.current.clear(); + }; + }, [chartRef, ictConcepts, ictConcepts.length, visible, colors]); + + // Update concept data + useEffect(() => { + if (!chartRef.current || !visible || ictConcepts.length === 0) { + return; + } + + ictConcepts.forEach((concept, index) => { + const conceptId = `${concept.type}-${index}`; + const series = conceptSeriesRef.current.get(conceptId); + + if (!series) return; + + // Create rectangle data + const timeStart = (concept.timeStart / 1000) as Time; + const timeEnd = (concept.timeEnd / 1000) as Time; + + const rectangleData: RectangleData[] = [ + { time: timeStart, value: concept.priceTop }, + { time: timeStart, value: concept.priceBottom }, + { time: timeEnd, value: concept.priceBottom }, + { time: timeEnd, value: concept.priceTop }, + { time: timeStart, value: concept.priceTop }, + ]; + + series.setData(rectangleData); + }); + }, [chartRef, ictConcepts, visible]); + + // Update visibility + useEffect(() => { + if (!chartRef.current) return; + + conceptSeriesRef.current.forEach((series) => { + series.applyOptions({ + visible, + }); + }); + }, [chartRef, visible]); + + return null; +}; + +export default ICTConceptsOverlay; diff --git a/src/modules/trading/components/charts/overlays/MLPredictionOverlay.tsx b/src/modules/trading/components/charts/overlays/MLPredictionOverlay.tsx new file mode 100644 index 0000000..d7938f1 --- /dev/null +++ b/src/modules/trading/components/charts/overlays/MLPredictionOverlay.tsx @@ -0,0 +1,95 @@ +/** + * MLPredictionOverlay Component + * Renders ML price predictions as a line overlay on the trading chart + */ + +import { useEffect, useRef } from 'react'; +import type { IChartApi, ISeriesApi, LineData, Time } from 'lightweight-charts'; +import type { MLPrediction } from '../../../../../types/mlOverlay.types'; + +// ============================================================================ +// Types +// ============================================================================ + +export interface MLPredictionOverlayProps { + chartRef: React.RefObject; + predictions: MLPrediction[]; + visible?: boolean; + lineColor?: string; + lineWidth?: number; +} + +// ============================================================================ +// Component +// ============================================================================ + +export const MLPredictionOverlay: React.FC = ({ + chartRef, + predictions, + visible = true, + lineColor = '#3b82f6', + lineWidth = 2, +}) => { + const predictionSeriesRef = useRef | null>(null); + + // Initialize prediction line series + useEffect(() => { + if (!chartRef.current || !visible) return; + + const chart = chartRef.current; + + // Create line series for predictions + const predictionSeries = chart.addLineSeries({ + color: lineColor, + lineWidth: lineWidth as 1 | 2 | 3 | 4, + lineStyle: 0, + crosshairMarkerVisible: true, + crosshairMarkerRadius: 6, + crosshairMarkerBorderColor: lineColor, + crosshairMarkerBackgroundColor: lineColor, + lastValueVisible: true, + priceLineVisible: true, + title: 'ML Prediction', + }); + + predictionSeriesRef.current = predictionSeries; + + return () => { + if (predictionSeriesRef.current) { + chart.removeSeries(predictionSeriesRef.current); + predictionSeriesRef.current = null; + } + }; + }, [chartRef, visible, lineColor, lineWidth]); + + // Update prediction data + useEffect(() => { + if (!predictionSeriesRef.current || !visible || predictions.length === 0) { + return; + } + + // Convert predictions to line data + const lineData: LineData[] = predictions.map((pred) => ({ + time: (pred.timestamp / 1000) as Time, + value: pred.price, + })); + + // Sort by time (required by lightweight-charts) + lineData.sort((a, b) => (a.time as number) - (b.time as number)); + + predictionSeriesRef.current.setData(lineData); + }, [predictions, visible]); + + // Update visibility + useEffect(() => { + if (!predictionSeriesRef.current) return; + + predictionSeriesRef.current.applyOptions({ + visible, + }); + }, [visible]); + + return null; +}; + +export default MLPredictionOverlay; diff --git a/src/modules/trading/components/charts/overlays/README.md b/src/modules/trading/components/charts/overlays/README.md new file mode 100644 index 0000000..455de95 --- /dev/null +++ b/src/modules/trading/components/charts/overlays/README.md @@ -0,0 +1,154 @@ +# ML Chart Overlays + +Componentes de React para visualizar predicciones ML en gráficos de trading usando lightweight-charts. + +## Componentes + +### 1. MLPredictionOverlay +Renderiza predicciones de precio ML como una línea sobre el gráfico. + +**Props:** +- `chartRef`: Referencia al chart de lightweight-charts +- `predictions`: Array de predicciones ML +- `visible`: Mostrar/ocultar overlay (default: true) +- `lineColor`: Color de la línea (default: '#3b82f6') +- `lineWidth`: Grosor de línea (default: 2) + +### 2. SignalMarkers +Renderiza marcadores de señales BUY/SELL en el gráfico. + +**Props:** +- `chartRef`: Referencia al chart de lightweight-charts +- `candleSeriesRef`: Referencia a la serie de velas +- `signals`: Array de señales +- `visible`: Mostrar/ocultar markers (default: true) + +### 3. ICTConceptsOverlay +Renderiza conceptos ICT (Order Blocks, FVG, Liquidity) como rectángulos. + +**Props:** +- `chartRef`: Referencia al chart de lightweight-charts +- `ictConcepts`: Array de conceptos ICT +- `visible`: Mostrar/ocultar overlay (default: true) +- `colors`: Objeto con colores por tipo de concepto + +## Hook + +### useMlOverlayData +Hook personalizado para obtener datos ML desde el backend con caché automático. + +**Opciones:** +- `symbol`: Símbolo del activo (ej: 'EURUSD') +- `timeframe`: Temporalidad (ej: '1h', '4h') +- `enabled`: Activar/desactivar fetch (default: true) + +**Retorna:** +- `predictions`: Array de predicciones +- `signals`: Array de señales +- `ictConcepts`: Array de conceptos ICT +- `isLoading`: Estado de carga +- `error`: Error si existe +- `refetch`: Función para refetch manual + +## Ejemplo de Uso + +```tsx +import { useRef } from 'react'; +import { IChartApi, ISeriesApi } from 'lightweight-charts'; +import { useMlOverlayData } from '@/hooks/charts/useMlOverlayData'; +import { + MLPredictionOverlay, + SignalMarkers, + ICTConceptsOverlay, +} from '@/modules/trading/components/charts/overlays'; + +function TradingChart() { + const chartRef = useRef(null); + const candleSeriesRef = useRef>(null); + + const { predictions, signals, ictConcepts, isLoading } = useMlOverlayData({ + symbol: 'EURUSD', + timeframe: '1h', + }); + + if (isLoading) return
Loading...
; + + return ( +
+ {/* Tu componente de chart aquí */} + + + + + + +
+ ); +} +``` + +## API Backend + +Los componentes esperan que el backend exponga el siguiente endpoint: + +``` +GET /api/ml/predictions/:symbol/:timeframe +``` + +**Response:** +```json +{ + "symbol": "EURUSD", + "timeframe": "1h", + "predictions": [ + { + "timestamp": 1706400000000, + "price": 1.0850, + "confidence": 0.85, + "type": "buy" + } + ], + "signals": [ + { + "time": 1706400000000, + "position": "belowBar", + "color": "#10b981", + "text": "BUY", + "shape": "arrowUp" + } + ], + "ictConcepts": [ + { + "type": "OrderBlock", + "timeStart": 1706400000000, + "timeEnd": 1706403600000, + "priceTop": 1.0860, + "priceBottom": 1.0840 + } + ], + "generatedAt": "2026-01-28T10:00:00Z" +} +``` + +## Notas + +- Los componentes usan TanStack Query para caché automático (60s stale time) +- Las timestamps deben estar en milisegundos +- Los overlays se limpian automáticamente al desmontar +- Los colores predeterminados son: + - OrderBlock: Azul (#3b82f6) + - FVG: Amarillo (#fbbf24) + - Liquidity: Naranja (#f97316) diff --git a/src/modules/trading/components/charts/overlays/SignalMarkers.tsx b/src/modules/trading/components/charts/overlays/SignalMarkers.tsx new file mode 100644 index 0000000..463d437 --- /dev/null +++ b/src/modules/trading/components/charts/overlays/SignalMarkers.tsx @@ -0,0 +1,71 @@ +/** + * SignalMarkers Component + * Renders buy/sell signal markers on the trading chart + */ + +import { useEffect, useRef } from 'react'; +import type { IChartApi, ISeriesApi, Time, SeriesMarker } from 'lightweight-charts'; +import type { SignalMarker } from '../../../../../types/mlOverlay.types'; + +// ============================================================================ +// Types +// ============================================================================ + +export interface SignalMarkersProps { + chartRef: React.RefObject; + candleSeriesRef: React.RefObject>; + signals: SignalMarker[]; + visible?: boolean; +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +function convertToLightweightMarkers(signals: SignalMarker[]): SeriesMarker