[OQI-006] feat(ml): Complete ML Overlays for trading charts

- Enhanced MLPredictionOverlay with confidence bands (upper/lower bounds)
- Improved SignalMarkers with tooltips, signal types (BUY/SELL/HOLD), and click handlers
- Fixed ICTConceptsOverlay rectangle rendering with proper area series
- Added OverlayControlPanel for toggle/configuration of all overlays
- Created usePredictions hook for ML price predictions
- Created useSignals hook for trading signals
- Created useChartOverlays composite hook for unified overlay management
- Updated exports in index.ts files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-02-04 01:24:10 -06:00
parent 6a8cb3b9cf
commit 9e8f69d7f2
10 changed files with 1605 additions and 125 deletions

31
src/hooks/charts/index.ts Normal file
View File

@ -0,0 +1,31 @@
/**
* Chart Hooks - Barrel Export
* All hooks related to chart functionality and ML overlays
*/
// ML Overlay Data
export { useMlOverlayData, default as useMlOverlayDataDefault } from './useMlOverlayData';
export type { UseMlOverlayDataOptions, UseMlOverlayDataResult } from './useMlOverlayData';
// Predictions
export { usePredictions, default as usePredictionsDefault } from './usePredictions';
export type { UsePredictionsOptions, UsePredictionsResult, PredictionResponse } from './usePredictions';
// Signals
export { useSignals, default as useSignalsDefault } from './useSignals';
export type {
UseSignalsOptions,
UseSignalsResult,
TradingSignal,
SignalsResponse,
SignalDirection,
SignalStrength,
} from './useSignals';
// Composite Overlays Hook
export { useChartOverlays, default as useChartOverlaysDefault } from './useChartOverlays';
export type {
UseChartOverlaysOptions,
UseChartOverlaysResult,
ChartOverlayData,
} from './useChartOverlays';

View File

@ -0,0 +1,246 @@
/**
* useChartOverlays Hook
* Composite hook that manages all ML overlay data for trading charts
* Combines predictions, signals, ICT concepts, and AMD zones into a single interface
*/
import { useState, useCallback, useMemo } from 'react';
import { usePredictions } from './usePredictions';
import { useSignals } from './useSignals';
import { useMlOverlayData } from './useMlOverlayData';
import type { OverlayState, OverlayConfig } from '../../modules/trading/components/charts/overlays/OverlayControlPanel';
import type { MLPrediction, SignalMarker, ICTConcept, AMDZone } from '../../types/mlOverlay.types';
// ============================================================================
// Types
// ============================================================================
export interface UseChartOverlaysOptions {
symbol: string;
timeframe: string;
enabled?: boolean;
initialOverlayState?: Partial<OverlayState>;
initialConfig?: Partial<OverlayConfig>;
}
export interface ChartOverlayData {
predictions: MLPrediction[];
signals: SignalMarker[];
ictConcepts: ICTConcept[];
amdZones: AMDZone[];
}
export interface UseChartOverlaysResult {
// Data
data: ChartOverlayData;
// State management
overlayState: OverlayState;
setOverlayState: (key: keyof OverlayState, value: boolean) => void;
toggleOverlay: (key: keyof OverlayState) => void;
enableAll: () => void;
disableAll: () => void;
// Configuration
config: OverlayConfig;
setConfig: (config: OverlayConfig) => void;
updateConfig: (key: keyof OverlayConfig, value: string | number) => void;
// Loading states
isLoading: boolean;
isPredictionsLoading: boolean;
isSignalsLoading: boolean;
isICTLoading: boolean;
// Errors
error: Error | null;
// Actions
refetch: () => void;
refetchPredictions: () => void;
refetchSignals: () => void;
generateSignal: () => void;
isGeneratingSignal: boolean;
}
// ============================================================================
// Default Values
// ============================================================================
const DEFAULT_OVERLAY_STATE: OverlayState = {
predictions: true,
signals: true,
ictConcepts: false,
amdZones: true,
confidenceBands: true,
};
const DEFAULT_CONFIG: OverlayConfig = {
predictionModel: 'ensemble',
signalMinConfidence: 0.6,
amdLookback: 10,
refreshInterval: 30,
};
// ============================================================================
// Hook
// ============================================================================
export function useChartOverlays({
symbol,
timeframe,
enabled = true,
initialOverlayState = {},
initialConfig = {},
}: UseChartOverlaysOptions): UseChartOverlaysResult {
// State
const [overlayState, setOverlayStateInternal] = useState<OverlayState>({
...DEFAULT_OVERLAY_STATE,
...initialOverlayState,
});
const [config, setConfigInternal] = useState<OverlayConfig>({
...DEFAULT_CONFIG,
...initialConfig,
});
// Predictions hook
const {
predictions,
isLoading: isPredictionsLoading,
error: predictionsError,
refetch: refetchPredictions,
} = usePredictions({
symbol,
timeframe,
enabled: enabled && overlayState.predictions,
refetchInterval: config.refreshInterval * 1000,
});
// Signals hook
const {
markers: signalMarkers,
isLoading: isSignalsLoading,
error: signalsError,
refetch: refetchSignals,
generateSignal,
isGenerating: isGeneratingSignal,
} = useSignals({
symbol,
enabled: enabled && overlayState.signals,
refetchInterval: config.refreshInterval * 1000,
});
// ML Overlay data (ICT concepts and AMD zones)
const {
ictConcepts,
isLoading: isICTLoading,
error: mlError,
refetch: refetchMl,
} = useMlOverlayData({
symbol,
timeframe,
enabled: enabled && (overlayState.ictConcepts || overlayState.amdZones),
});
// Combine data
const data: ChartOverlayData = useMemo(
() => ({
predictions: overlayState.predictions ? predictions : [],
signals: overlayState.signals ? (signalMarkers as SignalMarker[]) : [],
ictConcepts: overlayState.ictConcepts ? ictConcepts : [],
amdZones: [], // AMD zones come from ML service, add when available
}),
[predictions, signalMarkers, ictConcepts, overlayState]
);
// State setters
const setOverlayState = useCallback(
(key: keyof OverlayState, value: boolean) => {
setOverlayStateInternal((prev) => ({
...prev,
[key]: value,
}));
},
[]
);
const toggleOverlay = useCallback((key: keyof OverlayState) => {
setOverlayStateInternal((prev) => ({
...prev,
[key]: !prev[key],
}));
}, []);
const enableAll = useCallback(() => {
setOverlayStateInternal({
predictions: true,
signals: true,
ictConcepts: true,
amdZones: true,
confidenceBands: true,
});
}, []);
const disableAll = useCallback(() => {
setOverlayStateInternal({
predictions: false,
signals: false,
ictConcepts: false,
amdZones: false,
confidenceBands: false,
});
}, []);
// Config setters
const setConfig = useCallback((newConfig: OverlayConfig) => {
setConfigInternal(newConfig);
}, []);
const updateConfig = useCallback(
(key: keyof OverlayConfig, value: string | number) => {
setConfigInternal((prev) => ({
...prev,
[key]: value,
}));
},
[]
);
// Refetch all data
const refetch = useCallback(() => {
refetchPredictions();
refetchSignals();
refetchMl();
}, [refetchPredictions, refetchSignals, refetchMl]);
// Combined loading state
const isLoading = isPredictionsLoading || isSignalsLoading || isICTLoading;
// Combined error
const error = predictionsError || signalsError || mlError;
return {
data,
overlayState,
setOverlayState,
toggleOverlay,
enableAll,
disableAll,
config,
setConfig,
updateConfig,
isLoading,
isPredictionsLoading,
isSignalsLoading,
isICTLoading,
error,
refetch,
refetchPredictions,
refetchSignals,
generateSignal,
isGeneratingSignal,
};
}
export default useChartOverlays;

View File

@ -0,0 +1,150 @@
/**
* usePredictions Hook
* Custom hook for fetching ML price predictions for a specific symbol and timeframe
* Uses TanStack Query for efficient caching and refetching
*/
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '@/lib/apiClient';
import type { MLPrediction } from '../../types/mlOverlay.types';
// ============================================================================
// Constants
// ============================================================================
const STALE_TIME = 30000; // 30 seconds
const CACHE_TIME = 300000; // 5 minutes
const ML_API_URL = import.meta.env.VITE_ML_URL || 'http://localhost:3083';
// ============================================================================
// Types
// ============================================================================
export interface PredictionResponse {
symbol: string;
timeframe: string;
predictions: MLPrediction[];
model: string;
confidence: number;
generatedAt: string;
}
export interface UsePredictionsOptions {
symbol: string;
timeframe: string;
enabled?: boolean;
refetchInterval?: number | false;
lookAhead?: number; // Number of candles to predict ahead
}
export interface UsePredictionsResult {
predictions: MLPrediction[];
isLoading: boolean;
isError: boolean;
error: Error | null;
isFetching: boolean;
refetch: () => void;
model: string | null;
confidence: number;
lastUpdated: Date | null;
}
// ============================================================================
// API Function
// ============================================================================
async function fetchPredictions(
symbol: string,
timeframe: string,
lookAhead: number
): Promise<PredictionResponse> {
try {
// Try the Express proxy first
const response = await apiClient.get(`/proxy/ml/predictions/${symbol}/${timeframe}`, {
params: { lookAhead },
});
return response.data;
} catch (proxyError) {
// Fallback to direct ML engine call
try {
const response = await fetch(
`${ML_API_URL}/api/ml/predictions/${symbol}/${timeframe}?lookAhead=${lookAhead}`
);
if (!response.ok) {
throw new Error(`Failed to fetch predictions: ${response.statusText}`);
}
return response.json();
} catch (directError) {
console.error('Both proxy and direct ML calls failed:', { proxyError, directError });
throw directError;
}
}
}
// ============================================================================
// Hook
// ============================================================================
export function usePredictions({
symbol,
timeframe,
enabled = true,
refetchInterval = 60000,
lookAhead = 5,
}: UsePredictionsOptions): UsePredictionsResult {
const queryClient = useQueryClient();
const queryKey = ['ml-predictions', symbol, timeframe, lookAhead];
const {
data,
isLoading,
isError,
error,
isFetching,
refetch,
} = useQuery({
queryKey,
queryFn: () => fetchPredictions(symbol, timeframe, lookAhead),
staleTime: STALE_TIME,
gcTime: CACHE_TIME,
enabled: enabled && !!symbol && !!timeframe,
refetchInterval,
retry: 2,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
});
// Prefetch next timeframe predictions
const prefetchNextTimeframe = () => {
const timeframes = ['1m', '5m', '15m', '30m', '1h', '4h', '1d'];
const currentIndex = timeframes.indexOf(timeframe);
const nextTimeframe = timeframes[currentIndex + 1] || timeframes[0];
queryClient.prefetchQuery({
queryKey: ['ml-predictions', symbol, nextTimeframe, lookAhead],
queryFn: () => fetchPredictions(symbol, nextTimeframe, lookAhead),
staleTime: STALE_TIME,
});
};
// Trigger prefetch when data is successfully loaded
if (data && !isLoading) {
prefetchNextTimeframe();
}
return {
predictions: data?.predictions || [],
isLoading,
isError,
error: error as Error | null,
isFetching,
refetch,
model: data?.model || null,
confidence: data?.confidence || 0,
lastUpdated: data?.generatedAt ? new Date(data.generatedAt) : null,
};
}
export default usePredictions;

View File

@ -0,0 +1,214 @@
/**
* useSignals Hook
* Custom hook for fetching ML trading signals for a specific symbol
* Provides real-time signals with BUY/SELL/HOLD recommendations
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '@/lib/apiClient';
import type { EnhancedSignalMarker } from '../../modules/trading/components/charts/overlays/SignalMarkers';
// ============================================================================
// Constants
// ============================================================================
const STALE_TIME = 15000; // 15 seconds (signals need to be fresh)
const CACHE_TIME = 180000; // 3 minutes
// ============================================================================
// Types
// ============================================================================
export type SignalDirection = 'long' | 'short';
export type SignalStrength = 'strong' | 'moderate' | 'weak';
export interface TradingSignal {
signal_id: string;
symbol: string;
direction: SignalDirection;
entry_price: number;
stop_loss: number;
take_profit: number;
risk_reward_ratio: number;
confidence_score: number;
prob_tp_first: number;
amd_phase: string;
volatility_regime: string;
valid_until: string;
created_at: string;
strength?: SignalStrength;
reasoning?: string[];
}
export interface SignalsResponse {
symbol: string;
signals: TradingSignal[];
activeSignal: TradingSignal | null;
timestamp: string;
}
export interface UseSignalsOptions {
symbol: string;
enabled?: boolean;
refetchInterval?: number | false;
includeHistory?: boolean;
historyLimit?: number;
}
export interface UseSignalsResult {
signals: TradingSignal[];
activeSignal: TradingSignal | null;
markers: EnhancedSignalMarker[];
isLoading: boolean;
isError: boolean;
error: Error | null;
isFetching: boolean;
refetch: () => void;
generateSignal: () => void;
isGenerating: boolean;
}
// ============================================================================
// API Functions
// ============================================================================
async function fetchSignals(
symbol: string,
includeHistory: boolean,
historyLimit: number
): Promise<SignalsResponse> {
try {
const response = await apiClient.get(`/proxy/ml/signals/latest/${symbol}`, {
params: { includeHistory, limit: historyLimit },
});
const signal = response.data?.signal || response.data;
return {
symbol,
signals: signal ? [signal] : [],
activeSignal: signal || null,
timestamp: new Date().toISOString(),
};
} catch (error) {
if ((error as { response?: { status: number } }).response?.status === 404) {
return {
symbol,
signals: [],
activeSignal: null,
timestamp: new Date().toISOString(),
};
}
throw error;
}
}
async function requestSignalGeneration(symbol: string): Promise<TradingSignal | null> {
const response = await apiClient.post('/proxy/ml/signals/generate', { symbol });
return response.data?.signal || response.data || null;
}
// ============================================================================
// Helper Functions
// ============================================================================
function convertSignalToMarker(signal: TradingSignal): EnhancedSignalMarker {
const signalType = signal.direction === 'long' ? 'BUY' : 'SELL';
const color = signal.direction === 'long' ? '#10b981' : '#ef4444';
return {
time: new Date(signal.created_at).getTime(),
position: signal.direction === 'long' ? 'belowBar' : 'aboveBar',
color,
text: `${signalType} ${Math.round(signal.confidence_score * 100)}%`,
shape: signal.direction === 'long' ? 'arrowUp' : 'arrowDown',
signalType,
confidence: signal.confidence_score,
price: signal.entry_price,
stopLoss: signal.stop_loss,
takeProfit: signal.take_profit,
riskReward: signal.risk_reward_ratio,
};
}
function getSignalStrength(confidence: number): SignalStrength {
if (confidence >= 0.8) return 'strong';
if (confidence >= 0.6) return 'moderate';
return 'weak';
}
// ============================================================================
// Hook
// ============================================================================
export function useSignals({
symbol,
enabled = true,
refetchInterval = 30000,
includeHistory = false,
historyLimit = 10,
}: UseSignalsOptions): UseSignalsResult {
const queryClient = useQueryClient();
const queryKey = ['ml-signals', symbol, includeHistory, historyLimit];
const {
data,
isLoading,
isError,
error,
isFetching,
refetch,
} = useQuery({
queryKey,
queryFn: () => fetchSignals(symbol, includeHistory, historyLimit),
staleTime: STALE_TIME,
gcTime: CACHE_TIME,
enabled: enabled && !!symbol,
refetchInterval,
retry: 2,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 5000),
});
// Mutation for generating new signals
const generateMutation = useMutation({
mutationFn: () => requestSignalGeneration(symbol),
onSuccess: (newSignal) => {
if (newSignal) {
// Update cache with new signal
queryClient.setQueryData<SignalsResponse>(queryKey, (old) => ({
symbol,
signals: [newSignal, ...(old?.signals || [])].slice(0, historyLimit),
activeSignal: newSignal,
timestamp: new Date().toISOString(),
}));
}
// Refetch to ensure consistency
refetch();
},
});
// Convert signals to markers for chart overlay
const signals = data?.signals || [];
const signalsWithStrength = signals.map((s) => ({
...s,
strength: s.strength || getSignalStrength(s.confidence_score),
}));
const markers: EnhancedSignalMarker[] = signalsWithStrength.map(convertSignalToMarker);
return {
signals: signalsWithStrength,
activeSignal: data?.activeSignal || null,
markers,
isLoading,
isError,
error: error as Error | null,
isFetching,
refetch,
generateSignal: generateMutation.mutate,
isGenerating: generateMutation.isPending,
};
}
export default useSignals;

View File

@ -3,4 +3,33 @@
* Barrel export for all custom hooks
*/
// ML Analysis Hooks
export { useMLAnalysis, useQuickSignals, DEFAULT_SYMBOLS } from './useMLAnalysis';
// Chart Overlay Hooks
export { useMlOverlayData } from './charts/useMlOverlayData';
export type { UseMlOverlayDataOptions, UseMlOverlayDataResult } from './charts/useMlOverlayData';
export { usePredictions } from './charts/usePredictions';
export type { UsePredictionsOptions, UsePredictionsResult, PredictionResponse } from './charts/usePredictions';
export { useSignals } from './charts/useSignals';
export type {
UseSignalsOptions,
UseSignalsResult,
TradingSignal,
SignalsResponse,
SignalDirection,
SignalStrength,
} from './charts/useSignals';
// Composite Chart Overlays Hook
export { useChartOverlays } from './charts/useChartOverlays';
export type {
UseChartOverlaysOptions,
UseChartOverlaysResult,
ChartOverlayData,
} from './charts/useChartOverlays';
// Feature Flags
export { useFeatureFlags } from './useFeatureFlags';

View File

@ -1,11 +1,12 @@
/**
* ICTConceptsOverlay Component
* Renders ICT concepts (Order Blocks, FVG, Liquidity) as rectangles on the chart
* Renders ICT concepts (Order Blocks, FVG, Liquidity) as highlighted zones on the chart
* Uses area series with top/bottom lines to create box-like regions
*/
import { useEffect, useRef } from 'react';
import type { IChartApi, ISeriesApi, Time } from 'lightweight-charts';
import type { ICTConcept } from '../../../../../types/mlOverlay.types';
import type { IChartApi, ISeriesApi, Time, LineData } from 'lightweight-charts';
import type { ICTConcept, ICTConceptType } from '../../../../../types/mlOverlay.types';
// ============================================================================
// Types
@ -15,28 +16,64 @@ export interface ICTConceptsOverlayProps {
chartRef: React.RefObject<IChartApi>;
ictConcepts: ICTConcept[];
visible?: boolean;
colors?: {
OrderBlock: string;
FVG: string;
Liquidity: string;
};
colors?: Record<ICTConceptType, string>;
showLabels?: boolean;
onConceptClick?: (concept: ICTConcept) => void;
}
interface RectangleData {
time: Time;
value: number;
interface ConceptSeries {
area: ISeriesApi<'Area'>;
topLine: ISeriesApi<'Line'>;
bottomLine: ISeriesApi<'Line'>;
}
// ============================================================================
// 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
const DEFAULT_COLORS: Record<ICTConceptType, string> = {
OrderBlock: 'rgba(59, 130, 246, 0.25)', // Blue
FVG: 'rgba(251, 191, 36, 0.25)', // Yellow/Amber
Liquidity: 'rgba(249, 115, 22, 0.25)', // Orange
};
const BORDER_COLORS: Record<ICTConceptType, string> = {
OrderBlock: 'rgba(59, 130, 246, 0.6)',
FVG: 'rgba(251, 191, 36, 0.6)',
Liquidity: 'rgba(249, 115, 22, 0.6)',
};
// ============================================================================
// Helper Functions
// ============================================================================
function generateConceptId(concept: ICTConcept, index: number): string {
return `ict-${concept.type}-${index}-${concept.timeStart}`;
}
function createLineData(
timeStart: number,
timeEnd: number,
price: number
): LineData[] {
return [
{ time: (timeStart / 1000) as Time, value: price },
{ time: (timeEnd / 1000) as Time, value: price },
];
}
function createAreaData(
timeStart: number,
timeEnd: number,
priceTop: number
): LineData[] {
// Area series uses the value as the top, and bottomColor fills below
return [
{ time: (timeStart / 1000) as Time, value: priceTop },
{ time: (timeEnd / 1000) as Time, value: priceTop },
];
}
// ============================================================================
// Component
// ============================================================================
@ -46,87 +83,154 @@ export const ICTConceptsOverlay: React.FC<ICTConceptsOverlayProps> = ({
ictConcepts,
visible = true,
colors = DEFAULT_COLORS,
showLabels = true,
onConceptClick,
}) => {
const conceptSeriesRef = useRef<Map<string, ISeriesApi<'Area'>>>(new Map());
const seriesMapRef = useRef<Map<string, ConceptSeries>>(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
// Initialize and update series for ICT concepts
useEffect(() => {
if (!chartRef.current) return;
conceptSeriesRef.current.forEach((series) => {
series.applyOptions({
visible,
const chart = chartRef.current;
// Clear existing series first
seriesMapRef.current.forEach((conceptSeries) => {
try {
chart.removeSeries(conceptSeries.area);
chart.removeSeries(conceptSeries.topLine);
chart.removeSeries(conceptSeries.bottomLine);
} catch {
// Series might already be removed
}
});
seriesMapRef.current.clear();
if (!visible || ictConcepts.length === 0) return;
// Create series for each concept
ictConcepts.forEach((concept, index) => {
const conceptId = generateConceptId(concept, index);
const fillColor = colors[concept.type] || DEFAULT_COLORS[concept.type];
const borderColor = BORDER_COLORS[concept.type];
// Create area series for the zone fill
const areaSeries = chart.addAreaSeries({
topColor: fillColor,
bottomColor: fillColor,
lineColor: 'transparent',
lineWidth: 1,
crosshairMarkerVisible: false,
lastValueVisible: false,
priceLineVisible: false,
priceScaleId: 'right',
});
// Create top boundary line
const topLine = chart.addLineSeries({
color: borderColor,
lineWidth: 1,
lineStyle: 0,
crosshairMarkerVisible: false,
lastValueVisible: showLabels,
priceLineVisible: false,
title: showLabels ? concept.type : '',
priceScaleId: 'right',
});
// Create bottom boundary line
const bottomLine = chart.addLineSeries({
color: borderColor,
lineWidth: 1,
lineStyle: 0,
crosshairMarkerVisible: false,
lastValueVisible: false,
priceLineVisible: false,
priceScaleId: 'right',
});
// Set data for each series
const areaData = createAreaData(
concept.timeStart,
concept.timeEnd,
concept.priceTop
);
areaSeries.setData(areaData);
const topLineData = createLineData(
concept.timeStart,
concept.timeEnd,
concept.priceTop
);
topLine.setData(topLineData);
const bottomLineData = createLineData(
concept.timeStart,
concept.timeEnd,
concept.priceBottom
);
bottomLine.setData(bottomLineData);
// Store series references
seriesMapRef.current.set(conceptId, {
area: areaSeries,
topLine,
bottomLine,
});
});
}, [chartRef, visible]);
return () => {
// Cleanup on unmount
seriesMapRef.current.forEach((conceptSeries) => {
try {
chart.removeSeries(conceptSeries.area);
chart.removeSeries(conceptSeries.topLine);
chart.removeSeries(conceptSeries.bottomLine);
} catch {
// Series might already be removed
}
});
seriesMapRef.current.clear();
};
}, [chartRef, ictConcepts, visible, colors, showLabels]);
// Handle click events for concepts
useEffect(() => {
if (!chartRef.current || !onConceptClick || ictConcepts.length === 0) return;
const chart = chartRef.current;
const handleClick = (param: { time?: Time; point?: { x: number; y: number } }) => {
if (!param.time || !param.point) return;
const clickedTime = (param.time as number) * 1000;
// Find the concept that contains the clicked time
const clickedConcept = ictConcepts.find(
(concept) =>
clickedTime >= concept.timeStart && clickedTime <= concept.timeEnd
);
if (clickedConcept) {
onConceptClick(clickedConcept);
}
};
chart.subscribeClick(handleClick);
return () => {
chart.unsubscribeClick(handleClick);
};
}, [chartRef, ictConcepts, onConceptClick]);
// Update visibility
useEffect(() => {
seriesMapRef.current.forEach((conceptSeries) => {
conceptSeries.area.applyOptions({ visible });
conceptSeries.topLine.applyOptions({ visible });
conceptSeries.bottomLine.applyOptions({ visible });
});
}, [visible]);
return null;
};

View File

@ -1,6 +1,7 @@
/**
* MLPredictionOverlay Component
* Renders ML price predictions as a line overlay on the trading chart
* Renders ML price predictions with confidence bands on the trading chart
* Supports: prediction line, upper/lower confidence bands, bullish/bearish colors
*/
import { useEffect, useRef } from 'react';
@ -15,8 +16,48 @@ export interface MLPredictionOverlayProps {
chartRef: React.RefObject<IChartApi>;
predictions: MLPrediction[];
visible?: boolean;
lineColor?: string;
showConfidenceBands?: boolean;
colors?: {
bullish: string;
bearish: string;
neutral: string;
bandUpper: string;
bandLower: string;
};
lineWidth?: number;
bandOpacity?: number;
}
// ============================================================================
// Default Colors
// ============================================================================
const DEFAULT_COLORS = {
bullish: '#10b981',
bearish: '#ef4444',
neutral: '#3b82f6',
bandUpper: 'rgba(16, 185, 129, 0.15)',
bandLower: 'rgba(239, 68, 68, 0.15)',
};
// ============================================================================
// Helper Functions
// ============================================================================
function getPredictionColor(type: MLPrediction['type'], colors: typeof DEFAULT_COLORS): string {
switch (type) {
case 'buy':
return colors.bullish;
case 'sell':
return colors.bearish;
default:
return colors.neutral;
}
}
function calculateConfidenceBand(price: number, confidence: number, direction: 'upper' | 'lower'): number {
const bandWidth = price * (1 - confidence) * 0.02;
return direction === 'upper' ? price + bandWidth : price - bandWidth;
}
// ============================================================================
@ -27,40 +68,90 @@ export const MLPredictionOverlay: React.FC<MLPredictionOverlayProps> = ({
chartRef,
predictions,
visible = true,
lineColor = '#3b82f6',
showConfidenceBands = true,
colors = DEFAULT_COLORS,
lineWidth = 2,
bandOpacity = 0.15,
}) => {
const predictionSeriesRef = useRef<ISeriesApi<'Line'> | null>(null);
const upperBandSeriesRef = useRef<ISeriesApi<'Line'> | null>(null);
const lowerBandSeriesRef = useRef<ISeriesApi<'Line'> | null>(null);
const areaSeriesRef = useRef<ISeriesApi<'Area'> | null>(null);
// Initialize prediction line series
// Initialize prediction series
useEffect(() => {
if (!chartRef.current || !visible) return;
const chart = chartRef.current;
// Create line series for predictions
// Create main prediction line series
const predictionSeries = chart.addLineSeries({
color: lineColor,
color: colors.neutral,
lineWidth: lineWidth as 1 | 2 | 3 | 4,
lineStyle: 0,
crosshairMarkerVisible: true,
crosshairMarkerRadius: 6,
crosshairMarkerBorderColor: lineColor,
crosshairMarkerBackgroundColor: lineColor,
crosshairMarkerRadius: 4,
lastValueVisible: true,
priceLineVisible: true,
priceLineVisible: false,
title: 'ML Prediction',
});
predictionSeriesRef.current = predictionSeries;
// Create confidence band series if enabled
if (showConfidenceBands) {
// Upper band (dashed line)
const upperBandSeries = chart.addLineSeries({
color: colors.bandUpper.replace(String(bandOpacity), '0.5'),
lineWidth: 1,
lineStyle: 2,
crosshairMarkerVisible: false,
lastValueVisible: false,
priceLineVisible: false,
});
upperBandSeriesRef.current = upperBandSeries;
// Lower band (dashed line)
const lowerBandSeries = chart.addLineSeries({
color: colors.bandLower.replace(String(bandOpacity), '0.5'),
lineWidth: 1,
lineStyle: 2,
crosshairMarkerVisible: false,
lastValueVisible: false,
priceLineVisible: false,
});
lowerBandSeriesRef.current = lowerBandSeries;
// Area fill between bands
const areaSeries = chart.addAreaSeries({
topColor: `rgba(59, 130, 246, ${bandOpacity})`,
bottomColor: 'transparent',
lineColor: 'transparent',
crosshairMarkerVisible: false,
lastValueVisible: false,
priceLineVisible: false,
});
areaSeriesRef.current = areaSeries;
}
return () => {
if (predictionSeriesRef.current) {
chart.removeSeries(predictionSeriesRef.current);
try { chart.removeSeries(predictionSeriesRef.current); } catch { /* ignore */ }
predictionSeriesRef.current = null;
}
if (upperBandSeriesRef.current) {
try { chart.removeSeries(upperBandSeriesRef.current); } catch { /* ignore */ }
upperBandSeriesRef.current = null;
}
if (lowerBandSeriesRef.current) {
try { chart.removeSeries(lowerBandSeriesRef.current); } catch { /* ignore */ }
lowerBandSeriesRef.current = null;
}
if (areaSeriesRef.current) {
try { chart.removeSeries(areaSeriesRef.current); } catch { /* ignore */ }
areaSeriesRef.current = null;
}
};
}, [chartRef, visible, lineColor, lineWidth]);
}, [chartRef, visible, showConfidenceBands, colors, lineWidth, bandOpacity]);
// Update prediction data
useEffect(() => {
@ -68,24 +159,56 @@ export const MLPredictionOverlay: React.FC<MLPredictionOverlayProps> = ({
return;
}
// Convert predictions to line data
const lineData: LineData[] = predictions.map((pred) => ({
// Sort predictions by time
const sortedPredictions = [...predictions].sort((a, b) => a.timestamp - b.timestamp);
// Main prediction line
const lineData: LineData[] = sortedPredictions.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 line color based on latest prediction type
const latestPrediction = sortedPredictions[sortedPredictions.length - 1];
const lineColor = getPredictionColor(latestPrediction.type, colors);
predictionSeriesRef.current.applyOptions({ color: lineColor });
// Update confidence bands if enabled
if (showConfidenceBands && upperBandSeriesRef.current && lowerBandSeriesRef.current) {
const upperBandData: LineData[] = sortedPredictions.map((pred) => ({
time: (pred.timestamp / 1000) as Time,
value: calculateConfidenceBand(pred.price, pred.confidence, 'upper'),
}));
const lowerBandData: LineData[] = sortedPredictions.map((pred) => ({
time: (pred.timestamp / 1000) as Time,
value: calculateConfidenceBand(pred.price, pred.confidence, 'lower'),
}));
upperBandSeriesRef.current.setData(upperBandData);
lowerBandSeriesRef.current.setData(lowerBandData);
// Update area series with upper band data
if (areaSeriesRef.current) {
areaSeriesRef.current.setData(upperBandData);
}
}
}, [predictions, visible, showConfidenceBands, colors]);
// Update visibility
useEffect(() => {
if (!predictionSeriesRef.current) return;
const series = [
predictionSeriesRef.current,
upperBandSeriesRef.current,
lowerBandSeriesRef.current,
areaSeriesRef.current,
];
predictionSeriesRef.current.applyOptions({
visible,
series.forEach((s) => {
if (s) {
s.applyOptions({ visible });
}
});
}, [visible]);

View File

@ -0,0 +1,413 @@
/**
* OverlayControlPanel Component
* UI panel for controlling ML overlay visibility and configuration
* Provides toggles for each overlay type and configuration options
*/
import React, { useState, useCallback } from 'react';
// ============================================================================
// Types
// ============================================================================
export interface OverlayState {
predictions: boolean;
signals: boolean;
ictConcepts: boolean;
amdZones: boolean;
confidenceBands: boolean;
}
export interface OverlayConfig {
predictionModel: string;
signalMinConfidence: number;
amdLookback: number;
refreshInterval: number;
}
export interface OverlayControlPanelProps {
overlayState: OverlayState;
onOverlayChange: (key: keyof OverlayState, value: boolean) => void;
config?: OverlayConfig;
onConfigChange?: (config: OverlayConfig) => void;
availableModels?: string[];
isLoading?: boolean;
compact?: boolean;
theme?: 'dark' | 'light';
}
// ============================================================================
// Default Values
// ============================================================================
const DEFAULT_CONFIG: OverlayConfig = {
predictionModel: 'ensemble',
signalMinConfidence: 0.6,
amdLookback: 10,
refreshInterval: 30,
};
const DEFAULT_MODELS = ['ensemble', 'lstm', 'xgboost', 'random-forest'];
// ============================================================================
// Subcomponents
// ============================================================================
interface ToggleSwitchProps {
label: string;
description?: string;
checked: boolean;
onChange: (checked: boolean) => void;
disabled?: boolean;
theme: 'dark' | 'light';
}
const ToggleSwitch: React.FC<ToggleSwitchProps> = ({
label,
description,
checked,
onChange,
disabled = false,
theme,
}) => {
const textColor = theme === 'dark' ? 'text-gray-200' : 'text-gray-800';
const descColor = theme === 'dark' ? 'text-gray-400' : 'text-gray-500';
return (
<label className="flex items-center justify-between cursor-pointer group">
<div className="flex flex-col">
<span className={`text-sm font-medium ${textColor}`}>{label}</span>
{description && (
<span className={`text-xs ${descColor}`}>{description}</span>
)}
</div>
<div className="relative">
<input
type="checkbox"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
disabled={disabled}
className="sr-only"
/>
<div
className={`w-10 h-5 rounded-full transition-colors duration-200 ${
checked
? 'bg-blue-500'
: theme === 'dark'
? 'bg-gray-600'
: 'bg-gray-300'
} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<div
className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white transition-transform duration-200 ${
checked ? 'translate-x-5' : ''
}`}
/>
</div>
</div>
</label>
);
};
interface SliderInputProps {
label: string;
value: number;
onChange: (value: number) => void;
min: number;
max: number;
step?: number;
unit?: string;
theme: 'dark' | 'light';
}
const SliderInput: React.FC<SliderInputProps> = ({
label,
value,
onChange,
min,
max,
step = 1,
unit = '',
theme,
}) => {
const textColor = theme === 'dark' ? 'text-gray-200' : 'text-gray-800';
const valueColor = theme === 'dark' ? 'text-blue-400' : 'text-blue-600';
return (
<div className="space-y-1">
<div className="flex justify-between">
<span className={`text-sm ${textColor}`}>{label}</span>
<span className={`text-sm font-mono ${valueColor}`}>
{value}
{unit}
</span>
</div>
<input
type="range"
min={min}
max={max}
step={step}
value={value}
onChange={(e) => onChange(Number(e.target.value))}
className="w-full h-2 bg-gray-600 rounded-lg appearance-none cursor-pointer accent-blue-500"
/>
</div>
);
};
interface SelectInputProps {
label: string;
value: string;
onChange: (value: string) => void;
options: { value: string; label: string }[];
theme: 'dark' | 'light';
}
const SelectInput: React.FC<SelectInputProps> = ({
label,
value,
onChange,
options,
theme,
}) => {
const textColor = theme === 'dark' ? 'text-gray-200' : 'text-gray-800';
const bgColor = theme === 'dark' ? 'bg-gray-700' : 'bg-gray-100';
const borderColor = theme === 'dark' ? 'border-gray-600' : 'border-gray-300';
return (
<div className="space-y-1">
<span className={`text-sm ${textColor}`}>{label}</span>
<select
value={value}
onChange={(e) => onChange(e.target.value)}
className={`w-full px-3 py-1.5 text-sm rounded-md border ${bgColor} ${borderColor} ${textColor} focus:outline-none focus:ring-2 focus:ring-blue-500`}
>
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
);
};
// ============================================================================
// Main Component
// ============================================================================
export const OverlayControlPanel: React.FC<OverlayControlPanelProps> = ({
overlayState,
onOverlayChange,
config = DEFAULT_CONFIG,
onConfigChange,
availableModels = DEFAULT_MODELS,
isLoading = false,
compact = false,
theme = 'dark',
}) => {
const [showAdvanced, setShowAdvanced] = useState(false);
const bgColor = theme === 'dark' ? 'bg-gray-800' : 'bg-white';
const borderColor = theme === 'dark' ? 'border-gray-700' : 'border-gray-200';
const headerColor = theme === 'dark' ? 'text-gray-100' : 'text-gray-900';
const dividerColor = theme === 'dark' ? 'border-gray-700' : 'border-gray-200';
const handleConfigChange = useCallback(
(key: keyof OverlayConfig, value: string | number) => {
if (onConfigChange) {
onConfigChange({
...config,
[key]: value,
});
}
},
[config, onConfigChange]
);
if (compact) {
return (
<div
className={`flex items-center gap-2 px-3 py-2 rounded-lg ${bgColor} border ${borderColor}`}
>
<span className={`text-xs font-medium ${headerColor} mr-2`}>Overlays:</span>
{Object.entries(overlayState).map(([key, value]) => (
<button
key={key}
onClick={() => onOverlayChange(key as keyof OverlayState, !value)}
className={`px-2 py-1 text-xs rounded-md transition-colors ${
value
? 'bg-blue-500 text-white'
: theme === 'dark'
? 'bg-gray-700 text-gray-400'
: 'bg-gray-200 text-gray-600'
}`}
disabled={isLoading}
>
{key.charAt(0).toUpperCase() + key.slice(1).replace(/([A-Z])/g, ' $1')}
</button>
))}
</div>
);
}
return (
<div
className={`rounded-lg ${bgColor} border ${borderColor} overflow-hidden`}
>
{/* Header */}
<div
className={`flex items-center justify-between px-4 py-3 border-b ${dividerColor}`}
>
<h3 className={`text-sm font-semibold ${headerColor}`}>ML Overlays</h3>
{isLoading && (
<div className="flex items-center gap-1.5 text-xs text-gray-400">
<svg className="animate-spin h-3 w-3" viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
Loading...
</div>
)}
</div>
{/* Toggle Section */}
<div className="p-4 space-y-3">
<ToggleSwitch
label="ML Predictions"
description="Price prediction lines"
checked={overlayState.predictions}
onChange={(v) => onOverlayChange('predictions', v)}
disabled={isLoading}
theme={theme}
/>
<ToggleSwitch
label="Confidence Bands"
description="Upper/lower prediction range"
checked={overlayState.confidenceBands}
onChange={(v) => onOverlayChange('confidenceBands', v)}
disabled={isLoading || !overlayState.predictions}
theme={theme}
/>
<ToggleSwitch
label="Trading Signals"
description="Buy/Sell/Hold markers"
checked={overlayState.signals}
onChange={(v) => onOverlayChange('signals', v)}
disabled={isLoading}
theme={theme}
/>
<ToggleSwitch
label="ICT Concepts"
description="Order blocks, FVG, liquidity"
checked={overlayState.ictConcepts}
onChange={(v) => onOverlayChange('ictConcepts', v)}
disabled={isLoading}
theme={theme}
/>
<ToggleSwitch
label="AMD Zones"
description="Accumulation/Manipulation/Distribution"
checked={overlayState.amdZones}
onChange={(v) => onOverlayChange('amdZones', v)}
disabled={isLoading}
theme={theme}
/>
</div>
{/* Advanced Settings */}
{onConfigChange && (
<>
<div className={`border-t ${dividerColor}`}>
<button
onClick={() => setShowAdvanced(!showAdvanced)}
className={`w-full flex items-center justify-between px-4 py-2 text-sm ${
theme === 'dark' ? 'text-gray-300 hover:bg-gray-700' : 'text-gray-600 hover:bg-gray-50'
}`}
>
<span>Advanced Settings</span>
<svg
className={`w-4 h-4 transition-transform ${showAdvanced ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
</div>
{showAdvanced && (
<div className="p-4 space-y-4">
<SelectInput
label="Prediction Model"
value={config.predictionModel}
onChange={(v) => handleConfigChange('predictionModel', v)}
options={availableModels.map((m) => ({
value: m,
label: m.charAt(0).toUpperCase() + m.slice(1).replace(/-/g, ' '),
}))}
theme={theme}
/>
<SliderInput
label="Min Signal Confidence"
value={config.signalMinConfidence * 100}
onChange={(v) => handleConfigChange('signalMinConfidence', v / 100)}
min={40}
max={95}
step={5}
unit="%"
theme={theme}
/>
<SliderInput
label="AMD Lookback Zones"
value={config.amdLookback}
onChange={(v) => handleConfigChange('amdLookback', v)}
min={5}
max={30}
step={5}
theme={theme}
/>
<SliderInput
label="Refresh Interval"
value={config.refreshInterval}
onChange={(v) => handleConfigChange('refreshInterval', v)}
min={10}
max={120}
step={10}
unit="s"
theme={theme}
/>
</div>
)}
</>
)}
</div>
);
};
export default OverlayControlPanel;

View File

@ -1,9 +1,10 @@
/**
* SignalMarkers Component
* Renders buy/sell signal markers on the trading chart
* Renders buy/sell/hold signal markers on the trading chart
* Supports custom colors, shapes, and enhanced tooltips
*/
import { useEffect, useRef } from 'react';
import { useEffect, useRef, useCallback } from 'react';
import type { IChartApi, ISeriesApi, Time, SeriesMarker } from 'lightweight-charts';
import type { SignalMarker } from '../../../../../types/mlOverlay.types';
@ -11,24 +12,117 @@ import type { SignalMarker } from '../../../../../types/mlOverlay.types';
// Types
// ============================================================================
export type SignalType = 'BUY' | 'SELL' | 'HOLD';
export interface EnhancedSignalMarker extends SignalMarker {
signalType?: SignalType;
confidence?: number;
price?: number;
stopLoss?: number;
takeProfit?: number;
riskReward?: number;
}
export interface SignalMarkersProps {
chartRef: React.RefObject<IChartApi>;
candleSeriesRef: React.RefObject<ISeriesApi<'Candlestick'>>;
signals: SignalMarker[];
signals: EnhancedSignalMarker[];
visible?: boolean;
colors?: {
buy: string;
sell: string;
hold: string;
};
showConfidence?: boolean;
onSignalClick?: (signal: EnhancedSignalMarker) => void;
}
// ============================================================================
// Default Colors
// ============================================================================
const DEFAULT_COLORS = {
buy: '#10b981',
sell: '#ef4444',
hold: '#f59e0b',
};
// ============================================================================
// Helper Functions
// ============================================================================
function convertToLightweightMarkers(signals: SignalMarker[]): SeriesMarker<Time>[] {
function getSignalColor(
signal: EnhancedSignalMarker,
colors: typeof DEFAULT_COLORS
): string {
if (signal.color) return signal.color;
switch (signal.signalType) {
case 'BUY':
return colors.buy;
case 'SELL':
return colors.sell;
case 'HOLD':
return colors.hold;
default:
return colors.hold;
}
}
function getSignalShape(signalType?: SignalType): 'arrowUp' | 'arrowDown' | 'circle' {
switch (signalType) {
case 'BUY':
return 'arrowUp';
case 'SELL':
return 'arrowDown';
case 'HOLD':
return 'circle';
default:
return 'circle';
}
}
function getSignalPosition(signalType?: SignalType): 'aboveBar' | 'belowBar' {
switch (signalType) {
case 'BUY':
return 'belowBar';
case 'SELL':
return 'aboveBar';
default:
return 'aboveBar';
}
}
function formatSignalText(signal: EnhancedSignalMarker, showConfidence: boolean): string {
const parts: string[] = [];
if (signal.signalType) {
parts.push(signal.signalType);
}
if (showConfidence && signal.confidence !== undefined) {
parts.push(`${Math.round(signal.confidence * 100)}%`);
}
if (signal.riskReward !== undefined) {
parts.push(`RR:${signal.riskReward.toFixed(1)}`);
}
return parts.join(' ') || signal.text;
}
function convertToLightweightMarkers(
signals: EnhancedSignalMarker[],
colors: typeof DEFAULT_COLORS,
showConfidence: boolean
): SeriesMarker<Time>[] {
return signals.map((signal) => ({
time: (signal.time / 1000) as Time,
position: signal.position,
color: signal.color,
shape: signal.shape,
text: signal.text,
position: signal.position || getSignalPosition(signal.signalType),
color: getSignalColor(signal, colors),
shape: signal.shape || getSignalShape(signal.signalType),
text: formatSignalText(signal, showConfidence),
size: signal.confidence ? Math.max(1, Math.min(3, Math.round(signal.confidence * 3))) : 2,
}));
}
@ -41,29 +135,89 @@ export const SignalMarkers: React.FC<SignalMarkersProps> = ({
candleSeriesRef,
signals,
visible = true,
colors = DEFAULT_COLORS,
showConfidence = true,
onSignalClick,
}) => {
const markersSetRef = useRef(false);
const signalsRef = useRef<EnhancedSignalMarker[]>([]);
// Store signals for click handling
useEffect(() => {
if (!chartRef.current || !candleSeriesRef.current || !visible || signals.length === 0) {
signalsRef.current = signals;
}, [signals]);
// Handle chart click to detect signal clicks
const handleChartClick = useCallback(
(param: { time?: Time; point?: { x: number; y: number } }) => {
if (!param.time || !onSignalClick || signalsRef.current.length === 0) return;
const clickedTime = param.time as number;
// Find signal closest to clicked time
const tolerance = 3600; // 1 hour tolerance
const clickedSignal = signalsRef.current.find((signal) => {
const signalTime = signal.time / 1000;
return Math.abs(signalTime - clickedTime) < tolerance;
});
if (clickedSignal) {
onSignalClick(clickedSignal);
}
},
[onSignalClick]
);
// Subscribe to chart clicks
useEffect(() => {
if (!chartRef.current || !onSignalClick) return;
const chart = chartRef.current;
chart.subscribeClick(handleChartClick);
return () => {
chart.unsubscribeClick(handleChartClick);
};
}, [chartRef, handleChartClick, onSignalClick]);
// Update markers
useEffect(() => {
if (!chartRef.current || !candleSeriesRef.current) {
return;
}
const candleSeries = candleSeriesRef.current;
if (!visible || signals.length === 0) {
// Clear markers if not visible or no signals
if (markersSetRef.current) {
candleSeries.setMarkers([]);
markersSetRef.current = false;
}
return;
}
// Convert and set markers
const markers = convertToLightweightMarkers(signals);
const markers = convertToLightweightMarkers(signals, colors, showConfidence);
// Sort markers by time (required by lightweight-charts)
markers.sort((a, b) => (a.time as number) - (b.time as number));
candleSeries.setMarkers(markers);
markersSetRef.current = true;
return () => {
// Clear markers on cleanup
if (markersSetRef.current && candleSeriesRef.current) {
candleSeriesRef.current.setMarkers([]);
try {
candleSeriesRef.current.setMarkers([]);
} catch {
// Series might be removed
}
markersSetRef.current = false;
}
};
}, [chartRef, candleSeriesRef, signals, visible]);
}, [chartRef, candleSeriesRef, signals, visible, colors, showConfidence]);
return null;
};

View File

@ -3,14 +3,30 @@
* Exports all ML chart overlay components
*/
// ML Prediction Overlay
export { MLPredictionOverlay } from './MLPredictionOverlay';
export type { MLPredictionOverlayProps } from './MLPredictionOverlay';
// Signal Markers
export { SignalMarkers } from './SignalMarkers';
export type { SignalMarkersProps } from './SignalMarkers';
export type {
SignalMarkersProps,
EnhancedSignalMarker,
SignalType,
} from './SignalMarkers';
// ICT Concepts Overlay
export { ICTConceptsOverlay } from './ICTConceptsOverlay';
export type { ICTConceptsOverlayProps } from './ICTConceptsOverlay';
// AMD Zones Overlay
export { AMDZonesOverlay } from './AMDZonesOverlay';
export type { AMDZonesOverlayProps, AMDZone, AMDPhaseType } from './AMDZonesOverlay';
// Overlay Control Panel
export { OverlayControlPanel } from './OverlayControlPanel';
export type {
OverlayControlPanelProps,
OverlayState,
OverlayConfig,
} from './OverlayControlPanel';