[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
|
||||
*/
|
||||
|
||||
// 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';
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
/**
|
||||
* ICTConceptsOverlay Component
|
||||
* Renders ICT concepts (Order Blocks, FVG, Liquidity) as rectangles on the chart
|
||||
* Renders ICT concepts (Order Blocks, FVG, Liquidity) as highlighted zones on the chart
|
||||
* Uses area series with top/bottom lines to create box-like regions
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import type { IChartApi, ISeriesApi, Time } from 'lightweight-charts';
|
||||
import type { ICTConcept } from '../../../../../types/mlOverlay.types';
|
||||
import type { IChartApi, ISeriesApi, Time, LineData } from 'lightweight-charts';
|
||||
import type { ICTConcept, ICTConceptType } from '../../../../../types/mlOverlay.types';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
@ -15,28 +16,64 @@ export interface ICTConceptsOverlayProps {
|
||||
chartRef: React.RefObject<IChartApi>;
|
||||
ictConcepts: ICTConcept[];
|
||||
visible?: boolean;
|
||||
colors?: {
|
||||
OrderBlock: string;
|
||||
FVG: string;
|
||||
Liquidity: string;
|
||||
};
|
||||
colors?: Record<ICTConceptType, string>;
|
||||
showLabels?: boolean;
|
||||
onConceptClick?: (concept: ICTConcept) => void;
|
||||
}
|
||||
|
||||
interface RectangleData {
|
||||
time: Time;
|
||||
value: number;
|
||||
interface ConceptSeries {
|
||||
area: ISeriesApi<'Area'>;
|
||||
topLine: ISeriesApi<'Line'>;
|
||||
bottomLine: ISeriesApi<'Line'>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Default Colors
|
||||
// ============================================================================
|
||||
|
||||
const DEFAULT_COLORS = {
|
||||
OrderBlock: 'rgba(59, 130, 246, 0.3)', // Blue
|
||||
FVG: 'rgba(251, 191, 36, 0.3)', // Yellow
|
||||
Liquidity: 'rgba(249, 115, 22, 0.3)', // Orange
|
||||
const DEFAULT_COLORS: Record<ICTConceptType, string> = {
|
||||
OrderBlock: 'rgba(59, 130, 246, 0.25)', // Blue
|
||||
FVG: 'rgba(251, 191, 36, 0.25)', // Yellow/Amber
|
||||
Liquidity: 'rgba(249, 115, 22, 0.25)', // Orange
|
||||
};
|
||||
|
||||
const BORDER_COLORS: Record<ICTConceptType, string> = {
|
||||
OrderBlock: 'rgba(59, 130, 246, 0.6)',
|
||||
FVG: 'rgba(251, 191, 36, 0.6)',
|
||||
Liquidity: 'rgba(249, 115, 22, 0.6)',
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
function generateConceptId(concept: ICTConcept, index: number): string {
|
||||
return `ict-${concept.type}-${index}-${concept.timeStart}`;
|
||||
}
|
||||
|
||||
function createLineData(
|
||||
timeStart: number,
|
||||
timeEnd: number,
|
||||
price: number
|
||||
): LineData[] {
|
||||
return [
|
||||
{ time: (timeStart / 1000) as Time, value: price },
|
||||
{ time: (timeEnd / 1000) as Time, value: price },
|
||||
];
|
||||
}
|
||||
|
||||
function createAreaData(
|
||||
timeStart: number,
|
||||
timeEnd: number,
|
||||
priceTop: number
|
||||
): LineData[] {
|
||||
// Area series uses the value as the top, and bottomColor fills below
|
||||
return [
|
||||
{ time: (timeStart / 1000) as Time, value: priceTop },
|
||||
{ time: (timeEnd / 1000) as Time, value: priceTop },
|
||||
];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Component
|
||||
// ============================================================================
|
||||
@ -46,87 +83,154 @@ export const ICTConceptsOverlay: React.FC<ICTConceptsOverlayProps> = ({
|
||||
ictConcepts,
|
||||
visible = true,
|
||||
colors = DEFAULT_COLORS,
|
||||
showLabels = true,
|
||||
onConceptClick,
|
||||
}) => {
|
||||
const conceptSeriesRef = useRef<Map<string, ISeriesApi<'Area'>>>(new Map());
|
||||
const seriesMapRef = useRef<Map<string, ConceptSeries>>(new Map());
|
||||
|
||||
// Initialize series for each ICT concept
|
||||
useEffect(() => {
|
||||
if (!chartRef.current || !visible) return;
|
||||
|
||||
const chart = chartRef.current;
|
||||
const seriesMap = new Map<string, ISeriesApi<'Area'>>();
|
||||
|
||||
// Create series for each concept type
|
||||
ictConcepts.forEach((concept, index) => {
|
||||
const conceptId = `${concept.type}-${index}`;
|
||||
|
||||
if (!conceptSeriesRef.current.has(conceptId)) {
|
||||
const color = colors[concept.type] || DEFAULT_COLORS[concept.type];
|
||||
|
||||
const series = chart.addAreaSeries({
|
||||
topColor: color,
|
||||
bottomColor: color,
|
||||
lineColor: color.replace('0.3', '0.8'),
|
||||
lineWidth: 1,
|
||||
crosshairMarkerVisible: false,
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
title: `${concept.type}`,
|
||||
});
|
||||
|
||||
seriesMap.set(conceptId, series);
|
||||
}
|
||||
});
|
||||
|
||||
conceptSeriesRef.current = seriesMap;
|
||||
|
||||
return () => {
|
||||
// Cleanup: remove all series
|
||||
conceptSeriesRef.current.forEach((series) => {
|
||||
chart.removeSeries(series);
|
||||
});
|
||||
conceptSeriesRef.current.clear();
|
||||
};
|
||||
}, [chartRef, ictConcepts, ictConcepts.length, visible, colors]);
|
||||
|
||||
// Update concept data
|
||||
useEffect(() => {
|
||||
if (!chartRef.current || !visible || ictConcepts.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
ictConcepts.forEach((concept, index) => {
|
||||
const conceptId = `${concept.type}-${index}`;
|
||||
const series = conceptSeriesRef.current.get(conceptId);
|
||||
|
||||
if (!series) return;
|
||||
|
||||
// Create rectangle data
|
||||
const timeStart = (concept.timeStart / 1000) as Time;
|
||||
const timeEnd = (concept.timeEnd / 1000) as Time;
|
||||
|
||||
const rectangleData: RectangleData[] = [
|
||||
{ time: timeStart, value: concept.priceTop },
|
||||
{ time: timeStart, value: concept.priceBottom },
|
||||
{ time: timeEnd, value: concept.priceBottom },
|
||||
{ time: timeEnd, value: concept.priceTop },
|
||||
{ time: timeStart, value: concept.priceTop },
|
||||
];
|
||||
|
||||
series.setData(rectangleData);
|
||||
});
|
||||
}, [chartRef, ictConcepts, visible]);
|
||||
|
||||
// Update visibility
|
||||
// Initialize and update series for ICT concepts
|
||||
useEffect(() => {
|
||||
if (!chartRef.current) return;
|
||||
|
||||
conceptSeriesRef.current.forEach((series) => {
|
||||
series.applyOptions({
|
||||
visible,
|
||||
const chart = chartRef.current;
|
||||
|
||||
// Clear existing series first
|
||||
seriesMapRef.current.forEach((conceptSeries) => {
|
||||
try {
|
||||
chart.removeSeries(conceptSeries.area);
|
||||
chart.removeSeries(conceptSeries.topLine);
|
||||
chart.removeSeries(conceptSeries.bottomLine);
|
||||
} catch {
|
||||
// Series might already be removed
|
||||
}
|
||||
});
|
||||
seriesMapRef.current.clear();
|
||||
|
||||
if (!visible || ictConcepts.length === 0) return;
|
||||
|
||||
// Create series for each concept
|
||||
ictConcepts.forEach((concept, index) => {
|
||||
const conceptId = generateConceptId(concept, index);
|
||||
const fillColor = colors[concept.type] || DEFAULT_COLORS[concept.type];
|
||||
const borderColor = BORDER_COLORS[concept.type];
|
||||
|
||||
// Create area series for the zone fill
|
||||
const areaSeries = chart.addAreaSeries({
|
||||
topColor: fillColor,
|
||||
bottomColor: fillColor,
|
||||
lineColor: 'transparent',
|
||||
lineWidth: 1,
|
||||
crosshairMarkerVisible: false,
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
priceScaleId: 'right',
|
||||
});
|
||||
|
||||
// Create top boundary line
|
||||
const topLine = chart.addLineSeries({
|
||||
color: borderColor,
|
||||
lineWidth: 1,
|
||||
lineStyle: 0,
|
||||
crosshairMarkerVisible: false,
|
||||
lastValueVisible: showLabels,
|
||||
priceLineVisible: false,
|
||||
title: showLabels ? concept.type : '',
|
||||
priceScaleId: 'right',
|
||||
});
|
||||
|
||||
// Create bottom boundary line
|
||||
const bottomLine = chart.addLineSeries({
|
||||
color: borderColor,
|
||||
lineWidth: 1,
|
||||
lineStyle: 0,
|
||||
crosshairMarkerVisible: false,
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
priceScaleId: 'right',
|
||||
});
|
||||
|
||||
// Set data for each series
|
||||
const areaData = createAreaData(
|
||||
concept.timeStart,
|
||||
concept.timeEnd,
|
||||
concept.priceTop
|
||||
);
|
||||
areaSeries.setData(areaData);
|
||||
|
||||
const topLineData = createLineData(
|
||||
concept.timeStart,
|
||||
concept.timeEnd,
|
||||
concept.priceTop
|
||||
);
|
||||
topLine.setData(topLineData);
|
||||
|
||||
const bottomLineData = createLineData(
|
||||
concept.timeStart,
|
||||
concept.timeEnd,
|
||||
concept.priceBottom
|
||||
);
|
||||
bottomLine.setData(bottomLineData);
|
||||
|
||||
// Store series references
|
||||
seriesMapRef.current.set(conceptId, {
|
||||
area: areaSeries,
|
||||
topLine,
|
||||
bottomLine,
|
||||
});
|
||||
});
|
||||
}, [chartRef, visible]);
|
||||
|
||||
return () => {
|
||||
// Cleanup on unmount
|
||||
seriesMapRef.current.forEach((conceptSeries) => {
|
||||
try {
|
||||
chart.removeSeries(conceptSeries.area);
|
||||
chart.removeSeries(conceptSeries.topLine);
|
||||
chart.removeSeries(conceptSeries.bottomLine);
|
||||
} catch {
|
||||
// Series might already be removed
|
||||
}
|
||||
});
|
||||
seriesMapRef.current.clear();
|
||||
};
|
||||
}, [chartRef, ictConcepts, visible, colors, showLabels]);
|
||||
|
||||
// Handle click events for concepts
|
||||
useEffect(() => {
|
||||
if (!chartRef.current || !onConceptClick || ictConcepts.length === 0) return;
|
||||
|
||||
const chart = chartRef.current;
|
||||
|
||||
const handleClick = (param: { time?: Time; point?: { x: number; y: number } }) => {
|
||||
if (!param.time || !param.point) return;
|
||||
|
||||
const clickedTime = (param.time as number) * 1000;
|
||||
|
||||
// Find the concept that contains the clicked time
|
||||
const clickedConcept = ictConcepts.find(
|
||||
(concept) =>
|
||||
clickedTime >= concept.timeStart && clickedTime <= concept.timeEnd
|
||||
);
|
||||
|
||||
if (clickedConcept) {
|
||||
onConceptClick(clickedConcept);
|
||||
}
|
||||
};
|
||||
|
||||
chart.subscribeClick(handleClick);
|
||||
|
||||
return () => {
|
||||
chart.unsubscribeClick(handleClick);
|
||||
};
|
||||
}, [chartRef, ictConcepts, onConceptClick]);
|
||||
|
||||
// Update visibility
|
||||
useEffect(() => {
|
||||
seriesMapRef.current.forEach((conceptSeries) => {
|
||||
conceptSeries.area.applyOptions({ visible });
|
||||
conceptSeries.topLine.applyOptions({ visible });
|
||||
conceptSeries.bottomLine.applyOptions({ visible });
|
||||
});
|
||||
}, [visible]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
/**
|
||||
* MLPredictionOverlay Component
|
||||
* Renders ML price predictions as a line overlay on the trading chart
|
||||
* Renders ML price predictions with confidence bands on the trading chart
|
||||
* Supports: prediction line, upper/lower confidence bands, bullish/bearish colors
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
@ -15,8 +16,48 @@ export interface MLPredictionOverlayProps {
|
||||
chartRef: React.RefObject<IChartApi>;
|
||||
predictions: MLPrediction[];
|
||||
visible?: boolean;
|
||||
lineColor?: string;
|
||||
showConfidenceBands?: boolean;
|
||||
colors?: {
|
||||
bullish: string;
|
||||
bearish: string;
|
||||
neutral: string;
|
||||
bandUpper: string;
|
||||
bandLower: string;
|
||||
};
|
||||
lineWidth?: number;
|
||||
bandOpacity?: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Default Colors
|
||||
// ============================================================================
|
||||
|
||||
const DEFAULT_COLORS = {
|
||||
bullish: '#10b981',
|
||||
bearish: '#ef4444',
|
||||
neutral: '#3b82f6',
|
||||
bandUpper: 'rgba(16, 185, 129, 0.15)',
|
||||
bandLower: 'rgba(239, 68, 68, 0.15)',
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
function getPredictionColor(type: MLPrediction['type'], colors: typeof DEFAULT_COLORS): string {
|
||||
switch (type) {
|
||||
case 'buy':
|
||||
return colors.bullish;
|
||||
case 'sell':
|
||||
return colors.bearish;
|
||||
default:
|
||||
return colors.neutral;
|
||||
}
|
||||
}
|
||||
|
||||
function calculateConfidenceBand(price: number, confidence: number, direction: 'upper' | 'lower'): number {
|
||||
const bandWidth = price * (1 - confidence) * 0.02;
|
||||
return direction === 'upper' ? price + bandWidth : price - bandWidth;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@ -27,40 +68,90 @@ export const MLPredictionOverlay: React.FC<MLPredictionOverlayProps> = ({
|
||||
chartRef,
|
||||
predictions,
|
||||
visible = true,
|
||||
lineColor = '#3b82f6',
|
||||
showConfidenceBands = true,
|
||||
colors = DEFAULT_COLORS,
|
||||
lineWidth = 2,
|
||||
bandOpacity = 0.15,
|
||||
}) => {
|
||||
const predictionSeriesRef = useRef<ISeriesApi<'Line'> | null>(null);
|
||||
const upperBandSeriesRef = useRef<ISeriesApi<'Line'> | null>(null);
|
||||
const lowerBandSeriesRef = useRef<ISeriesApi<'Line'> | null>(null);
|
||||
const areaSeriesRef = useRef<ISeriesApi<'Area'> | null>(null);
|
||||
|
||||
// Initialize prediction line series
|
||||
// Initialize prediction series
|
||||
useEffect(() => {
|
||||
if (!chartRef.current || !visible) return;
|
||||
|
||||
const chart = chartRef.current;
|
||||
|
||||
// Create line series for predictions
|
||||
// Create main prediction line series
|
||||
const predictionSeries = chart.addLineSeries({
|
||||
color: lineColor,
|
||||
color: colors.neutral,
|
||||
lineWidth: lineWidth as 1 | 2 | 3 | 4,
|
||||
lineStyle: 0,
|
||||
crosshairMarkerVisible: true,
|
||||
crosshairMarkerRadius: 6,
|
||||
crosshairMarkerBorderColor: lineColor,
|
||||
crosshairMarkerBackgroundColor: lineColor,
|
||||
crosshairMarkerRadius: 4,
|
||||
lastValueVisible: true,
|
||||
priceLineVisible: true,
|
||||
priceLineVisible: false,
|
||||
title: 'ML Prediction',
|
||||
});
|
||||
|
||||
predictionSeriesRef.current = predictionSeries;
|
||||
|
||||
// Create confidence band series if enabled
|
||||
if (showConfidenceBands) {
|
||||
// Upper band (dashed line)
|
||||
const upperBandSeries = chart.addLineSeries({
|
||||
color: colors.bandUpper.replace(String(bandOpacity), '0.5'),
|
||||
lineWidth: 1,
|
||||
lineStyle: 2,
|
||||
crosshairMarkerVisible: false,
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
});
|
||||
upperBandSeriesRef.current = upperBandSeries;
|
||||
|
||||
// Lower band (dashed line)
|
||||
const lowerBandSeries = chart.addLineSeries({
|
||||
color: colors.bandLower.replace(String(bandOpacity), '0.5'),
|
||||
lineWidth: 1,
|
||||
lineStyle: 2,
|
||||
crosshairMarkerVisible: false,
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
});
|
||||
lowerBandSeriesRef.current = lowerBandSeries;
|
||||
|
||||
// Area fill between bands
|
||||
const areaSeries = chart.addAreaSeries({
|
||||
topColor: `rgba(59, 130, 246, ${bandOpacity})`,
|
||||
bottomColor: 'transparent',
|
||||
lineColor: 'transparent',
|
||||
crosshairMarkerVisible: false,
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
});
|
||||
areaSeriesRef.current = areaSeries;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (predictionSeriesRef.current) {
|
||||
chart.removeSeries(predictionSeriesRef.current);
|
||||
try { chart.removeSeries(predictionSeriesRef.current); } catch { /* ignore */ }
|
||||
predictionSeriesRef.current = null;
|
||||
}
|
||||
if (upperBandSeriesRef.current) {
|
||||
try { chart.removeSeries(upperBandSeriesRef.current); } catch { /* ignore */ }
|
||||
upperBandSeriesRef.current = null;
|
||||
}
|
||||
if (lowerBandSeriesRef.current) {
|
||||
try { chart.removeSeries(lowerBandSeriesRef.current); } catch { /* ignore */ }
|
||||
lowerBandSeriesRef.current = null;
|
||||
}
|
||||
if (areaSeriesRef.current) {
|
||||
try { chart.removeSeries(areaSeriesRef.current); } catch { /* ignore */ }
|
||||
areaSeriesRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [chartRef, visible, lineColor, lineWidth]);
|
||||
}, [chartRef, visible, showConfidenceBands, colors, lineWidth, bandOpacity]);
|
||||
|
||||
// Update prediction data
|
||||
useEffect(() => {
|
||||
@ -68,24 +159,56 @@ export const MLPredictionOverlay: React.FC<MLPredictionOverlayProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert predictions to line data
|
||||
const lineData: LineData[] = predictions.map((pred) => ({
|
||||
// Sort predictions by time
|
||||
const sortedPredictions = [...predictions].sort((a, b) => a.timestamp - b.timestamp);
|
||||
|
||||
// Main prediction line
|
||||
const lineData: LineData[] = sortedPredictions.map((pred) => ({
|
||||
time: (pred.timestamp / 1000) as Time,
|
||||
value: pred.price,
|
||||
}));
|
||||
|
||||
// Sort by time (required by lightweight-charts)
|
||||
lineData.sort((a, b) => (a.time as number) - (b.time as number));
|
||||
|
||||
predictionSeriesRef.current.setData(lineData);
|
||||
}, [predictions, visible]);
|
||||
|
||||
// Update line color based on latest prediction type
|
||||
const latestPrediction = sortedPredictions[sortedPredictions.length - 1];
|
||||
const lineColor = getPredictionColor(latestPrediction.type, colors);
|
||||
predictionSeriesRef.current.applyOptions({ color: lineColor });
|
||||
|
||||
// Update confidence bands if enabled
|
||||
if (showConfidenceBands && upperBandSeriesRef.current && lowerBandSeriesRef.current) {
|
||||
const upperBandData: LineData[] = sortedPredictions.map((pred) => ({
|
||||
time: (pred.timestamp / 1000) as Time,
|
||||
value: calculateConfidenceBand(pred.price, pred.confidence, 'upper'),
|
||||
}));
|
||||
|
||||
const lowerBandData: LineData[] = sortedPredictions.map((pred) => ({
|
||||
time: (pred.timestamp / 1000) as Time,
|
||||
value: calculateConfidenceBand(pred.price, pred.confidence, 'lower'),
|
||||
}));
|
||||
|
||||
upperBandSeriesRef.current.setData(upperBandData);
|
||||
lowerBandSeriesRef.current.setData(lowerBandData);
|
||||
|
||||
// Update area series with upper band data
|
||||
if (areaSeriesRef.current) {
|
||||
areaSeriesRef.current.setData(upperBandData);
|
||||
}
|
||||
}
|
||||
}, [predictions, visible, showConfidenceBands, colors]);
|
||||
|
||||
// Update visibility
|
||||
useEffect(() => {
|
||||
if (!predictionSeriesRef.current) return;
|
||||
const series = [
|
||||
predictionSeriesRef.current,
|
||||
upperBandSeriesRef.current,
|
||||
lowerBandSeriesRef.current,
|
||||
areaSeriesRef.current,
|
||||
];
|
||||
|
||||
predictionSeriesRef.current.applyOptions({
|
||||
visible,
|
||||
series.forEach((s) => {
|
||||
if (s) {
|
||||
s.applyOptions({ visible });
|
||||
}
|
||||
});
|
||||
}, [visible]);
|
||||
|
||||
|
||||
@ -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
|
||||
* Renders buy/sell signal markers on the trading chart
|
||||
* Renders buy/sell/hold signal markers on the trading chart
|
||||
* Supports custom colors, shapes, and enhanced tooltips
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import type { IChartApi, ISeriesApi, Time, SeriesMarker } from 'lightweight-charts';
|
||||
import type { SignalMarker } from '../../../../../types/mlOverlay.types';
|
||||
|
||||
@ -11,24 +12,117 @@ import type { SignalMarker } from '../../../../../types/mlOverlay.types';
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export type SignalType = 'BUY' | 'SELL' | 'HOLD';
|
||||
|
||||
export interface EnhancedSignalMarker extends SignalMarker {
|
||||
signalType?: SignalType;
|
||||
confidence?: number;
|
||||
price?: number;
|
||||
stopLoss?: number;
|
||||
takeProfit?: number;
|
||||
riskReward?: number;
|
||||
}
|
||||
|
||||
export interface SignalMarkersProps {
|
||||
chartRef: React.RefObject<IChartApi>;
|
||||
candleSeriesRef: React.RefObject<ISeriesApi<'Candlestick'>>;
|
||||
signals: SignalMarker[];
|
||||
signals: EnhancedSignalMarker[];
|
||||
visible?: boolean;
|
||||
colors?: {
|
||||
buy: string;
|
||||
sell: string;
|
||||
hold: string;
|
||||
};
|
||||
showConfidence?: boolean;
|
||||
onSignalClick?: (signal: EnhancedSignalMarker) => void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Default Colors
|
||||
// ============================================================================
|
||||
|
||||
const DEFAULT_COLORS = {
|
||||
buy: '#10b981',
|
||||
sell: '#ef4444',
|
||||
hold: '#f59e0b',
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
function convertToLightweightMarkers(signals: SignalMarker[]): SeriesMarker<Time>[] {
|
||||
function getSignalColor(
|
||||
signal: EnhancedSignalMarker,
|
||||
colors: typeof DEFAULT_COLORS
|
||||
): string {
|
||||
if (signal.color) return signal.color;
|
||||
|
||||
switch (signal.signalType) {
|
||||
case 'BUY':
|
||||
return colors.buy;
|
||||
case 'SELL':
|
||||
return colors.sell;
|
||||
case 'HOLD':
|
||||
return colors.hold;
|
||||
default:
|
||||
return colors.hold;
|
||||
}
|
||||
}
|
||||
|
||||
function getSignalShape(signalType?: SignalType): 'arrowUp' | 'arrowDown' | 'circle' {
|
||||
switch (signalType) {
|
||||
case 'BUY':
|
||||
return 'arrowUp';
|
||||
case 'SELL':
|
||||
return 'arrowDown';
|
||||
case 'HOLD':
|
||||
return 'circle';
|
||||
default:
|
||||
return 'circle';
|
||||
}
|
||||
}
|
||||
|
||||
function getSignalPosition(signalType?: SignalType): 'aboveBar' | 'belowBar' {
|
||||
switch (signalType) {
|
||||
case 'BUY':
|
||||
return 'belowBar';
|
||||
case 'SELL':
|
||||
return 'aboveBar';
|
||||
default:
|
||||
return 'aboveBar';
|
||||
}
|
||||
}
|
||||
|
||||
function formatSignalText(signal: EnhancedSignalMarker, showConfidence: boolean): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (signal.signalType) {
|
||||
parts.push(signal.signalType);
|
||||
}
|
||||
|
||||
if (showConfidence && signal.confidence !== undefined) {
|
||||
parts.push(`${Math.round(signal.confidence * 100)}%`);
|
||||
}
|
||||
|
||||
if (signal.riskReward !== undefined) {
|
||||
parts.push(`RR:${signal.riskReward.toFixed(1)}`);
|
||||
}
|
||||
|
||||
return parts.join(' ') || signal.text;
|
||||
}
|
||||
|
||||
function convertToLightweightMarkers(
|
||||
signals: EnhancedSignalMarker[],
|
||||
colors: typeof DEFAULT_COLORS,
|
||||
showConfidence: boolean
|
||||
): SeriesMarker<Time>[] {
|
||||
return signals.map((signal) => ({
|
||||
time: (signal.time / 1000) as Time,
|
||||
position: signal.position,
|
||||
color: signal.color,
|
||||
shape: signal.shape,
|
||||
text: signal.text,
|
||||
position: signal.position || getSignalPosition(signal.signalType),
|
||||
color: getSignalColor(signal, colors),
|
||||
shape: signal.shape || getSignalShape(signal.signalType),
|
||||
text: formatSignalText(signal, showConfidence),
|
||||
size: signal.confidence ? Math.max(1, Math.min(3, Math.round(signal.confidence * 3))) : 2,
|
||||
}));
|
||||
}
|
||||
|
||||
@ -41,29 +135,89 @@ export const SignalMarkers: React.FC<SignalMarkersProps> = ({
|
||||
candleSeriesRef,
|
||||
signals,
|
||||
visible = true,
|
||||
colors = DEFAULT_COLORS,
|
||||
showConfidence = true,
|
||||
onSignalClick,
|
||||
}) => {
|
||||
const markersSetRef = useRef(false);
|
||||
const signalsRef = useRef<EnhancedSignalMarker[]>([]);
|
||||
|
||||
// Store signals for click handling
|
||||
useEffect(() => {
|
||||
if (!chartRef.current || !candleSeriesRef.current || !visible || signals.length === 0) {
|
||||
signalsRef.current = signals;
|
||||
}, [signals]);
|
||||
|
||||
// Handle chart click to detect signal clicks
|
||||
const handleChartClick = useCallback(
|
||||
(param: { time?: Time; point?: { x: number; y: number } }) => {
|
||||
if (!param.time || !onSignalClick || signalsRef.current.length === 0) return;
|
||||
|
||||
const clickedTime = param.time as number;
|
||||
|
||||
// Find signal closest to clicked time
|
||||
const tolerance = 3600; // 1 hour tolerance
|
||||
const clickedSignal = signalsRef.current.find((signal) => {
|
||||
const signalTime = signal.time / 1000;
|
||||
return Math.abs(signalTime - clickedTime) < tolerance;
|
||||
});
|
||||
|
||||
if (clickedSignal) {
|
||||
onSignalClick(clickedSignal);
|
||||
}
|
||||
},
|
||||
[onSignalClick]
|
||||
);
|
||||
|
||||
// Subscribe to chart clicks
|
||||
useEffect(() => {
|
||||
if (!chartRef.current || !onSignalClick) return;
|
||||
|
||||
const chart = chartRef.current;
|
||||
chart.subscribeClick(handleChartClick);
|
||||
|
||||
return () => {
|
||||
chart.unsubscribeClick(handleChartClick);
|
||||
};
|
||||
}, [chartRef, handleChartClick, onSignalClick]);
|
||||
|
||||
// Update markers
|
||||
useEffect(() => {
|
||||
if (!chartRef.current || !candleSeriesRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const candleSeries = candleSeriesRef.current;
|
||||
|
||||
if (!visible || signals.length === 0) {
|
||||
// Clear markers if not visible or no signals
|
||||
if (markersSetRef.current) {
|
||||
candleSeries.setMarkers([]);
|
||||
markersSetRef.current = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert and set markers
|
||||
const markers = convertToLightweightMarkers(signals);
|
||||
const markers = convertToLightweightMarkers(signals, colors, showConfidence);
|
||||
|
||||
// Sort markers by time (required by lightweight-charts)
|
||||
markers.sort((a, b) => (a.time as number) - (b.time as number));
|
||||
|
||||
candleSeries.setMarkers(markers);
|
||||
markersSetRef.current = true;
|
||||
|
||||
return () => {
|
||||
// Clear markers on cleanup
|
||||
if (markersSetRef.current && candleSeriesRef.current) {
|
||||
candleSeriesRef.current.setMarkers([]);
|
||||
try {
|
||||
candleSeriesRef.current.setMarkers([]);
|
||||
} catch {
|
||||
// Series might be removed
|
||||
}
|
||||
markersSetRef.current = false;
|
||||
}
|
||||
};
|
||||
}, [chartRef, candleSeriesRef, signals, visible]);
|
||||
}, [chartRef, candleSeriesRef, signals, visible, colors, showConfidence]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@ -3,14 +3,30 @@
|
||||
* Exports all ML chart overlay components
|
||||
*/
|
||||
|
||||
// ML Prediction Overlay
|
||||
export { MLPredictionOverlay } from './MLPredictionOverlay';
|
||||
export type { MLPredictionOverlayProps } from './MLPredictionOverlay';
|
||||
|
||||
// Signal Markers
|
||||
export { SignalMarkers } from './SignalMarkers';
|
||||
export type { SignalMarkersProps } from './SignalMarkers';
|
||||
export type {
|
||||
SignalMarkersProps,
|
||||
EnhancedSignalMarker,
|
||||
SignalType,
|
||||
} from './SignalMarkers';
|
||||
|
||||
// ICT Concepts Overlay
|
||||
export { ICTConceptsOverlay } from './ICTConceptsOverlay';
|
||||
export type { ICTConceptsOverlayProps } from './ICTConceptsOverlay';
|
||||
|
||||
// AMD Zones Overlay
|
||||
export { AMDZonesOverlay } from './AMDZonesOverlay';
|
||||
export type { AMDZonesOverlayProps, AMDZone, AMDPhaseType } from './AMDZonesOverlay';
|
||||
|
||||
// Overlay Control Panel
|
||||
export { OverlayControlPanel } from './OverlayControlPanel';
|
||||
export type {
|
||||
OverlayControlPanelProps,
|
||||
OverlayState,
|
||||
OverlayConfig,
|
||||
} from './OverlayControlPanel';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user