[SUBTASK-004] feat(ml): Integrate EnsemblePanel UI and add more symbols

- Integrate EnsemblePanel component in MLDashboard ensemble tab
- Add configuration UI for ensemble model weights and voting method
- Implement weighted ensemble prediction calculation
- Expand symbol list: EURUSD, GBPUSD, USDJPY, XAUUSD, BTCUSD, ETHUSD, USDCHF, AUDUSD
- Add ability to save/reset ensemble configuration to localStorage
- Show agreement count and signal validity in prediction summary

ST-004.1: Ensemble Models UI [8 SP] - COMPLETED
ST-004.2: More symbols support [5 SP] - COMPLETED
ST-004.3: WebSocket real-time - ALREADY IMPLEMENTED (mlSignalsWS exists)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-02-03 23:54:38 -06:00
parent 295bd5e31e
commit ad7171da2c

View File

@ -32,6 +32,7 @@ import { SignalsTimeline } from '../components/SignalsTimeline';
import { AccuracyMetrics } from '../components/AccuracyMetrics';
import { ICTAnalysisCard } from '../components/ICTAnalysisCard';
import { EnsembleSignalCard } from '../components/EnsembleSignalCard';
import EnsemblePanel, { type EnsembleConfig } from '../components/EnsemblePanel';
// Mock accuracy metrics (replace with API call)
const mockMetrics = {
@ -47,10 +48,33 @@ const mockMetrics = {
profit_factor: 1.7,
};
// Available symbols and timeframes
const SYMBOLS = ['EURUSD', 'GBPUSD', 'USDJPY', 'XAUUSD', 'BTCUSD', 'ETHUSD'];
// Available symbols and timeframes - Configurable list
// Includes Forex majors, commodities, and crypto
const SYMBOLS = [
'EURUSD', // EUR/USD - Forex major
'GBPUSD', // GBP/USD - Forex major
'USDJPY', // USD/JPY - Forex major
'XAUUSD', // XAU/USD - Gold
'BTCUSD', // BTC/USD - Bitcoin
'ETHUSD', // ETH/USD - Ethereum
'USDCHF', // USD/CHF - Forex major
'AUDUSD', // AUD/USD - Forex major
];
const TIMEFRAMES = ['15M', '30M', '1H', '4H', '1D'];
// Default ensemble configuration for model weights
const DEFAULT_ENSEMBLE_CONFIG: EnsembleConfig = {
votingMethod: 'weighted',
minimumAgreement: 2,
confidenceThreshold: 60,
weights: [
{ modelId: 'amd', modelName: 'AMD Phase Detector', weight: 25, accuracy: 72 },
{ modelId: 'ict', modelName: 'ICT/SMC Analyzer', weight: 30, accuracy: 75 },
{ modelId: 'range', modelName: 'Range Predictor', weight: 25, accuracy: 68 },
{ modelId: 'tpsl', modelName: 'TP/SL Optimizer', weight: 20, accuracy: 70 },
],
};
export default function MLDashboard() {
const [signals, setSignals] = useState<MLSignal[]>([]);
const [amdPhases, setAmdPhases] = useState<Map<string, AMDPhase>>(new Map());
@ -69,6 +93,10 @@ export default function MLDashboard() {
const [selectedTimeframe, setSelectedTimeframe] = useState<string>('1H');
const [showOnlyActive, setShowOnlyActive] = useState(true);
// Ensemble configuration state
const [ensembleConfig, setEnsembleConfig] = useState<EnsembleConfig>(DEFAULT_ENSEMBLE_CONFIG);
const [showEnsembleConfig, setShowEnsembleConfig] = useState(false);
// Fetch all ML data
const fetchMLData = useCallback(async () => {
setLoading(true);
@ -174,6 +202,67 @@ export default function MLDashboard() {
alert(`Would execute ${direction.toUpperCase()} trade for ${selectedSymbol}`);
};
// Handle ensemble config changes
const handleEnsembleConfigChange = (newConfig: EnsembleConfig) => {
setEnsembleConfig(newConfig);
};
const handleSaveEnsembleConfig = () => {
// Save ensemble config to localStorage for persistence
localStorage.setItem('ensembleConfig', JSON.stringify(ensembleConfig));
setShowEnsembleConfig(false);
// Refetch ensemble signal with new weights
fetchAdvancedAnalysis();
};
const handleResetEnsembleConfig = () => {
setEnsembleConfig(DEFAULT_ENSEMBLE_CONFIG);
};
// Calculate combined ensemble prediction based on current weights
const getWeightedEnsemblePrediction = useCallback(() => {
if (!ensembleSignal) return null;
const strategies = ensembleSignal.strategy_signals;
const weights = ensembleConfig.weights;
let totalWeightedScore = 0;
let totalWeight = 0;
let agreementCount = 0;
const action = ensembleSignal.action;
// Calculate weighted score based on user-configured weights
weights.forEach((w) => {
const strategyKey = w.modelId as keyof typeof strategies;
const strategy = strategies[strategyKey];
if (strategy) {
const normalizedWeight = w.weight / 100;
totalWeightedScore += strategy.score * normalizedWeight;
totalWeight += normalizedWeight;
// Count agreement
if (
(action === 'BUY' && strategy.action === 'BUY') ||
(action === 'SELL' && strategy.action === 'SELL')
) {
agreementCount++;
}
}
});
const adjustedConfidence = totalWeight > 0 ? totalWeightedScore / totalWeight : 0;
const meetsAgreement = agreementCount >= ensembleConfig.minimumAgreement;
const meetsConfidence = Math.abs(adjustedConfidence) * 100 >= ensembleConfig.confidenceThreshold;
return {
adjustedScore: adjustedConfidence,
agreementCount,
meetsAgreement,
meetsConfidence,
isValidSignal: meetsAgreement && meetsConfidence,
};
}, [ensembleSignal, ensembleConfig]);
return (
<div className="space-y-6">
{/* Header */}
@ -364,48 +453,117 @@ export default function MLDashboard() {
{/* Tab Content - Ensemble Signal */}
{activeTab === 'ensemble' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{ensembleSignal ? (
<EnsembleSignalCard
signal={ensembleSignal}
onExecuteTrade={handleAdvancedTrade}
/>
) : (
<div className="card p-8 flex items-center justify-center">
<div className="text-gray-500">Loading ensemble signal...</div>
</div>
)}
<div className="space-y-6">
{/* Ensemble Controls */}
<div className="flex items-center justify-between">
<h2 className="text-lg font-bold text-white">Ensemble Analysis</h2>
<button
onClick={() => setShowEnsembleConfig(!showEnsembleConfig)}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors"
>
<BeakerIcon className="w-4 h-4" />
{showEnsembleConfig ? 'Hide Configuration' : 'Configure Models'}
</button>
</div>
{/* Quick comparison of all symbols */}
<div className="card p-5">
<h3 className="text-lg font-bold text-white mb-4">All Symbols Overview</h3>
<div className="space-y-2">
{scanResults.map((result, idx) => (
<div key={idx} className="p-3 bg-gray-800 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="font-semibold text-white">{result.symbol}</span>
<span className={`text-sm font-bold ${
result.signal.action === 'BUY' ? 'text-green-400' :
result.signal.action === 'SELL' ? 'text-red-400' : 'text-gray-400'
{/* Weighted Prediction Summary */}
{ensembleSignal && (
<div className="card p-4 bg-gradient-to-r from-purple-900/30 to-blue-900/30 border border-purple-500/30">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm text-gray-400">Weighted Ensemble Prediction</h3>
<div className="flex items-center gap-4 mt-1">
<span className={`text-2xl font-bold ${
ensembleSignal.action === 'BUY' ? 'text-green-400' :
ensembleSignal.action === 'SELL' ? 'text-red-400' : 'text-gray-400'
}`}>
{result.signal.action}
{ensembleSignal.action}
</span>
</div>
<div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-gray-700 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${
result.signal.net_score > 0 ? 'bg-green-500' : 'bg-red-500'
}`}
style={{ width: `${Math.abs(result.signal.net_score) * 50 + 50}%` }}
/>
</div>
<span className="text-xs text-gray-400 w-12 text-right">
{result.signal.net_score >= 0 ? '+' : ''}{result.signal.net_score.toFixed(2)}
<span className="text-lg text-white">
{Math.round(ensembleSignal.confidence * 100)}% confidence
</span>
</div>
</div>
))}
{(() => {
const prediction = getWeightedEnsemblePrediction();
if (!prediction) return null;
return (
<div className="text-right">
<div className={`text-sm ${prediction.isValidSignal ? 'text-green-400' : 'text-yellow-400'}`}>
{prediction.isValidSignal ? 'Signal Valid' : 'Signal Weak'}
</div>
<div className="text-xs text-gray-400">
{prediction.agreementCount}/{ensembleConfig.weights.length} models agree
</div>
</div>
);
})()}
</div>
</div>
)}
{/* Ensemble Configuration Panel */}
{showEnsembleConfig && (
<EnsemblePanel
config={ensembleConfig}
onConfigChange={handleEnsembleConfigChange}
onSave={handleSaveEnsembleConfig}
onReset={handleResetEnsembleConfig}
isLoading={false}
/>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{ensembleSignal ? (
<EnsembleSignalCard
signal={ensembleSignal}
onExecuteTrade={handleAdvancedTrade}
/>
) : (
<div className="card p-8 flex items-center justify-center">
<div className="text-gray-500">Loading ensemble signal...</div>
</div>
)}
{/* Quick comparison of all symbols */}
<div className="card p-5">
<h3 className="text-lg font-bold text-white mb-4">All Symbols Overview</h3>
<div className="space-y-2 max-h-96 overflow-y-auto">
{scanResults.map((result, idx) => (
<button
key={idx}
onClick={() => setSelectedSymbol(result.symbol)}
className={`w-full p-3 rounded-lg transition-colors text-left ${
selectedSymbol === result.symbol
? 'bg-purple-900/30 border border-purple-500'
: 'bg-gray-800 hover:bg-gray-700'
}`}
>
<div className="flex items-center justify-between mb-2">
<span className="font-semibold text-white">{result.symbol}</span>
<span className={`text-sm font-bold ${
result.signal.action === 'BUY' ? 'text-green-400' :
result.signal.action === 'SELL' ? 'text-red-400' : 'text-gray-400'
}`}>
{result.signal.action}
</span>
</div>
<div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-gray-700 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${
result.signal.net_score > 0 ? 'bg-green-500' : 'bg-red-500'
}`}
style={{ width: `${Math.abs(result.signal.net_score) * 50 + 50}%` }}
/>
</div>
<span className="text-xs text-gray-400 w-12 text-right">
{result.signal.net_score >= 0 ? '+' : ''}{result.signal.net_score.toFixed(2)}
</span>
</div>
</button>
))}
</div>
</div>
</div>
</div>