[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:
parent
6a8cb3b9cf
commit
9e8f69d7f2
31
src/hooks/charts/index.ts
Normal file
31
src/hooks/charts/index.ts
Normal 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';
|
||||||
246
src/hooks/charts/useChartOverlays.ts
Normal file
246
src/hooks/charts/useChartOverlays.ts
Normal 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;
|
||||||
150
src/hooks/charts/usePredictions.ts
Normal file
150
src/hooks/charts/usePredictions.ts
Normal 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;
|
||||||
214
src/hooks/charts/useSignals.ts
Normal file
214
src/hooks/charts/useSignals.ts
Normal 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;
|
||||||
@ -3,4 +3,33 @@
|
|||||||
* Barrel export for all custom hooks
|
* Barrel export for all custom hooks
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// ML Analysis Hooks
|
||||||
export { useMLAnalysis, useQuickSignals, DEFAULT_SYMBOLS } from './useMLAnalysis';
|
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';
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* ICTConceptsOverlay Component
|
* 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 { useEffect, useRef } from 'react';
|
||||||
import type { IChartApi, ISeriesApi, Time } from 'lightweight-charts';
|
import type { IChartApi, ISeriesApi, Time, LineData } from 'lightweight-charts';
|
||||||
import type { ICTConcept } from '../../../../../types/mlOverlay.types';
|
import type { ICTConcept, ICTConceptType } from '../../../../../types/mlOverlay.types';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Types
|
// Types
|
||||||
@ -15,28 +16,64 @@ export interface ICTConceptsOverlayProps {
|
|||||||
chartRef: React.RefObject<IChartApi>;
|
chartRef: React.RefObject<IChartApi>;
|
||||||
ictConcepts: ICTConcept[];
|
ictConcepts: ICTConcept[];
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
colors?: {
|
colors?: Record<ICTConceptType, string>;
|
||||||
OrderBlock: string;
|
showLabels?: boolean;
|
||||||
FVG: string;
|
onConceptClick?: (concept: ICTConcept) => void;
|
||||||
Liquidity: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RectangleData {
|
interface ConceptSeries {
|
||||||
time: Time;
|
area: ISeriesApi<'Area'>;
|
||||||
value: number;
|
topLine: ISeriesApi<'Line'>;
|
||||||
|
bottomLine: ISeriesApi<'Line'>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Default Colors
|
// Default Colors
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const DEFAULT_COLORS = {
|
const DEFAULT_COLORS: Record<ICTConceptType, string> = {
|
||||||
OrderBlock: 'rgba(59, 130, 246, 0.3)', // Blue
|
OrderBlock: 'rgba(59, 130, 246, 0.25)', // Blue
|
||||||
FVG: 'rgba(251, 191, 36, 0.3)', // Yellow
|
FVG: 'rgba(251, 191, 36, 0.25)', // Yellow/Amber
|
||||||
Liquidity: 'rgba(249, 115, 22, 0.3)', // Orange
|
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
|
// Component
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -46,87 +83,154 @@ export const ICTConceptsOverlay: React.FC<ICTConceptsOverlayProps> = ({
|
|||||||
ictConcepts,
|
ictConcepts,
|
||||||
visible = true,
|
visible = true,
|
||||||
colors = DEFAULT_COLORS,
|
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
|
// Initialize and update series for ICT concepts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chartRef.current || !visible) return;
|
if (!chartRef.current) return;
|
||||||
|
|
||||||
const chart = chartRef.current;
|
const chart = chartRef.current;
|
||||||
const seriesMap = new Map<string, ISeriesApi<'Area'>>();
|
|
||||||
|
|
||||||
// Create series for each concept type
|
// 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) => {
|
ictConcepts.forEach((concept, index) => {
|
||||||
const conceptId = `${concept.type}-${index}`;
|
const conceptId = generateConceptId(concept, index);
|
||||||
|
const fillColor = colors[concept.type] || DEFAULT_COLORS[concept.type];
|
||||||
|
const borderColor = BORDER_COLORS[concept.type];
|
||||||
|
|
||||||
if (!conceptSeriesRef.current.has(conceptId)) {
|
// Create area series for the zone fill
|
||||||
const color = colors[concept.type] || DEFAULT_COLORS[concept.type];
|
const areaSeries = chart.addAreaSeries({
|
||||||
|
topColor: fillColor,
|
||||||
const series = chart.addAreaSeries({
|
bottomColor: fillColor,
|
||||||
topColor: color,
|
lineColor: 'transparent',
|
||||||
bottomColor: color,
|
|
||||||
lineColor: color.replace('0.3', '0.8'),
|
|
||||||
lineWidth: 1,
|
lineWidth: 1,
|
||||||
crosshairMarkerVisible: false,
|
crosshairMarkerVisible: false,
|
||||||
lastValueVisible: false,
|
lastValueVisible: false,
|
||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
title: `${concept.type}`,
|
priceScaleId: 'right',
|
||||||
});
|
});
|
||||||
|
|
||||||
seriesMap.set(conceptId, series);
|
// 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',
|
||||||
});
|
});
|
||||||
|
|
||||||
conceptSeriesRef.current = seriesMap;
|
// 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// Cleanup: remove all series
|
// Cleanup on unmount
|
||||||
conceptSeriesRef.current.forEach((series) => {
|
seriesMapRef.current.forEach((conceptSeries) => {
|
||||||
chart.removeSeries(series);
|
try {
|
||||||
});
|
chart.removeSeries(conceptSeries.area);
|
||||||
conceptSeriesRef.current.clear();
|
chart.removeSeries(conceptSeries.topLine);
|
||||||
};
|
chart.removeSeries(conceptSeries.bottomLine);
|
||||||
}, [chartRef, ictConcepts, ictConcepts.length, visible, colors]);
|
} catch {
|
||||||
|
// Series might already be removed
|
||||||
// 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]);
|
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
|
// Update visibility
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chartRef.current) return;
|
seriesMapRef.current.forEach((conceptSeries) => {
|
||||||
|
conceptSeries.area.applyOptions({ visible });
|
||||||
conceptSeriesRef.current.forEach((series) => {
|
conceptSeries.topLine.applyOptions({ visible });
|
||||||
series.applyOptions({
|
conceptSeries.bottomLine.applyOptions({ visible });
|
||||||
visible,
|
|
||||||
});
|
});
|
||||||
});
|
}, [visible]);
|
||||||
}, [chartRef, visible]);
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MLPredictionOverlay Component
|
* 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';
|
import { useEffect, useRef } from 'react';
|
||||||
@ -15,8 +16,48 @@ export interface MLPredictionOverlayProps {
|
|||||||
chartRef: React.RefObject<IChartApi>;
|
chartRef: React.RefObject<IChartApi>;
|
||||||
predictions: MLPrediction[];
|
predictions: MLPrediction[];
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
lineColor?: string;
|
showConfidenceBands?: boolean;
|
||||||
|
colors?: {
|
||||||
|
bullish: string;
|
||||||
|
bearish: string;
|
||||||
|
neutral: string;
|
||||||
|
bandUpper: string;
|
||||||
|
bandLower: string;
|
||||||
|
};
|
||||||
lineWidth?: number;
|
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,
|
chartRef,
|
||||||
predictions,
|
predictions,
|
||||||
visible = true,
|
visible = true,
|
||||||
lineColor = '#3b82f6',
|
showConfidenceBands = true,
|
||||||
|
colors = DEFAULT_COLORS,
|
||||||
lineWidth = 2,
|
lineWidth = 2,
|
||||||
|
bandOpacity = 0.15,
|
||||||
}) => {
|
}) => {
|
||||||
const predictionSeriesRef = useRef<ISeriesApi<'Line'> | null>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!chartRef.current || !visible) return;
|
if (!chartRef.current || !visible) return;
|
||||||
|
|
||||||
const chart = chartRef.current;
|
const chart = chartRef.current;
|
||||||
|
|
||||||
// Create line series for predictions
|
// Create main prediction line series
|
||||||
const predictionSeries = chart.addLineSeries({
|
const predictionSeries = chart.addLineSeries({
|
||||||
color: lineColor,
|
color: colors.neutral,
|
||||||
lineWidth: lineWidth as 1 | 2 | 3 | 4,
|
lineWidth: lineWidth as 1 | 2 | 3 | 4,
|
||||||
lineStyle: 0,
|
lineStyle: 0,
|
||||||
crosshairMarkerVisible: true,
|
crosshairMarkerVisible: true,
|
||||||
crosshairMarkerRadius: 6,
|
crosshairMarkerRadius: 4,
|
||||||
crosshairMarkerBorderColor: lineColor,
|
|
||||||
crosshairMarkerBackgroundColor: lineColor,
|
|
||||||
lastValueVisible: true,
|
lastValueVisible: true,
|
||||||
priceLineVisible: true,
|
priceLineVisible: false,
|
||||||
title: 'ML Prediction',
|
title: 'ML Prediction',
|
||||||
});
|
});
|
||||||
|
|
||||||
predictionSeriesRef.current = predictionSeries;
|
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 () => {
|
return () => {
|
||||||
if (predictionSeriesRef.current) {
|
if (predictionSeriesRef.current) {
|
||||||
chart.removeSeries(predictionSeriesRef.current);
|
try { chart.removeSeries(predictionSeriesRef.current); } catch { /* ignore */ }
|
||||||
predictionSeriesRef.current = null;
|
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
|
// Update prediction data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -68,24 +159,56 @@ export const MLPredictionOverlay: React.FC<MLPredictionOverlayProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert predictions to line data
|
// Sort predictions by time
|
||||||
const lineData: LineData[] = predictions.map((pred) => ({
|
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,
|
time: (pred.timestamp / 1000) as Time,
|
||||||
value: pred.price,
|
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);
|
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
|
// Update visibility
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!predictionSeriesRef.current) return;
|
const series = [
|
||||||
|
predictionSeriesRef.current,
|
||||||
|
upperBandSeriesRef.current,
|
||||||
|
lowerBandSeriesRef.current,
|
||||||
|
areaSeriesRef.current,
|
||||||
|
];
|
||||||
|
|
||||||
predictionSeriesRef.current.applyOptions({
|
series.forEach((s) => {
|
||||||
visible,
|
if (s) {
|
||||||
|
s.applyOptions({ visible });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
@ -1,9 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* SignalMarkers Component
|
* 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 { IChartApi, ISeriesApi, Time, SeriesMarker } from 'lightweight-charts';
|
||||||
import type { SignalMarker } from '../../../../../types/mlOverlay.types';
|
import type { SignalMarker } from '../../../../../types/mlOverlay.types';
|
||||||
|
|
||||||
@ -11,24 +12,117 @@ import type { SignalMarker } from '../../../../../types/mlOverlay.types';
|
|||||||
// 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 {
|
export interface SignalMarkersProps {
|
||||||
chartRef: React.RefObject<IChartApi>;
|
chartRef: React.RefObject<IChartApi>;
|
||||||
candleSeriesRef: React.RefObject<ISeriesApi<'Candlestick'>>;
|
candleSeriesRef: React.RefObject<ISeriesApi<'Candlestick'>>;
|
||||||
signals: SignalMarker[];
|
signals: EnhancedSignalMarker[];
|
||||||
visible?: boolean;
|
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
|
// 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) => ({
|
return signals.map((signal) => ({
|
||||||
time: (signal.time / 1000) as Time,
|
time: (signal.time / 1000) as Time,
|
||||||
position: signal.position,
|
position: signal.position || getSignalPosition(signal.signalType),
|
||||||
color: signal.color,
|
color: getSignalColor(signal, colors),
|
||||||
shape: signal.shape,
|
shape: signal.shape || getSignalShape(signal.signalType),
|
||||||
text: signal.text,
|
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,
|
candleSeriesRef,
|
||||||
signals,
|
signals,
|
||||||
visible = true,
|
visible = true,
|
||||||
|
colors = DEFAULT_COLORS,
|
||||||
|
showConfidence = true,
|
||||||
|
onSignalClick,
|
||||||
}) => {
|
}) => {
|
||||||
const markersSetRef = useRef(false);
|
const markersSetRef = useRef(false);
|
||||||
|
const signalsRef = useRef<EnhancedSignalMarker[]>([]);
|
||||||
|
|
||||||
|
// Store signals for click handling
|
||||||
useEffect(() => {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const candleSeries = candleSeriesRef.current;
|
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
|
// 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);
|
candleSeries.setMarkers(markers);
|
||||||
markersSetRef.current = true;
|
markersSetRef.current = true;
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// Clear markers on cleanup
|
// Clear markers on cleanup
|
||||||
if (markersSetRef.current && candleSeriesRef.current) {
|
if (markersSetRef.current && candleSeriesRef.current) {
|
||||||
|
try {
|
||||||
candleSeriesRef.current.setMarkers([]);
|
candleSeriesRef.current.setMarkers([]);
|
||||||
|
} catch {
|
||||||
|
// Series might be removed
|
||||||
|
}
|
||||||
markersSetRef.current = false;
|
markersSetRef.current = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [chartRef, candleSeriesRef, signals, visible]);
|
}, [chartRef, candleSeriesRef, signals, visible, colors, showConfidence]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,14 +3,30 @@
|
|||||||
* Exports all ML chart overlay components
|
* Exports all ML chart overlay components
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// ML Prediction Overlay
|
||||||
export { MLPredictionOverlay } from './MLPredictionOverlay';
|
export { MLPredictionOverlay } from './MLPredictionOverlay';
|
||||||
export type { MLPredictionOverlayProps } from './MLPredictionOverlay';
|
export type { MLPredictionOverlayProps } from './MLPredictionOverlay';
|
||||||
|
|
||||||
|
// Signal Markers
|
||||||
export { SignalMarkers } from './SignalMarkers';
|
export { SignalMarkers } from './SignalMarkers';
|
||||||
export type { SignalMarkersProps } from './SignalMarkers';
|
export type {
|
||||||
|
SignalMarkersProps,
|
||||||
|
EnhancedSignalMarker,
|
||||||
|
SignalType,
|
||||||
|
} from './SignalMarkers';
|
||||||
|
|
||||||
|
// ICT Concepts Overlay
|
||||||
export { ICTConceptsOverlay } from './ICTConceptsOverlay';
|
export { ICTConceptsOverlay } from './ICTConceptsOverlay';
|
||||||
export type { ICTConceptsOverlayProps } from './ICTConceptsOverlay';
|
export type { ICTConceptsOverlayProps } from './ICTConceptsOverlay';
|
||||||
|
|
||||||
|
// AMD Zones Overlay
|
||||||
export { AMDZonesOverlay } from './AMDZonesOverlay';
|
export { AMDZonesOverlay } from './AMDZonesOverlay';
|
||||||
export type { AMDZonesOverlayProps, AMDZone, AMDPhaseType } from './AMDZonesOverlay';
|
export type { AMDZonesOverlayProps, AMDZone, AMDPhaseType } from './AMDZonesOverlay';
|
||||||
|
|
||||||
|
// Overlay Control Panel
|
||||||
|
export { OverlayControlPanel } from './OverlayControlPanel';
|
||||||
|
export type {
|
||||||
|
OverlayControlPanelProps,
|
||||||
|
OverlayState,
|
||||||
|
OverlayConfig,
|
||||||
|
} from './OverlayControlPanel';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user