trading-platform-frontend-v2/src/modules/ml/pages/MLDashboard.tsx
Adrian Flores Cortes ad7171da2c [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>
2026-02-03 23:54:38 -06:00

726 lines
27 KiB
TypeScript

/**
* MLDashboard Page
* Main dashboard for ML predictions and signals
* Enhanced with ICT/SMC Analysis and Ensemble Signals
*/
import React, { useEffect, useState, useCallback } from 'react';
import {
SparklesIcon,
FunnelIcon,
ArrowPathIcon,
ExclamationTriangleIcon,
ChartBarIcon,
BeakerIcon,
CpuChipIcon,
} from '@heroicons/react/24/solid';
import {
getActiveSignals,
getAMDPhase,
getICTAnalysis,
getEnsembleSignal,
scanSymbols,
type MLSignal,
type AMDPhase,
type ICTAnalysis,
type EnsembleSignal,
type ScanResult,
} from '../../../services/mlService';
import { AMDPhaseIndicator } from '../components/AMDPhaseIndicator';
import { PredictionCard } from '../components/PredictionCard';
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 = {
overall_accuracy: 68.5,
win_rate: 62.3,
total_signals: 156,
successful_signals: 97,
failed_signals: 59,
avg_risk_reward: 2.3,
avg_confidence: 72,
best_performing_phase: 'accumulation',
sharpe_ratio: 1.8,
profit_factor: 1.7,
};
// 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());
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
// ICT/SMC and Ensemble data
const [ictAnalysis, setIctAnalysis] = useState<ICTAnalysis | null>(null);
const [ensembleSignal, setEnsembleSignal] = useState<EnsembleSignal | null>(null);
const [scanResults, setScanResults] = useState<ScanResult[]>([]);
const [activeTab, setActiveTab] = useState<'signals' | 'ict' | 'ensemble'>('signals');
// Filters
const [selectedSymbol, setSelectedSymbol] = useState<string>('EURUSD');
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);
setError(null);
try {
// Fetch active signals
const activeSignals = await getActiveSignals();
setSignals(activeSignals);
// Fetch AMD phases for each unique symbol
const uniqueSymbols = [...new Set(activeSignals.map(s => s.symbol))];
const amdPhasesMap = new Map<string, AMDPhase>();
await Promise.all(
uniqueSymbols.map(async (symbol) => {
try {
const phase = await getAMDPhase(symbol);
if (phase) {
amdPhasesMap.set(symbol, phase);
}
} catch (err) {
console.error(`Failed to fetch AMD phase for ${symbol}:`, err);
}
})
);
setAmdPhases(amdPhasesMap);
setLastUpdate(new Date());
} catch (err) {
setError('Failed to fetch ML data');
console.error('ML data fetch error:', err);
} finally {
setLoading(false);
}
}, []);
// Fetch ICT and Ensemble data for selected symbol
const fetchAdvancedAnalysis = useCallback(async () => {
try {
const [ict, ensemble, scan] = await Promise.all([
getICTAnalysis(selectedSymbol, selectedTimeframe),
getEnsembleSignal(selectedSymbol, selectedTimeframe),
scanSymbols(SYMBOLS, 0.5),
]);
setIctAnalysis(ict);
setEnsembleSignal(ensemble);
setScanResults(scan);
} catch (err) {
console.error('Failed to fetch advanced analysis:', err);
}
}, [selectedSymbol, selectedTimeframe]);
// Handle symbol/timeframe change
useEffect(() => {
fetchAdvancedAnalysis();
}, [selectedSymbol, selectedTimeframe, fetchAdvancedAnalysis]);
// Initial fetch
useEffect(() => {
fetchMLData();
// Auto-refresh every 60 seconds
const interval = setInterval(fetchMLData, 60000);
return () => clearInterval(interval);
}, [fetchMLData]);
// Filter signals
const filteredSignals = signals.filter((signal) => {
if (selectedSymbol !== 'all' && signal.symbol !== selectedSymbol) return false;
if (showOnlyActive) {
const isValid = new Date(signal.valid_until) > new Date();
if (!isValid) return false;
}
return true;
});
// Get unique symbols for filter
const uniqueSymbols = [...new Set(signals.map(s => s.symbol))];
// Get primary AMD phase (most common or highest confidence)
const getPrimaryAMDPhase = (): AMDPhase | null => {
const phases = Array.from(amdPhases.values());
if (phases.length === 0) return null;
// Return the phase with highest confidence
return phases.reduce((prev, current) =>
(current.confidence > prev.confidence) ? current : prev
);
};
const primaryPhase = getPrimaryAMDPhase();
// Handle trade execution
const handleExecuteTrade = (signal: MLSignal) => {
// Navigate to trading page with pre-filled signal
window.location.href = `/trading?symbol=${signal.symbol}&signal=${signal.signal_id}`;
};
// Handle advanced trade execution (ICT/Ensemble)
const handleAdvancedTrade = (direction: 'buy' | 'sell', data: unknown) => {
console.log('Execute trade:', direction, data);
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 */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white flex items-center gap-2">
<SparklesIcon className="w-7 h-7 text-blue-400" />
ML Predictions Dashboard
</h1>
<p className="text-gray-400 mt-1">
AI-powered trading signals and market analysis
</p>
</div>
<div className="flex items-center gap-3">
{lastUpdate && (
<span className="text-sm text-gray-400">
Updated {lastUpdate.toLocaleTimeString()}
</span>
)}
<button
onClick={fetchMLData}
disabled={loading}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-700 text-white rounded-lg transition-colors"
>
<ArrowPathIcon className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
Refresh
</button>
</div>
</div>
{/* Error Message */}
{error && (
<div className="flex items-center gap-3 p-4 bg-red-900/20 border border-red-800 rounded-lg">
<ExclamationTriangleIcon className="w-6 h-6 text-red-400" />
<div className="flex-1">
<p className="text-red-400 font-medium">{error}</p>
<p className="text-sm text-red-300 mt-1">Please try again or contact support</p>
</div>
</div>
)}
{/* Primary AMD Phase Indicator */}
{primaryPhase && (
<div className="lg:col-span-2">
<AMDPhaseIndicator
phase={primaryPhase.phase}
confidence={primaryPhase.confidence}
phaseDuration={primaryPhase.phase_duration_bars}
nextPhaseProbability={primaryPhase.next_phase_probability}
keyLevels={primaryPhase.key_levels}
/>
</div>
)}
{/* Symbol and Timeframe Selector */}
<div className="card p-4">
<div className="flex items-center justify-between flex-wrap gap-4">
{/* Symbol Selector */}
<div className="flex items-center gap-3">
<CpuChipIcon className="w-5 h-5 text-purple-400" />
<div className="flex gap-1">
{SYMBOLS.map((sym) => (
<button
key={sym}
onClick={() => setSelectedSymbol(sym)}
className={`px-3 py-1.5 text-sm rounded-lg transition-colors ${
selectedSymbol === sym
? 'bg-purple-600 text-white'
: 'bg-gray-800 text-gray-300 hover:bg-gray-700'
}`}
>
{sym}
</button>
))}
</div>
</div>
{/* Timeframe Selector */}
<div className="flex items-center gap-2">
{TIMEFRAMES.map((tf) => (
<button
key={tf}
onClick={() => setSelectedTimeframe(tf)}
className={`px-3 py-1.5 text-sm rounded-lg transition-colors ${
selectedTimeframe === tf
? 'bg-blue-600 text-white'
: 'bg-gray-800 text-gray-300 hover:bg-gray-700'
}`}
>
{tf}
</button>
))}
</div>
</div>
</div>
{/* Analysis Tabs */}
<div className="flex gap-2 border-b border-gray-700 pb-2">
<button
onClick={() => setActiveTab('signals')}
className={`flex items-center gap-2 px-4 py-2 rounded-t-lg transition-colors ${
activeTab === 'signals'
? 'bg-gray-800 text-blue-400 border-b-2 border-blue-400'
: 'text-gray-400 hover:text-white'
}`}
>
<SparklesIcon className="w-4 h-4" />
ML Signals
</button>
<button
onClick={() => setActiveTab('ict')}
className={`flex items-center gap-2 px-4 py-2 rounded-t-lg transition-colors ${
activeTab === 'ict'
? 'bg-gray-800 text-purple-400 border-b-2 border-purple-400'
: 'text-gray-400 hover:text-white'
}`}
>
<ChartBarIcon className="w-4 h-4" />
ICT/SMC Analysis
</button>
<button
onClick={() => setActiveTab('ensemble')}
className={`flex items-center gap-2 px-4 py-2 rounded-t-lg transition-colors ${
activeTab === 'ensemble'
? 'bg-gray-800 text-green-400 border-b-2 border-green-400'
: 'text-gray-400 hover:text-white'
}`}
>
<BeakerIcon className="w-4 h-4" />
Ensemble Signal
</button>
</div>
{/* Tab Content - ICT Analysis */}
{activeTab === 'ict' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{ictAnalysis ? (
<ICTAnalysisCard
analysis={ictAnalysis}
onExecuteTrade={handleAdvancedTrade}
/>
) : (
<div className="card p-8 flex items-center justify-center">
<div className="text-gray-500">Loading ICT analysis...</div>
</div>
)}
{/* Scanner Results */}
<div className="card p-5">
<h3 className="text-lg font-bold text-white mb-4 flex items-center gap-2">
<FunnelIcon className="w-5 h-5 text-purple-400" />
Market Scanner ({scanResults.length} opportunities)
</h3>
{scanResults.length > 0 ? (
<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 flex items-center justify-between p-3 rounded-lg transition-colors ${
selectedSymbol === result.symbol
? 'bg-purple-900/30 border border-purple-500'
: 'bg-gray-800 hover:bg-gray-700'
}`}
>
<span className="font-semibold text-white">{result.symbol}</span>
<div className="flex items-center gap-3">
<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>
<span className="text-xs text-gray-400">
{Math.round(result.signal.confidence * 100)}%
</span>
</div>
</button>
))}
</div>
) : (
<div className="text-center py-4 text-gray-500">No opportunities found</div>
)}
</div>
</div>
)}
{/* Tab Content - Ensemble Signal */}
{activeTab === 'ensemble' && (
<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>
{/* 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'
}`}>
{ensembleSignal.action}
</span>
<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>
)}
{/* Tab Content - Original ML Signals */}
{activeTab === 'signals' && (
<>
{/* Filters and Stats Bar */}
<div className="card p-4">
<div className="flex items-center justify-between flex-wrap gap-4">
{/* Filters */}
<div className="flex items-center gap-3">
<FunnelIcon className="w-5 h-5 text-gray-400" />
{/* Active Only Toggle */}
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={showOnlyActive}
onChange={(e) => setShowOnlyActive(e.target.checked)}
className="w-4 h-4 text-blue-600 bg-gray-800 border-gray-700 rounded focus:ring-blue-500"
/>
<span className="text-sm text-gray-300">Active Only</span>
</label>
</div>
{/* Stats */}
<div className="flex items-center gap-4 text-sm">
<div className="flex items-center gap-2">
<ChartBarIcon className="w-4 h-4 text-blue-400" />
<span className="text-gray-400">Total Signals:</span>
<span className="text-white font-bold">{signals.length}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-gray-400">Active:</span>
<span className="text-green-400 font-bold">{filteredSignals.length}</span>
</div>
</div>
</div>
</div>
{/* Main Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Active Signals */}
<div className="lg:col-span-2 space-y-6">
<div>
<h2 className="text-lg font-bold text-white mb-4">Active Predictions</h2>
{loading ? (
<div className="card p-8">
<div className="flex items-center justify-center">
<ArrowPathIcon className="w-8 h-8 text-blue-400 animate-spin" />
<span className="ml-3 text-gray-400">Loading signals...</span>
</div>
</div>
) : filteredSignals.length > 0 ? (
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
{filteredSignals.map((signal) => (
<PredictionCard
key={signal.signal_id}
signal={signal}
onExecuteTrade={handleExecuteTrade}
/>
))}
</div>
) : (
<div className="card p-8 text-center">
<SparklesIcon className="w-12 h-12 mx-auto mb-3 text-gray-600" />
<p className="text-gray-400">No active signals found</p>
<p className="text-sm text-gray-500 mt-1">
{selectedSymbol !== 'all'
? `No signals for ${selectedSymbol}`
: 'Try adjusting your filters or refresh to load new signals'}
</p>
</div>
)}
</div>
{/* Signals Timeline */}
<SignalsTimeline
signals={signals}
maxItems={8}
/>
</div>
{/* Right Column - Metrics and Info */}
<div className="space-y-6">
{/* Accuracy Metrics */}
<AccuracyMetrics
metrics={mockMetrics}
period="Last 30 days"
/>
{/* AMD Phases by Symbol */}
{amdPhases.size > 0 && (
<div className="card p-5">
<h3 className="text-lg font-bold text-white mb-4">AMD Phases by Symbol</h3>
<div className="space-y-3">
{Array.from(amdPhases.entries()).map(([symbol, phase]) => (
<div key={symbol} className="p-3 bg-gray-800 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="font-bold text-white">{symbol}</span>
<span className="text-xs text-gray-400">
{Math.round(phase.confidence * 100)}% confidence
</span>
</div>
<AMDPhaseIndicator
phase={phase.phase}
confidence={phase.confidence}
compact={true}
/>
</div>
))}
</div>
</div>
)}
{/* Quick Stats Card */}
<div className="card p-5">
<h3 className="text-lg font-bold text-white mb-4">Quick Stats</h3>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 bg-gray-800 rounded">
<span className="text-sm text-gray-400">Avg Confidence</span>
<span className="text-white font-bold">
{signals.length > 0
? Math.round(
signals.reduce((sum, s) => sum + s.confidence_score, 0) /
signals.length *
100
)
: 0}
%
</span>
</div>
<div className="flex items-center justify-between p-3 bg-gray-800 rounded">
<span className="text-sm text-gray-400">Avg Risk:Reward</span>
<span className="text-white font-bold">
{signals.length > 0
? (
signals.reduce((sum, s) => sum + s.risk_reward_ratio, 0) /
signals.length
).toFixed(1)
: '0.0'}
</span>
</div>
<div className="flex items-center justify-between p-3 bg-gray-800 rounded">
<span className="text-sm text-gray-400">Tracked Symbols</span>
<span className="text-white font-bold">{uniqueSymbols.length}</span>
</div>
</div>
</div>
</div>
</div>
</>
)}
</div>
);
}