From f7a5ddcca8b1cd430c9f20d326ca65a9fb157536 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Mon, 26 Jan 2026 11:03:20 -0600 Subject: [PATCH] [OQI-006] feat: Add 3 ML utility panel components - ModelSelector: Model selection with dropdown/tabs/cards variants - EnsemblePanel: Ensemble voting configuration with weight sliders - ICTAnalysisPanel: ICT analysis parameters with collapsible sections Co-Authored-By: Claude Opus 4.5 --- src/modules/ml/components/EnsemblePanel.tsx | 379 ++++++++++++++ .../ml/components/ICTAnalysisPanel.tsx | 475 ++++++++++++++++++ src/modules/ml/components/ModelSelector.tsx | 274 ++++++++++ src/modules/ml/components/index.ts | 8 + 4 files changed, 1136 insertions(+) create mode 100644 src/modules/ml/components/EnsemblePanel.tsx create mode 100644 src/modules/ml/components/ICTAnalysisPanel.tsx create mode 100644 src/modules/ml/components/ModelSelector.tsx diff --git a/src/modules/ml/components/EnsemblePanel.tsx b/src/modules/ml/components/EnsemblePanel.tsx new file mode 100644 index 0000000..581c44c --- /dev/null +++ b/src/modules/ml/components/EnsemblePanel.tsx @@ -0,0 +1,379 @@ +/** + * EnsemblePanel Component + * Configuration panel for ensemble model weights and voting mechanism + * OQI-006: Senales ML + */ + +import React, { useState, useMemo, useCallback } from 'react'; +import { + Sparkles, + Sliders, + RotateCcw, + Save, + AlertTriangle, + CheckCircle, + Info, + Lock, + Unlock, + TrendingUp, + BarChart3, + Percent, +} from 'lucide-react'; + +export interface ModelWeight { + modelId: string; + modelName: string; + weight: number; + accuracy: number; + locked?: boolean; +} + +export interface EnsembleConfig { + votingMethod: 'weighted' | 'majority' | 'unanimous'; + minimumAgreement: number; + confidenceThreshold: number; + weights: ModelWeight[]; +} + +export interface EnsemblePanelProps { + config: EnsembleConfig; + onConfigChange: (config: EnsembleConfig) => void; + onSave?: () => void; + onReset?: () => void; + isLoading?: boolean; + readOnly?: boolean; +} + +const EnsemblePanel: React.FC = ({ + config, + onConfigChange, + onSave, + onReset, + isLoading = false, + readOnly = false, +}) => { + const [hasChanges, setHasChanges] = useState(false); + + const totalWeight = useMemo( + () => config.weights.reduce((sum, w) => sum + w.weight, 0), + [config.weights] + ); + + const isValidConfig = useMemo(() => { + return Math.abs(totalWeight - 100) < 0.1 && config.minimumAgreement >= 1; + }, [totalWeight, config.minimumAgreement]); + + const updateWeight = useCallback( + (modelId: string, newWeight: number) => { + if (readOnly) return; + + const updatedWeights = config.weights.map((w) => + w.modelId === modelId ? { ...w, weight: Math.max(0, Math.min(100, newWeight)) } : w + ); + + onConfigChange({ ...config, weights: updatedWeights }); + setHasChanges(true); + }, + [config, onConfigChange, readOnly] + ); + + const toggleLock = useCallback( + (modelId: string) => { + if (readOnly) return; + + const updatedWeights = config.weights.map((w) => + w.modelId === modelId ? { ...w, locked: !w.locked } : w + ); + + onConfigChange({ ...config, weights: updatedWeights }); + setHasChanges(true); + }, + [config, onConfigChange, readOnly] + ); + + const normalizeWeights = useCallback(() => { + if (readOnly || totalWeight === 0) return; + + const unlockedModels = config.weights.filter((w) => !w.locked); + const lockedTotal = config.weights + .filter((w) => w.locked) + .reduce((sum, w) => sum + w.weight, 0); + + const remainingWeight = 100 - lockedTotal; + const unlockedTotal = unlockedModels.reduce((sum, w) => sum + w.weight, 0); + + const updatedWeights = config.weights.map((w) => { + if (w.locked) return w; + return { + ...w, + weight: unlockedTotal > 0 ? (w.weight / unlockedTotal) * remainingWeight : remainingWeight / unlockedModels.length, + }; + }); + + onConfigChange({ ...config, weights: updatedWeights }); + setHasChanges(true); + }, [config, onConfigChange, readOnly, totalWeight]); + + const autoWeightByAccuracy = useCallback(() => { + if (readOnly) return; + + const totalAccuracy = config.weights.reduce((sum, w) => sum + w.accuracy, 0); + const updatedWeights = config.weights.map((w) => ({ + ...w, + weight: (w.accuracy / totalAccuracy) * 100, + locked: false, + })); + + onConfigChange({ ...config, weights: updatedWeights }); + setHasChanges(true); + }, [config, onConfigChange, readOnly]); + + const getWeightColor = (weight: number) => { + if (weight >= 30) return 'bg-green-500'; + if (weight >= 15) return 'bg-blue-500'; + if (weight >= 5) return 'bg-yellow-500'; + return 'bg-gray-500'; + }; + + const handleSave = () => { + if (onSave && isValidConfig) { + onSave(); + setHasChanges(false); + } + }; + + const handleReset = () => { + if (onReset) { + onReset(); + setHasChanges(false); + } + }; + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

Ensemble Configuration

+

Configure model weights and voting

+
+
+ + {!readOnly && ( +
+ + +
+ )} +
+ + {/* Voting Method */} +
+ +
+ {(['weighted', 'majority', 'unanimous'] as const).map((method) => ( + + ))} +
+
+ + {/* Thresholds */} +
+
+ +
+ !readOnly && onConfigChange({ ...config, minimumAgreement: Number(e.target.value) })} + disabled={readOnly} + className="flex-1 h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-purple-500" + /> + + {config.minimumAgreement}/{config.weights.length} + +
+
+ +
+ +
+ !readOnly && onConfigChange({ ...config, confidenceThreshold: Number(e.target.value) })} + disabled={readOnly} + className="flex-1 h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-purple-500" + /> + {config.confidenceThreshold}% +
+
+
+ + {/* Model Weights */} +
+
+ + {!readOnly && ( +
+ + | + +
+ )} +
+ + {/* Weight validation */} +
+ {isValidConfig ? ( + <> + + Total: {totalWeight.toFixed(1)}% - Configuration valid + + ) : ( + <> + + Total: {totalWeight.toFixed(1)}% - Must equal 100% + + )} +
+ + {/* Weight sliders */} +
+ {config.weights.map((model) => ( +
+
+
+ + {model.modelName} + ({model.accuracy}% acc) +
+
+ updateWeight(model.modelId, Number(e.target.value))} + disabled={readOnly || model.locked} + className="w-16 px-2 py-1 text-sm text-right bg-gray-800 border border-gray-700 rounded text-white disabled:opacity-50" + /> + % +
+
+
+
+
+ {!readOnly && !model.locked && ( + updateWeight(model.modelId, Number(e.target.value))} + className="w-full h-2 mt-2 bg-transparent appearance-none cursor-pointer accent-blue-500" + style={{ + background: 'transparent', + }} + /> + )} +
+ ))} +
+
+ + {/* Info */} +
+ +
+

+ Weighted voting: Each model's prediction is multiplied by its weight. +

+

+ Majority voting: Signal generated when majority of models agree. +

+

+ Unanimous: All models must agree for a signal to be generated. +

+
+
+
+ ); +}; + +export default EnsemblePanel; diff --git a/src/modules/ml/components/ICTAnalysisPanel.tsx b/src/modules/ml/components/ICTAnalysisPanel.tsx new file mode 100644 index 0000000..85a99d3 --- /dev/null +++ b/src/modules/ml/components/ICTAnalysisPanel.tsx @@ -0,0 +1,475 @@ +/** + * ICTAnalysisPanel Component + * Configuration panel for ICT (Inner Circle Trader) analysis parameters + * OQI-006: Senales ML + */ + +import React, { useState, useCallback } from 'react'; +import { + Settings2, + Target, + TrendingUp, + BarChart2, + Clock, + Layers, + Eye, + EyeOff, + RotateCcw, + Save, + AlertTriangle, + CheckCircle, + Info, + ChevronDown, + ChevronUp, +} from 'lucide-react'; + +export interface ICTParams { + // Timeframe settings + timeframe: '1m' | '5m' | '15m' | '1h' | '4h' | '1d'; + htfBias: '15m' | '1h' | '4h' | '1d' | '1w'; + + // Order Block settings + orderBlocks: { + enabled: boolean; + lookback: number; + minImbalance: number; + showMitigated: boolean; + }; + + // Fair Value Gap settings + fvg: { + enabled: boolean; + minSize: number; + showFilled: boolean; + filterByTrend: boolean; + }; + + // Market Structure settings + marketStructure: { + enabled: boolean; + swingLookback: number; + showBOS: boolean; + showCHOCH: boolean; + }; + + // Liquidity settings + liquidity: { + enabled: boolean; + showBSL: boolean; + showSSL: boolean; + equalHighsLows: boolean; + }; + + // Session settings + sessions: { + showAsian: boolean; + showLondon: boolean; + showNewYork: boolean; + highlightKillzones: boolean; + }; +} + +export interface ICTAnalysisPanelProps { + params: ICTParams; + onParamsChange: (params: ICTParams) => void; + onSave?: () => void; + onReset?: () => void; + isLoading?: boolean; + readOnly?: boolean; +} + +const ICTAnalysisPanel: React.FC = ({ + params, + onParamsChange, + onSave, + onReset, + isLoading = false, + readOnly = false, +}) => { + const [hasChanges, setHasChanges] = useState(false); + const [expandedSections, setExpandedSections] = useState(['timeframe', 'orderBlocks']); + + const toggleSection = (section: string) => { + setExpandedSections((prev) => + prev.includes(section) ? prev.filter((s) => s !== section) : [...prev, section] + ); + }; + + const updateParams = useCallback( + (updates: Partial) => { + if (readOnly) return; + onParamsChange({ ...params, ...updates }); + setHasChanges(true); + }, + [params, onParamsChange, readOnly] + ); + + const handleSave = () => { + if (onSave) { + onSave(); + setHasChanges(false); + } + }; + + const handleReset = () => { + if (onReset) { + onReset(); + setHasChanges(false); + } + }; + + const timeframes = ['1m', '5m', '15m', '1h', '4h', '1d'] as const; + const htfOptions = ['15m', '1h', '4h', '1d', '1w'] as const; + + const SectionHeader: React.FC<{ + id: string; + icon: React.ReactNode; + title: string; + enabled?: boolean; + onToggle?: (enabled: boolean) => void; + }> = ({ id, icon, title, enabled, onToggle }) => ( + + )} + {expandedSections.includes(id) ? ( + + ) : ( + + )} +
+ + ); + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

ICT Analysis Settings

+

Configure market structure detection

+
+
+ + {!readOnly && ( +
+ + +
+ )} +
+ + {/* Sections */} +
+ {/* Timeframe Section */} +
+ } title="Timeframe" /> + {expandedSections.includes('timeframe') && ( +
+
+ +
+ {timeframes.map((tf) => ( + + ))} +
+
+
+ +
+ {htfOptions.map((tf) => ( + + ))} +
+
+
+ )} +
+ + {/* Order Blocks Section */} +
+ } + title="Order Blocks" + enabled={params.orderBlocks.enabled} + onToggle={(enabled) => updateParams({ orderBlocks: { ...params.orderBlocks, enabled } })} + /> + {expandedSections.includes('orderBlocks') && params.orderBlocks.enabled && ( +
+
+ + + updateParams({ orderBlocks: { ...params.orderBlocks, lookback: Number(e.target.value) } }) + } + disabled={readOnly} + className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-cyan-500" + /> +
+
+ + + updateParams({ orderBlocks: { ...params.orderBlocks, minImbalance: Number(e.target.value) } }) + } + disabled={readOnly} + className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-cyan-500" + /> +
+ +
+ )} +
+ + {/* Fair Value Gap Section */} +
+ } + title="Fair Value Gaps" + enabled={params.fvg.enabled} + onToggle={(enabled) => updateParams({ fvg: { ...params.fvg, enabled } })} + /> + {expandedSections.includes('fvg') && params.fvg.enabled && ( +
+
+ + updateParams({ fvg: { ...params.fvg, minSize: Number(e.target.value) } })} + disabled={readOnly} + className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-cyan-500" + /> +
+ + +
+ )} +
+ + {/* Market Structure Section */} +
+ } + title="Market Structure" + enabled={params.marketStructure.enabled} + onToggle={(enabled) => updateParams({ marketStructure: { ...params.marketStructure, enabled } })} + /> + {expandedSections.includes('marketStructure') && params.marketStructure.enabled && ( +
+
+ + + updateParams({ marketStructure: { ...params.marketStructure, swingLookback: Number(e.target.value) } }) + } + disabled={readOnly} + className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-cyan-500" + /> +
+ + +
+ )} +
+ + {/* Liquidity Section */} +
+ } + title="Liquidity" + enabled={params.liquidity.enabled} + onToggle={(enabled) => updateParams({ liquidity: { ...params.liquidity, enabled } })} + /> + {expandedSections.includes('liquidity') && params.liquidity.enabled && ( +
+ + + +
+ )} +
+
+ + {/* Info */} +
+ +
+

+ ICT (Inner Circle Trader) concepts help identify institutional order flow through market structure, + order blocks, and liquidity zones. Adjust sensitivity based on your trading timeframe. +

+
+
+
+ ); +}; + +export default ICTAnalysisPanel; diff --git a/src/modules/ml/components/ModelSelector.tsx b/src/modules/ml/components/ModelSelector.tsx new file mode 100644 index 0000000..5cba2d8 --- /dev/null +++ b/src/modules/ml/components/ModelSelector.tsx @@ -0,0 +1,274 @@ +/** + * ModelSelector Component + * Dropdown/tabs interface to switch between ML prediction models + * OQI-006: Senales ML + */ + +import React, { useMemo } from 'react'; +import { + Brain, + ChevronDown, + Check, + Cpu, + LineChart, + Network, + Layers, + Sparkles, + TrendingUp, + AlertCircle, +} from 'lucide-react'; + +export interface MLModel { + id: string; + name: string; + type: 'lstm' | 'xgboost' | 'random_forest' | 'svm' | 'ensemble' | 'transformer'; + accuracy: number; + lastUpdated: string; + status: 'active' | 'training' | 'inactive' | 'deprecated'; + description?: string; + features?: string[]; +} + +export interface ModelSelectorProps { + models: MLModel[]; + selectedModelId: string; + onModelChange: (modelId: string) => void; + variant?: 'dropdown' | 'tabs' | 'cards'; + showMetrics?: boolean; + disabled?: boolean; + loading?: boolean; +} + +const ModelSelector: React.FC = ({ + models, + selectedModelId, + onModelChange, + variant = 'dropdown', + showMetrics = true, + disabled = false, + loading = false, +}) => { + const [isOpen, setIsOpen] = React.useState(false); + const dropdownRef = React.useRef(null); + + const selectedModel = useMemo( + () => models.find((m) => m.id === selectedModelId), + [models, selectedModelId] + ); + + // Close dropdown on outside click + React.useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const getModelIcon = (type: MLModel['type']) => { + switch (type) { + case 'lstm': + return ; + case 'xgboost': + return ; + case 'random_forest': + return ; + case 'svm': + return ; + case 'ensemble': + return ; + case 'transformer': + return ; + default: + return ; + } + }; + + const getStatusColor = (status: MLModel['status']) => { + switch (status) { + case 'active': + return 'text-green-400 bg-green-500/20'; + case 'training': + return 'text-yellow-400 bg-yellow-500/20'; + case 'inactive': + return 'text-gray-400 bg-gray-500/20'; + case 'deprecated': + return 'text-red-400 bg-red-500/20'; + default: + return 'text-gray-400 bg-gray-500/20'; + } + }; + + const getAccuracyColor = (accuracy: number) => { + if (accuracy >= 70) return 'text-green-400'; + if (accuracy >= 50) return 'text-yellow-400'; + return 'text-red-400'; + }; + + // Dropdown variant + if (variant === 'dropdown') { + return ( +
+ + + {isOpen && ( +
+
+ {models.map((model) => ( + + ))} +
+
+ )} +
+ ); + } + + // Tabs variant + if (variant === 'tabs') { + return ( +
+ {models.map((model) => ( + + ))} +
+ ); + } + + // Cards variant + return ( +
+ {models.map((model) => ( + + ))} +
+ ); +}; + +export default ModelSelector; diff --git a/src/modules/ml/components/index.ts b/src/modules/ml/components/index.ts index 35286ad..d2dbeb4 100644 --- a/src/modules/ml/components/index.ts +++ b/src/modules/ml/components/index.ts @@ -20,3 +20,11 @@ export { default as ModelAccuracyDashboard } from './ModelAccuracyDashboard'; export type { ModelMetrics } from './ModelAccuracyDashboard'; export { default as BacktestResultsVisualization } from './BacktestResultsVisualization'; export type { BacktestResult, BacktestTrade } from './BacktestResultsVisualization'; + +// Model Selection & Configuration Components (OQI-006) +export { default as ModelSelector } from './ModelSelector'; +export type { MLModel, ModelSelectorProps } from './ModelSelector'; +export { default as EnsemblePanel } from './EnsemblePanel'; +export type { ModelWeight, EnsembleConfig, EnsemblePanelProps } from './EnsemblePanel'; +export { default as ICTAnalysisPanel } from './ICTAnalysisPanel'; +export type { ICTParams, ICTAnalysisPanelProps } from './ICTAnalysisPanel';