- 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>
726 lines
27 KiB
TypeScript
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>
|
|
);
|
|
}
|