trading-platform-frontend-v2/src/modules/trading/components/charts/overlays/OverlayControlPanel.tsx
Adrian Flores Cortes 9e8f69d7f2 [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>
2026-02-04 01:24:10 -06:00

414 lines
12 KiB
TypeScript

/**
* 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;