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 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-28 12:28:04 -06:00
parent 261dc4c71c
commit d3f4aa3385
7 changed files with 664 additions and 0 deletions

View File

@ -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<MLPredictionResponse> {
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;

View File

@ -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<IChartApi>;
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<ICTConceptsOverlayProps> = ({
chartRef,
ictConcepts,
visible = true,
colors = DEFAULT_COLORS,
}) => {
const conceptSeriesRef = useRef<Map<string, ISeriesApi<'Area'>>>(new Map());
// Initialize series for each ICT concept
useEffect(() => {
if (!chartRef.current || !visible) return;
const chart = chartRef.current;
const seriesMap = new Map<string, ISeriesApi<'Area'>>();
// 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;

View File

@ -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<IChartApi>;
predictions: MLPrediction[];
visible?: boolean;
lineColor?: string;
lineWidth?: number;
}
// ============================================================================
// Component
// ============================================================================
export const MLPredictionOverlay: React.FC<MLPredictionOverlayProps> = ({
chartRef,
predictions,
visible = true,
lineColor = '#3b82f6',
lineWidth = 2,
}) => {
const predictionSeriesRef = useRef<ISeriesApi<'Line'> | 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;

View File

@ -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<IChartApi>(null);
const candleSeriesRef = useRef<ISeriesApi<'Candlestick'>>(null);
const { predictions, signals, ictConcepts, isLoading } = useMlOverlayData({
symbol: 'EURUSD',
timeframe: '1h',
});
if (isLoading) return <div>Loading...</div>;
return (
<div>
{/* Tu componente de chart aquí */}
<MLPredictionOverlay
chartRef={chartRef}
predictions={predictions}
visible={true}
/>
<SignalMarkers
chartRef={chartRef}
candleSeriesRef={candleSeriesRef}
signals={signals}
visible={true}
/>
<ICTConceptsOverlay
chartRef={chartRef}
ictConcepts={ictConcepts}
visible={true}
/>
</div>
);
}
```
## 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)

View File

@ -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<IChartApi>;
candleSeriesRef: React.RefObject<ISeriesApi<'Candlestick'>>;
signals: SignalMarker[];
visible?: boolean;
}
// ============================================================================
// Helper Functions
// ============================================================================
function convertToLightweightMarkers(signals: SignalMarker[]): SeriesMarker<Time>[] {
return signals.map((signal) => ({
time: (signal.time / 1000) as Time,
position: signal.position,
color: signal.color,
shape: signal.shape,
text: signal.text,
}));
}
// ============================================================================
// Component
// ============================================================================
export const SignalMarkers: React.FC<SignalMarkersProps> = ({
chartRef,
candleSeriesRef,
signals,
visible = true,
}) => {
const markersSetRef = useRef(false);
useEffect(() => {
if (!chartRef.current || !candleSeriesRef.current || !visible || signals.length === 0) {
return;
}
const candleSeries = candleSeriesRef.current;
// Convert and set markers
const markers = convertToLightweightMarkers(signals);
candleSeries.setMarkers(markers);
markersSetRef.current = true;
return () => {
// Clear markers on cleanup
if (markersSetRef.current && candleSeriesRef.current) {
candleSeriesRef.current.setMarkers([]);
markersSetRef.current = false;
}
};
}, [chartRef, candleSeriesRef, signals, visible]);
return null;
};
export default SignalMarkers;

View File

@ -0,0 +1,13 @@
/**
* Chart Overlays - Barrel Export
* Exports all ML chart overlay components
*/
export { MLPredictionOverlay } from './MLPredictionOverlay';
export type { MLPredictionOverlayProps } from './MLPredictionOverlay';
export { SignalMarkers } from './SignalMarkers';
export type { SignalMarkersProps } from './SignalMarkers';
export { ICTConceptsOverlay } from './ICTConceptsOverlay';
export type { ICTConceptsOverlayProps } from './ICTConceptsOverlay';

View File

@ -0,0 +1,106 @@
/**
* ML Overlay Types
* Type definitions for ML predictions overlays on trading charts
*/
// ============================================================================
// ML Prediction Types
// ============================================================================
export type PredictionType = 'buy' | 'sell' | 'neutral';
export interface MLPrediction {
timestamp: number;
price: number;
confidence: number;
type: PredictionType;
}
// ============================================================================
// Signal Marker Types
// ============================================================================
export type MarkerPosition = 'aboveBar' | 'belowBar';
export type MarkerShape = 'arrowUp' | 'arrowDown' | 'circle';
export interface SignalMarker {
time: number;
position: MarkerPosition;
color: string;
text: string;
shape: MarkerShape;
}
// ============================================================================
// ICT Concept Types
// ============================================================================
export type ICTConceptType = 'OrderBlock' | 'FVG' | 'Liquidity';
export interface ICTConcept {
type: ICTConceptType;
timeStart: number;
timeEnd: number;
priceTop: number;
priceBottom: number;
}
// ============================================================================
// Overlay Configuration Types
// ============================================================================
export interface MLOverlayConfig {
showPredictions: boolean;
showSignals: boolean;
showICTConcepts: boolean;
predictionLineColor: string;
predictionLineWidth: number;
signalColors: {
buy: string;
sell: string;
};
ictColors: {
OrderBlock: string;
FVG: string;
Liquidity: string;
};
}
export const DEFAULT_OVERLAY_CONFIG: MLOverlayConfig = {
showPredictions: true,
showSignals: true,
showICTConcepts: true,
predictionLineColor: '#3b82f6',
predictionLineWidth: 2,
signalColors: {
buy: '#10b981',
sell: '#ef4444',
},
ictColors: {
OrderBlock: '#3b82f6',
FVG: '#fbbf24',
Liquidity: '#f97316',
},
};
// ============================================================================
// API Response Types
// ============================================================================
export interface MLPredictionResponse {
symbol: string;
timeframe: string;
predictions: MLPrediction[];
signals: SignalMarker[];
ictConcepts: ICTConcept[];
generatedAt: string;
}
export interface MLOverlayData {
predictions: MLPrediction[];
signals: SignalMarker[];
ictConcepts: ICTConcept[];
isLoading: boolean;
error: string | null;
lastUpdated: string | null;
}