diff --git a/src/hooks/charts/index.ts b/src/hooks/charts/index.ts new file mode 100644 index 0000000..2ee0610 --- /dev/null +++ b/src/hooks/charts/index.ts @@ -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'; diff --git a/src/hooks/charts/useChartOverlays.ts b/src/hooks/charts/useChartOverlays.ts new file mode 100644 index 0000000..6bd2514 --- /dev/null +++ b/src/hooks/charts/useChartOverlays.ts @@ -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; + initialConfig?: Partial; +} + +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({ + ...DEFAULT_OVERLAY_STATE, + ...initialOverlayState, + }); + + const [config, setConfigInternal] = useState({ + ...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; diff --git a/src/hooks/charts/usePredictions.ts b/src/hooks/charts/usePredictions.ts new file mode 100644 index 0000000..f966ca9 --- /dev/null +++ b/src/hooks/charts/usePredictions.ts @@ -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 { + 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; diff --git a/src/hooks/charts/useSignals.ts b/src/hooks/charts/useSignals.ts new file mode 100644 index 0000000..0824978 --- /dev/null +++ b/src/hooks/charts/useSignals.ts @@ -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 { + 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 { + 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(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; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 03be03e..1cbc94f 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -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'; diff --git a/src/modules/trading/components/charts/overlays/ICTConceptsOverlay.tsx b/src/modules/trading/components/charts/overlays/ICTConceptsOverlay.tsx index fe7bf2f..93fae5b 100644 --- a/src/modules/trading/components/charts/overlays/ICTConceptsOverlay.tsx +++ b/src/modules/trading/components/charts/overlays/ICTConceptsOverlay.tsx @@ -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; ictConcepts: ICTConcept[]; visible?: boolean; - colors?: { - OrderBlock: string; - FVG: string; - Liquidity: string; - }; + colors?: Record; + 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 = { + 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 = { + 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 = ({ ictConcepts, visible = true, colors = DEFAULT_COLORS, + showLabels = true, + onConceptClick, }) => { - const conceptSeriesRef = useRef>>(new Map()); + const seriesMapRef = 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 + // 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; }; diff --git a/src/modules/trading/components/charts/overlays/MLPredictionOverlay.tsx b/src/modules/trading/components/charts/overlays/MLPredictionOverlay.tsx index d7938f1..44cd8b7 100644 --- a/src/modules/trading/components/charts/overlays/MLPredictionOverlay.tsx +++ b/src/modules/trading/components/charts/overlays/MLPredictionOverlay.tsx @@ -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; 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 = ({ chartRef, predictions, visible = true, - lineColor = '#3b82f6', + showConfidenceBands = true, + colors = DEFAULT_COLORS, lineWidth = 2, + bandOpacity = 0.15, }) => { const predictionSeriesRef = useRef | null>(null); + const upperBandSeriesRef = useRef | null>(null); + const lowerBandSeriesRef = useRef | null>(null); + const areaSeriesRef = useRef | 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 = ({ 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]); diff --git a/src/modules/trading/components/charts/overlays/OverlayControlPanel.tsx b/src/modules/trading/components/charts/overlays/OverlayControlPanel.tsx new file mode 100644 index 0000000..a2a2143 --- /dev/null +++ b/src/modules/trading/components/charts/overlays/OverlayControlPanel.tsx @@ -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 = ({ + 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 ( +