- 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>
414 lines
12 KiB
TypeScript
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;
|