[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:
parent
295bd5e31e
commit
ad7171da2c
@ -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,6 +453,66 @@ export default function MLDashboard() {
|
||||
|
||||
{/* 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
|
||||
@ -379,9 +528,17 @@ export default function MLDashboard() {
|
||||
{/* 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">
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{scanResults.map((result, idx) => (
|
||||
<div key={idx} className="p-3 bg-gray-800 rounded-lg">
|
||||
<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 ${
|
||||
@ -404,11 +561,12 @@ export default function MLDashboard() {
|
||||
{result.signal.net_score >= 0 ? '+' : ''}{result.signal.net_score.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab Content - Original ML Signals */}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user