[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 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-26 11:03:20 -06:00
parent 8347c6ad48
commit f7a5ddcca8
4 changed files with 1136 additions and 0 deletions

View File

@ -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<EnsemblePanelProps> = ({
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 (
<div className="bg-gray-800/50 rounded-xl border border-gray-700 p-4">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-purple-500/20 text-purple-400 rounded-lg">
<Sparkles className="w-5 h-5" />
</div>
<div>
<h3 className="font-semibold text-white">Ensemble Configuration</h3>
<p className="text-xs text-gray-400">Configure model weights and voting</p>
</div>
</div>
{!readOnly && (
<div className="flex items-center gap-2">
<button
onClick={handleReset}
disabled={isLoading || !hasChanges}
className="flex items-center gap-1 px-3 py-1.5 text-sm text-gray-400 hover:text-white transition-colors disabled:opacity-50"
>
<RotateCcw className="w-4 h-4" />
Reset
</button>
<button
onClick={handleSave}
disabled={isLoading || !hasChanges || !isValidConfig}
className="flex items-center gap-1 px-3 py-1.5 text-sm bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<Save className="w-4 h-4" />
Save
</button>
</div>
)}
</div>
{/* Voting Method */}
<div className="mb-6">
<label className="block text-sm text-gray-400 mb-2">Voting Method</label>
<div className="grid grid-cols-3 gap-2">
{(['weighted', 'majority', 'unanimous'] as const).map((method) => (
<button
key={method}
onClick={() => !readOnly && onConfigChange({ ...config, votingMethod: method })}
disabled={readOnly}
className={`p-3 rounded-lg border transition-all ${
config.votingMethod === method
? 'bg-purple-500/20 border-purple-500 text-purple-400'
: 'bg-gray-900/50 border-gray-700 text-gray-400 hover:border-gray-600'
} ${readOnly ? 'cursor-not-allowed' : ''}`}
>
<div className="text-sm font-medium capitalize">{method}</div>
<div className="text-xs mt-1 text-gray-500">
{method === 'weighted' && 'Use weight distribution'}
{method === 'majority' && '>50% agreement'}
{method === 'unanimous' && 'All models agree'}
</div>
</button>
))}
</div>
</div>
{/* Thresholds */}
<div className="grid grid-cols-2 gap-4 mb-6">
<div>
<label className="flex items-center gap-2 text-sm text-gray-400 mb-2">
<BarChart3 className="w-4 h-4" />
Minimum Agreement
</label>
<div className="flex items-center gap-2">
<input
type="range"
min="1"
max={config.weights.length}
value={config.minimumAgreement}
onChange={(e) => !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"
/>
<span className="text-white font-medium w-12 text-center">
{config.minimumAgreement}/{config.weights.length}
</span>
</div>
</div>
<div>
<label className="flex items-center gap-2 text-sm text-gray-400 mb-2">
<Percent className="w-4 h-4" />
Confidence Threshold
</label>
<div className="flex items-center gap-2">
<input
type="range"
min="0"
max="100"
value={config.confidenceThreshold}
onChange={(e) => !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"
/>
<span className="text-white font-medium w-12 text-center">{config.confidenceThreshold}%</span>
</div>
</div>
</div>
{/* Model Weights */}
<div className="mb-4">
<div className="flex items-center justify-between mb-3">
<label className="flex items-center gap-2 text-sm text-gray-400">
<Sliders className="w-4 h-4" />
Model Weights
</label>
{!readOnly && (
<div className="flex items-center gap-2">
<button
onClick={normalizeWeights}
className="text-xs text-blue-400 hover:text-blue-300 transition-colors"
>
Normalize to 100%
</button>
<span className="text-gray-600">|</span>
<button
onClick={autoWeightByAccuracy}
className="text-xs text-purple-400 hover:text-purple-300 transition-colors"
>
Auto (by accuracy)
</button>
</div>
)}
</div>
{/* Weight validation */}
<div
className={`mb-3 px-3 py-2 rounded-lg flex items-center gap-2 text-sm ${
isValidConfig
? 'bg-green-500/10 text-green-400 border border-green-500/30'
: 'bg-red-500/10 text-red-400 border border-red-500/30'
}`}
>
{isValidConfig ? (
<>
<CheckCircle className="w-4 h-4" />
<span>Total: {totalWeight.toFixed(1)}% - Configuration valid</span>
</>
) : (
<>
<AlertTriangle className="w-4 h-4" />
<span>Total: {totalWeight.toFixed(1)}% - Must equal 100%</span>
</>
)}
</div>
{/* Weight sliders */}
<div className="space-y-3">
{config.weights.map((model) => (
<div key={model.modelId} className="p-3 bg-gray-900/50 rounded-lg">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<button
onClick={() => toggleLock(model.modelId)}
disabled={readOnly}
className={`p-1 rounded transition-colors ${
model.locked ? 'text-yellow-400 bg-yellow-500/20' : 'text-gray-500 hover:text-gray-300'
}`}
>
{model.locked ? <Lock className="w-3 h-3" /> : <Unlock className="w-3 h-3" />}
</button>
<span className="font-medium text-white">{model.modelName}</span>
<span className="text-xs text-gray-500">({model.accuracy}% acc)</span>
</div>
<div className="flex items-center gap-2">
<input
type="number"
min="0"
max="100"
value={model.weight.toFixed(1)}
onChange={(e) => 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"
/>
<span className="text-gray-400">%</span>
</div>
</div>
<div className="relative h-2 bg-gray-700 rounded-full overflow-hidden">
<div
className={`absolute h-full transition-all ${getWeightColor(model.weight)}`}
style={{ width: `${model.weight}%` }}
/>
</div>
{!readOnly && !model.locked && (
<input
type="range"
min="0"
max="100"
step="0.5"
value={model.weight}
onChange={(e) => 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',
}}
/>
)}
</div>
))}
</div>
</div>
{/* Info */}
<div className="flex items-start gap-2 p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
<Info className="w-4 h-4 text-blue-400 flex-shrink-0 mt-0.5" />
<div className="text-xs text-gray-300">
<p className="mb-1">
<strong className="text-blue-400">Weighted voting:</strong> Each model's prediction is multiplied by its weight.
</p>
<p className="mb-1">
<strong className="text-blue-400">Majority voting:</strong> Signal generated when majority of models agree.
</p>
<p>
<strong className="text-blue-400">Unanimous:</strong> All models must agree for a signal to be generated.
</p>
</div>
</div>
</div>
);
};
export default EnsemblePanel;

View File

@ -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<ICTAnalysisPanelProps> = ({
params,
onParamsChange,
onSave,
onReset,
isLoading = false,
readOnly = false,
}) => {
const [hasChanges, setHasChanges] = useState(false);
const [expandedSections, setExpandedSections] = useState<string[]>(['timeframe', 'orderBlocks']);
const toggleSection = (section: string) => {
setExpandedSections((prev) =>
prev.includes(section) ? prev.filter((s) => s !== section) : [...prev, section]
);
};
const updateParams = useCallback(
(updates: Partial<ICTParams>) => {
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 }) => (
<button
onClick={() => toggleSection(id)}
className="w-full flex items-center justify-between p-3 bg-gray-900/50 rounded-lg hover:bg-gray-900 transition-colors"
>
<div className="flex items-center gap-3">
<div className="p-1.5 bg-blue-500/20 text-blue-400 rounded">{icon}</div>
<span className="font-medium text-white">{title}</span>
</div>
<div className="flex items-center gap-2">
{enabled !== undefined && onToggle && (
<button
onClick={(e) => {
e.stopPropagation();
if (!readOnly) onToggle(!enabled);
}}
className={`p-1 rounded transition-colors ${
enabled ? 'text-green-400 bg-green-500/20' : 'text-gray-500 bg-gray-700'
}`}
>
{enabled ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
</button>
)}
{expandedSections.includes(id) ? (
<ChevronUp className="w-4 h-4 text-gray-400" />
) : (
<ChevronDown className="w-4 h-4 text-gray-400" />
)}
</div>
</button>
);
return (
<div className="bg-gray-800/50 rounded-xl border border-gray-700 p-4">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-cyan-500/20 text-cyan-400 rounded-lg">
<Settings2 className="w-5 h-5" />
</div>
<div>
<h3 className="font-semibold text-white">ICT Analysis Settings</h3>
<p className="text-xs text-gray-400">Configure market structure detection</p>
</div>
</div>
{!readOnly && (
<div className="flex items-center gap-2">
<button
onClick={handleReset}
disabled={isLoading || !hasChanges}
className="flex items-center gap-1 px-3 py-1.5 text-sm text-gray-400 hover:text-white transition-colors disabled:opacity-50"
>
<RotateCcw className="w-4 h-4" />
Reset
</button>
<button
onClick={handleSave}
disabled={isLoading || !hasChanges}
className="flex items-center gap-1 px-3 py-1.5 text-sm bg-cyan-500 text-white rounded-lg hover:bg-cyan-600 transition-colors disabled:opacity-50"
>
<Save className="w-4 h-4" />
Save
</button>
</div>
)}
</div>
{/* Sections */}
<div className="space-y-3">
{/* Timeframe Section */}
<div>
<SectionHeader id="timeframe" icon={<Clock className="w-4 h-4" />} title="Timeframe" />
{expandedSections.includes('timeframe') && (
<div className="mt-2 p-3 bg-gray-900/30 rounded-lg space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-2">Analysis Timeframe</label>
<div className="flex flex-wrap gap-2">
{timeframes.map((tf) => (
<button
key={tf}
onClick={() => updateParams({ timeframe: tf })}
disabled={readOnly}
className={`px-3 py-1.5 rounded text-sm transition-colors ${
params.timeframe === tf
? 'bg-cyan-500 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
{tf}
</button>
))}
</div>
</div>
<div>
<label className="block text-sm text-gray-400 mb-2">HTF Bias Reference</label>
<div className="flex flex-wrap gap-2">
{htfOptions.map((tf) => (
<button
key={tf}
onClick={() => updateParams({ htfBias: tf })}
disabled={readOnly}
className={`px-3 py-1.5 rounded text-sm transition-colors ${
params.htfBias === tf
? 'bg-purple-500 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
{tf}
</button>
))}
</div>
</div>
</div>
)}
</div>
{/* Order Blocks Section */}
<div>
<SectionHeader
id="orderBlocks"
icon={<Target className="w-4 h-4" />}
title="Order Blocks"
enabled={params.orderBlocks.enabled}
onToggle={(enabled) => updateParams({ orderBlocks: { ...params.orderBlocks, enabled } })}
/>
{expandedSections.includes('orderBlocks') && params.orderBlocks.enabled && (
<div className="mt-2 p-3 bg-gray-900/30 rounded-lg space-y-4">
<div>
<label className="flex items-center justify-between text-sm text-gray-400 mb-2">
<span>Lookback Period</span>
<span className="text-white">{params.orderBlocks.lookback} candles</span>
</label>
<input
type="range"
min="10"
max="100"
value={params.orderBlocks.lookback}
onChange={(e) =>
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"
/>
</div>
<div>
<label className="flex items-center justify-between text-sm text-gray-400 mb-2">
<span>Min Imbalance %</span>
<span className="text-white">{params.orderBlocks.minImbalance}%</span>
</label>
<input
type="range"
min="0.1"
max="5"
step="0.1"
value={params.orderBlocks.minImbalance}
onChange={(e) =>
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"
/>
</div>
<label className="flex items-center gap-2 text-sm text-gray-300">
<input
type="checkbox"
checked={params.orderBlocks.showMitigated}
onChange={(e) =>
updateParams({ orderBlocks: { ...params.orderBlocks, showMitigated: e.target.checked } })
}
disabled={readOnly}
className="rounded border-gray-600 text-cyan-500 focus:ring-cyan-500"
/>
Show mitigated order blocks
</label>
</div>
)}
</div>
{/* Fair Value Gap Section */}
<div>
<SectionHeader
id="fvg"
icon={<BarChart2 className="w-4 h-4" />}
title="Fair Value Gaps"
enabled={params.fvg.enabled}
onToggle={(enabled) => updateParams({ fvg: { ...params.fvg, enabled } })}
/>
{expandedSections.includes('fvg') && params.fvg.enabled && (
<div className="mt-2 p-3 bg-gray-900/30 rounded-lg space-y-4">
<div>
<label className="flex items-center justify-between text-sm text-gray-400 mb-2">
<span>Minimum Gap Size</span>
<span className="text-white">{params.fvg.minSize} pips</span>
</label>
<input
type="range"
min="1"
max="50"
value={params.fvg.minSize}
onChange={(e) => 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"
/>
</div>
<label className="flex items-center gap-2 text-sm text-gray-300">
<input
type="checkbox"
checked={params.fvg.showFilled}
onChange={(e) => updateParams({ fvg: { ...params.fvg, showFilled: e.target.checked } })}
disabled={readOnly}
className="rounded border-gray-600 text-cyan-500 focus:ring-cyan-500"
/>
Show filled gaps
</label>
<label className="flex items-center gap-2 text-sm text-gray-300">
<input
type="checkbox"
checked={params.fvg.filterByTrend}
onChange={(e) => updateParams({ fvg: { ...params.fvg, filterByTrend: e.target.checked } })}
disabled={readOnly}
className="rounded border-gray-600 text-cyan-500 focus:ring-cyan-500"
/>
Filter by HTF trend direction
</label>
</div>
)}
</div>
{/* Market Structure Section */}
<div>
<SectionHeader
id="marketStructure"
icon={<TrendingUp className="w-4 h-4" />}
title="Market Structure"
enabled={params.marketStructure.enabled}
onToggle={(enabled) => updateParams({ marketStructure: { ...params.marketStructure, enabled } })}
/>
{expandedSections.includes('marketStructure') && params.marketStructure.enabled && (
<div className="mt-2 p-3 bg-gray-900/30 rounded-lg space-y-4">
<div>
<label className="flex items-center justify-between text-sm text-gray-400 mb-2">
<span>Swing Lookback</span>
<span className="text-white">{params.marketStructure.swingLookback} candles</span>
</label>
<input
type="range"
min="3"
max="20"
value={params.marketStructure.swingLookback}
onChange={(e) =>
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"
/>
</div>
<label className="flex items-center gap-2 text-sm text-gray-300">
<input
type="checkbox"
checked={params.marketStructure.showBOS}
onChange={(e) =>
updateParams({ marketStructure: { ...params.marketStructure, showBOS: e.target.checked } })
}
disabled={readOnly}
className="rounded border-gray-600 text-cyan-500 focus:ring-cyan-500"
/>
Show Break of Structure (BOS)
</label>
<label className="flex items-center gap-2 text-sm text-gray-300">
<input
type="checkbox"
checked={params.marketStructure.showCHOCH}
onChange={(e) =>
updateParams({ marketStructure: { ...params.marketStructure, showCHOCH: e.target.checked } })
}
disabled={readOnly}
className="rounded border-gray-600 text-cyan-500 focus:ring-cyan-500"
/>
Show Change of Character (CHOCH)
</label>
</div>
)}
</div>
{/* Liquidity Section */}
<div>
<SectionHeader
id="liquidity"
icon={<Layers className="w-4 h-4" />}
title="Liquidity"
enabled={params.liquidity.enabled}
onToggle={(enabled) => updateParams({ liquidity: { ...params.liquidity, enabled } })}
/>
{expandedSections.includes('liquidity') && params.liquidity.enabled && (
<div className="mt-2 p-3 bg-gray-900/30 rounded-lg space-y-3">
<label className="flex items-center gap-2 text-sm text-gray-300">
<input
type="checkbox"
checked={params.liquidity.showBSL}
onChange={(e) => updateParams({ liquidity: { ...params.liquidity, showBSL: e.target.checked } })}
disabled={readOnly}
className="rounded border-gray-600 text-cyan-500 focus:ring-cyan-500"
/>
Show Buy Side Liquidity (BSL)
</label>
<label className="flex items-center gap-2 text-sm text-gray-300">
<input
type="checkbox"
checked={params.liquidity.showSSL}
onChange={(e) => updateParams({ liquidity: { ...params.liquidity, showSSL: e.target.checked } })}
disabled={readOnly}
className="rounded border-gray-600 text-cyan-500 focus:ring-cyan-500"
/>
Show Sell Side Liquidity (SSL)
</label>
<label className="flex items-center gap-2 text-sm text-gray-300">
<input
type="checkbox"
checked={params.liquidity.equalHighsLows}
onChange={(e) => updateParams({ liquidity: { ...params.liquidity, equalHighsLows: e.target.checked } })}
disabled={readOnly}
className="rounded border-gray-600 text-cyan-500 focus:ring-cyan-500"
/>
Highlight Equal Highs/Lows
</label>
</div>
)}
</div>
</div>
{/* Info */}
<div className="mt-4 flex items-start gap-2 p-3 bg-cyan-500/10 border border-cyan-500/30 rounded-lg">
<Info className="w-4 h-4 text-cyan-400 flex-shrink-0 mt-0.5" />
<div className="text-xs text-gray-300">
<p>
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.
</p>
</div>
</div>
</div>
);
};
export default ICTAnalysisPanel;

View File

@ -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<ModelSelectorProps> = ({
models,
selectedModelId,
onModelChange,
variant = 'dropdown',
showMetrics = true,
disabled = false,
loading = false,
}) => {
const [isOpen, setIsOpen] = React.useState(false);
const dropdownRef = React.useRef<HTMLDivElement>(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 <Network className="w-4 h-4" />;
case 'xgboost':
return <TrendingUp className="w-4 h-4" />;
case 'random_forest':
return <Layers className="w-4 h-4" />;
case 'svm':
return <LineChart className="w-4 h-4" />;
case 'ensemble':
return <Sparkles className="w-4 h-4" />;
case 'transformer':
return <Cpu className="w-4 h-4" />;
default:
return <Brain className="w-4 h-4" />;
}
};
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 (
<div ref={dropdownRef} className="relative">
<button
onClick={() => !disabled && !loading && setIsOpen(!isOpen)}
disabled={disabled || loading}
className={`w-full flex items-center justify-between gap-3 px-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg transition-colors ${
disabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-800 hover:border-gray-600'
}`}
>
<div className="flex items-center gap-3">
{loading ? (
<div className="w-5 h-5 border-2 border-blue-400 border-t-transparent rounded-full animate-spin" />
) : selectedModel ? (
<>
<div className="p-1.5 bg-blue-500/20 text-blue-400 rounded">
{getModelIcon(selectedModel.type)}
</div>
<div className="text-left">
<div className="font-medium text-white">{selectedModel.name}</div>
{showMetrics && (
<div className="text-xs text-gray-400">
Accuracy: <span className={getAccuracyColor(selectedModel.accuracy)}>{selectedModel.accuracy}%</span>
</div>
)}
</div>
</>
) : (
<span className="text-gray-400">Select a model</span>
)}
</div>
<ChevronDown className={`w-5 h-5 text-gray-400 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
</button>
{isOpen && (
<div className="absolute z-50 w-full mt-2 bg-gray-800 border border-gray-700 rounded-lg shadow-xl overflow-hidden">
<div className="max-h-80 overflow-y-auto">
{models.map((model) => (
<button
key={model.id}
onClick={() => {
onModelChange(model.id);
setIsOpen(false);
}}
disabled={model.status === 'deprecated'}
className={`w-full flex items-center gap-3 px-4 py-3 transition-colors ${
model.id === selectedModelId
? 'bg-blue-500/20 border-l-2 border-blue-500'
: 'hover:bg-gray-700/50'
} ${model.status === 'deprecated' ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<div className={`p-1.5 rounded ${model.id === selectedModelId ? 'bg-blue-500/30 text-blue-400' : 'bg-gray-700 text-gray-400'}`}>
{getModelIcon(model.type)}
</div>
<div className="flex-1 text-left">
<div className="flex items-center gap-2">
<span className="font-medium text-white">{model.name}</span>
<span className={`text-xs px-1.5 py-0.5 rounded ${getStatusColor(model.status)}`}>
{model.status}
</span>
</div>
{showMetrics && (
<div className="text-xs text-gray-400 mt-0.5">
Accuracy: <span className={getAccuracyColor(model.accuracy)}>{model.accuracy}%</span>
{model.description && <span className="ml-2">- {model.description}</span>}
</div>
)}
</div>
{model.id === selectedModelId && <Check className="w-5 h-5 text-blue-400" />}
</button>
))}
</div>
</div>
)}
</div>
);
}
// Tabs variant
if (variant === 'tabs') {
return (
<div className="flex flex-wrap gap-2 p-1 bg-gray-800/50 rounded-lg">
{models.map((model) => (
<button
key={model.id}
onClick={() => onModelChange(model.id)}
disabled={disabled || model.status === 'deprecated'}
className={`flex items-center gap-2 px-4 py-2 rounded-md transition-all ${
model.id === selectedModelId
? 'bg-blue-500 text-white shadow-lg'
: 'text-gray-400 hover:text-white hover:bg-gray-700/50'
} ${model.status === 'deprecated' ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{getModelIcon(model.type)}
<span className="font-medium">{model.name}</span>
{showMetrics && (
<span className={`text-xs ${model.id === selectedModelId ? 'text-blue-100' : getAccuracyColor(model.accuracy)}`}>
{model.accuracy}%
</span>
)}
</button>
))}
</div>
);
}
// Cards variant
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{models.map((model) => (
<button
key={model.id}
onClick={() => onModelChange(model.id)}
disabled={disabled || model.status === 'deprecated'}
className={`relative p-4 rounded-xl border transition-all text-left ${
model.id === selectedModelId
? 'bg-blue-500/20 border-blue-500 shadow-lg shadow-blue-500/20'
: 'bg-gray-800/50 border-gray-700 hover:border-gray-600 hover:bg-gray-800'
} ${model.status === 'deprecated' ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{model.id === selectedModelId && (
<div className="absolute top-2 right-2">
<Check className="w-5 h-5 text-blue-400" />
</div>
)}
<div className="flex items-center gap-3 mb-3">
<div className={`p-2 rounded-lg ${model.id === selectedModelId ? 'bg-blue-500/30 text-blue-400' : 'bg-gray-700 text-gray-400'}`}>
{getModelIcon(model.type)}
</div>
<div>
<div className="font-semibold text-white">{model.name}</div>
<div className={`text-xs px-1.5 py-0.5 rounded inline-block ${getStatusColor(model.status)}`}>
{model.status}
</div>
</div>
</div>
{model.description && (
<p className="text-sm text-gray-400 mb-3 line-clamp-2">{model.description}</p>
)}
{showMetrics && (
<div className="flex items-center justify-between pt-3 border-t border-gray-700">
<div className="text-sm text-gray-400">Accuracy</div>
<div className={`text-lg font-bold ${getAccuracyColor(model.accuracy)}`}>
{model.accuracy}%
</div>
</div>
)}
{model.status === 'training' && (
<div className="mt-3 flex items-center gap-2 text-xs text-yellow-400">
<AlertCircle className="w-3 h-3" />
<span>Model is currently training</span>
</div>
)}
</button>
))}
</div>
);
};
export default ModelSelector;

View File

@ -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';