diff --git a/src/modules/ml/components/BacktestResultsVisualization.tsx b/src/modules/ml/components/BacktestResultsVisualization.tsx new file mode 100644 index 0000000..58a07bc --- /dev/null +++ b/src/modules/ml/components/BacktestResultsVisualization.tsx @@ -0,0 +1,432 @@ +/** + * BacktestResultsVisualization Component + * Comprehensive backtest results viewer + * OQI-006: Senales ML + */ + +import React, { useState, useMemo } from 'react'; +import { + LineChart, + TrendingUp, + TrendingDown, + Target, + BarChart3, + Activity, + AlertTriangle, + DollarSign, + Percent, + Calendar, + Clock, + ChevronDown, + ChevronUp, + Download, + Zap, + Shield, + Award, +} from 'lucide-react'; + +export interface BacktestTrade { + id: string; + symbol: string; + direction: 'BUY' | 'SELL'; + entryPrice: number; + exitPrice: number; + entryTime: string; + exitTime: string; + profit: number; + profitPercent: number; + duration: number; // minutes +} + +export interface BacktestResult { + strategyName: string; + period: { start: string; end: string }; + initialCapital: number; + finalCapital: number; + totalReturn: number; + totalReturnPercent: number; + totalTrades: number; + winningTrades: number; + losingTrades: number; + winRate: number; + profitFactor: number; + sharpeRatio: number; + sortinoRatio?: number; + maxDrawdown: number; + maxDrawdownPercent: number; + avgWin: number; + avgLoss: number; + avgRR: number; + expectancy: number; + avgTradeDuration: number; + largestWin: number; + largestLoss: number; + longestWinStreak: number; + longestLoseStreak: number; + trades: BacktestTrade[]; + equityCurve?: { date: string; equity: number; drawdown: number }[]; + monthlyReturns?: Record; +} + +interface BacktestResultsVisualizationProps { + result: BacktestResult; + onExport?: (format: 'csv' | 'pdf') => void; + onTradeSelect?: (trade: BacktestTrade) => void; + compact?: boolean; +} + +const BacktestResultsVisualization: React.FC = ({ + result, + onExport, + onTradeSelect, + compact = false, +}) => { + const [activeTab, setActiveTab] = useState<'overview' | 'trades' | 'monthly'>('overview'); + const [showAllTrades, setShowAllTrades] = useState(false); + const [expandedTradeId, setExpandedTradeId] = useState(null); + + const isProfitable = result.totalReturn >= 0; + const isGoodPerformance = result.winRate >= 50 && result.profitFactor >= 1.5; + + const getMetricColor = (value: number, thresholds: { good: number; bad: number }, inverse = false) => { + if (inverse) { + if (value <= thresholds.good) return 'text-green-400'; + if (value >= thresholds.bad) return 'text-red-400'; + return 'text-yellow-400'; + } + if (value >= thresholds.good) return 'text-green-400'; + if (value <= thresholds.bad) return 'text-red-400'; + return 'text-yellow-400'; + }; + + const formatCurrency = (value: number) => { + return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value); + }; + + const formatPercent = (value: number) => { + return `${value >= 0 ? '+' : ''}${value.toFixed(2)}%`; + }; + + const formatDuration = (minutes: number) => { + if (minutes < 60) return `${minutes}m`; + if (minutes < 1440) return `${Math.floor(minutes / 60)}h ${minutes % 60}m`; + return `${Math.floor(minutes / 1440)}d`; + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + }; + + const displayedTrades = useMemo(() => { + return showAllTrades ? result.trades : result.trades.slice(0, 10); + }, [result.trades, showAllTrades]); + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

{result.strategyName}

+

+ {formatDate(result.period.start)} - {formatDate(result.period.end)} +

+
+
+ + {onExport && ( +
+ + +
+ )} +
+ + {/* Main Result Card */} +
+
+
+
Total Return
+
+ {formatCurrency(result.totalReturn)} +
+
+ {formatPercent(result.totalReturnPercent)} +
+
+
+
+ {isGoodPerformance ? ( + + ) : ( + + )} + + {isGoodPerformance ? 'Strong Performance' : 'Needs Optimization'} + +
+
+ {formatCurrency(result.initialCapital)} → {formatCurrency(result.finalCapital)} +
+
+
+
+ + {/* Tabs */} +
+ {(['overview', 'trades', 'monthly'] as const).map((tab) => ( + + ))} +
+ + {/* Overview Tab */} + {activeTab === 'overview' && ( +
+ {/* Key Metrics Grid */} +
+
+
+ + Win Rate +
+
+ {result.winRate.toFixed(1)}% +
+
+
+
+ + Profit Factor +
+
+ {result.profitFactor.toFixed(2)} +
+
+
+
+ + Sharpe Ratio +
+
+ {result.sharpeRatio.toFixed(2)} +
+
+
+
+ + Max Drawdown +
+
+ {result.maxDrawdownPercent.toFixed(1)}% +
+
+
+ + {/* Trade Statistics */} +
+
+

Trade Statistics

+
+
+ Total Trades + {result.totalTrades} +
+
+ Winning + {result.winningTrades} +
+
+ Losing + {result.losingTrades} +
+
+ Avg Duration + {formatDuration(result.avgTradeDuration)} +
+
+
+ +
+

P&L Analysis

+
+
+ Avg Win + {formatCurrency(result.avgWin)} +
+
+ Avg Loss + {formatCurrency(result.avgLoss)} +
+
+ Avg R:R + 1:{result.avgRR.toFixed(2)} +
+
+ Expectancy + = 0 ? 'text-green-400' : 'text-red-400'}> + {formatCurrency(result.expectancy)} + +
+
+
+
+ + {/* Streaks & Extremes */} +
+

Extremes & Streaks

+
+
+
{formatCurrency(result.largestWin)}
+
Largest Win
+
+
+
{formatCurrency(result.largestLoss)}
+
Largest Loss
+
+
+
{result.longestWinStreak}
+
Win Streak
+
+
+
{result.longestLoseStreak}
+
Lose Streak
+
+
+
+
+ )} + + {/* Trades Tab */} + {activeTab === 'trades' && ( +
+
+ {displayedTrades.map((trade) => { + const isExpanded = expandedTradeId === trade.id; + return ( +
{ + setExpandedTradeId(isExpanded ? null : trade.id); + onTradeSelect?.(trade); + }} + className="p-3 bg-gray-900/50 rounded-lg border border-gray-700/50 cursor-pointer hover:border-gray-600 transition-colors" + > +
+
+
+ {trade.direction === 'BUY' ? ( + + ) : ( + + )} +
+
+ {trade.symbol} +
{formatDate(trade.entryTime)}
+
+
+
+
+
= 0 ? 'text-green-400' : 'text-red-400'}`}> + {trade.profit >= 0 ? '+' : ''}{formatCurrency(trade.profit)} +
+
= 0 ? 'text-green-400' : 'text-red-400'}`}> + {formatPercent(trade.profitPercent)} +
+
+ {isExpanded ? : } +
+
+ + {isExpanded && ( +
+
+ Entry: + {trade.entryPrice.toFixed(5)} +
+
+ Exit: + {trade.exitPrice.toFixed(5)} +
+
+ Duration: + {formatDuration(trade.duration)} +
+
+ Exit: + {formatDate(trade.exitTime)} +
+
+ )} +
+ ); + })} +
+ + {result.trades.length > 10 && ( + + )} +
+ )} + + {/* Monthly Tab */} + {activeTab === 'monthly' && result.monthlyReturns && ( +
+
+ {Object.entries(result.monthlyReturns).map(([month, returns]) => ( +
= 0 ? 'bg-green-500/10 border border-green-500/20' : 'bg-red-500/10 border border-red-500/20' + }`} + > +
{month}
+
= 0 ? 'text-green-400' : 'text-red-400'}`}> + {formatPercent(returns)} +
+
+ ))} +
+
+ )} +
+ ); +}; + +export default BacktestResultsVisualization; diff --git a/src/modules/ml/components/ConfidenceMeter.tsx b/src/modules/ml/components/ConfidenceMeter.tsx new file mode 100644 index 0000000..fa42887 --- /dev/null +++ b/src/modules/ml/components/ConfidenceMeter.tsx @@ -0,0 +1,302 @@ +/** + * ConfidenceMeter Component + * Advanced confidence indicator with uncertainty visualization + * OQI-006: Senales ML + */ + +import React, { useMemo } from 'react'; +import { + Gauge, + TrendingUp, + TrendingDown, + AlertTriangle, + CheckCircle, + Info, + ChevronDown, + ChevronUp, + Zap, + BarChart3, + Users, +} from 'lucide-react'; + +export interface ConfidenceData { + overall: number; + modelAgreement: number; + trend: 'up' | 'down' | 'stable'; + trendChange: number; + modelVotes: { + bullish: number; + bearish: number; + neutral: number; + }; + featureImportance?: { + feature: string; + contribution: number; + direction: 'positive' | 'negative'; + }[]; + historicalConfidence?: { timestamp: string; value: number }[]; + volatility?: number; +} + +interface ConfidenceMeterProps { + confidence: ConfidenceData; + direction?: 'BUY' | 'SELL' | 'NEUTRAL'; + symbol?: string; + showDetails?: boolean; + onToggleDetails?: () => void; + compact?: boolean; +} + +const ConfidenceMeter: React.FC = ({ + confidence, + direction = 'NEUTRAL', + symbol, + showDetails = false, + onToggleDetails, + compact = false, +}) => { + const [isExpanded, setIsExpanded] = React.useState(showDetails); + + // Get confidence zone + const getConfidenceZone = (value: number) => { + if (value >= 70) return { label: 'High', color: 'green' }; + if (value >= 40) return { label: 'Medium', color: 'yellow' }; + return { label: 'Low', color: 'red' }; + }; + + const zone = useMemo(() => getConfidenceZone(confidence.overall), [confidence.overall]); + + const getZoneColor = (color: string) => { + switch (color) { + case 'green': + return 'text-green-400 bg-green-500/20 border-green-500/30'; + case 'yellow': + return 'text-yellow-400 bg-yellow-500/20 border-yellow-500/30'; + case 'red': + return 'text-red-400 bg-red-500/20 border-red-500/30'; + default: + return 'text-gray-400 bg-gray-500/20 border-gray-500/30'; + } + }; + + const getGaugeBackground = (value: number) => { + const percentage = Math.min(Math.max(value, 0), 100); + const color = + percentage >= 70 + ? 'from-green-600 to-green-400' + : percentage >= 40 + ? 'from-yellow-600 to-yellow-400' + : 'from-red-600 to-red-400'; + return `bg-gradient-to-r ${color}`; + }; + + const totalVotes = confidence.modelVotes.bullish + confidence.modelVotes.bearish + confidence.modelVotes.neutral; + + const formatPercent = (value: number) => `${value.toFixed(1)}%`; + + if (compact) { + return ( +
+
+ +
+
+
+ Confidence + + {confidence.overall.toFixed(0)}% + +
+
+
+
+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

Confidence Meter

+ {symbol &&

{symbol}

} +
+
+
+ {zone.label} Confidence +
+
+ + {/* Main Gauge */} +
+
+ {confidence.overall.toFixed(0)}% +
+ {confidence.trend === 'up' ? ( + + ) : confidence.trend === 'down' ? ( + + ) : ( +
+ )} + + {confidence.trendChange > 0 ? '+' : ''} + {confidence.trendChange.toFixed(1)}% + +
+
+ + {/* Gauge Bar */} +
+ {/* Zone indicators */} +
+
+
+
+
+ {/* Current value */} +
+ {/* Marker */} +
+
+ + {/* Zone labels */} +
+ Low (0-40%) + Medium (40-70%) + High (70-100%) +
+
+ + {/* Model Agreement */} +
+
+
+ + Model Agreement +
+
{formatPercent(confidence.modelAgreement)}
+
+ + {confidence.volatility !== undefined && ( +
+
+ + Volatility Risk +
+
= 70 ? 'text-red-400' : confidence.volatility >= 40 ? 'text-yellow-400' : 'text-green-400' + }`} + > + {confidence.volatility.toFixed(0)}% +
+
+ )} +
+ + {/* Model Votes */} +
+
Model Votes ({totalVotes} models)
+
+
+
+
+
+
+ {confidence.modelVotes.bullish} Bullish + {confidence.modelVotes.neutral} Neutral + {confidence.modelVotes.bearish} Bearish +
+
+ + {/* Feature Importance (Expandable) */} + {confidence.featureImportance && confidence.featureImportance.length > 0 && ( +
+ + + {isExpanded && ( +
+ {confidence.featureImportance.map((feature, idx) => ( +
+
+ {feature.direction === 'positive' ? ( + + ) : ( + + )} + {feature.feature} +
+
+ {feature.direction === 'positive' ? '+' : '-'} + {Math.abs(feature.contribution).toFixed(1)}% +
+
+ ))} +
+ )} +
+ )} + + {/* Risk Advisory */} + {confidence.overall < 40 && ( +
+ +
+

Low Confidence Warning

+

+ This signal has low model agreement. Consider waiting for stronger confirmation. +

+
+
+ )} +
+ ); +}; + +export default ConfidenceMeter; diff --git a/src/modules/ml/components/ModelAccuracyDashboard.tsx b/src/modules/ml/components/ModelAccuracyDashboard.tsx new file mode 100644 index 0000000..3cfdfd4 --- /dev/null +++ b/src/modules/ml/components/ModelAccuracyDashboard.tsx @@ -0,0 +1,410 @@ +/** + * ModelAccuracyDashboard Component + * Individual model accuracy monitoring and comparison + * OQI-006: Senales ML + */ + +import React, { useState, useMemo } from 'react'; +import { + Brain, + TrendingUp, + TrendingDown, + Target, + BarChart3, + Activity, + CheckCircle, + XCircle, + Clock, + RefreshCw, + ChevronDown, + ChevronUp, + Zap, + Award, + AlertTriangle, + Filter, +} from 'lucide-react'; + +export interface ModelMetrics { + id: string; + name: string; + type: 'classification' | 'regression' | 'ensemble'; + accuracy: number; + winRate: number; + totalPredictions: number; + correctPredictions: number; + avgConfidence: number; + sharpeRatio?: number; + profitFactor?: number; + mae?: number; + rmse?: number; + lastUpdated: string; + status: 'active' | 'training' | 'inactive'; + performanceBySymbol?: Record; + performanceByTimeframe?: Record; + trendData?: { date: string; accuracy: number }[]; +} + +interface ModelAccuracyDashboardProps { + models: ModelMetrics[]; + selectedModelId?: string; + onModelSelect?: (modelId: string) => void; + onRefresh?: () => void; + isLoading?: boolean; + compact?: boolean; +} + +const ModelAccuracyDashboard: React.FC = ({ + models, + selectedModelId, + onModelSelect, + onRefresh, + isLoading = false, + compact = false, +}) => { + const [expandedModelId, setExpandedModelId] = useState(selectedModelId || null); + const [filterType, setFilterType] = useState<'all' | 'classification' | 'regression' | 'ensemble'>('all'); + const [sortBy, setSortBy] = useState<'accuracy' | 'predictions' | 'winRate'>('accuracy'); + const [compareMode, setCompareMode] = useState(false); + const [compareModels, setCompareModels] = useState([]); + + // Filter and sort models + const filteredModels = useMemo(() => { + let result = [...models]; + + if (filterType !== 'all') { + result = result.filter((m) => m.type === filterType); + } + + result.sort((a, b) => { + switch (sortBy) { + case 'accuracy': + return b.accuracy - a.accuracy; + case 'predictions': + return b.totalPredictions - a.totalPredictions; + case 'winRate': + return b.winRate - a.winRate; + default: + return 0; + } + }); + + return result; + }, [models, filterType, sortBy]); + + // Calculate aggregate stats + const aggregateStats = useMemo(() => { + const activeModels = models.filter((m) => m.status === 'active'); + const avgAccuracy = activeModels.reduce((sum, m) => sum + m.accuracy, 0) / (activeModels.length || 1); + const avgWinRate = activeModels.reduce((sum, m) => sum + m.winRate, 0) / (activeModels.length || 1); + const totalPredictions = models.reduce((sum, m) => sum + m.totalPredictions, 0); + const bestModel = models.reduce((best, m) => (m.accuracy > (best?.accuracy || 0) ? m : best), models[0]); + + return { avgAccuracy, avgWinRate, totalPredictions, bestModel, activeModels: activeModels.length }; + }, [models]); + + const getAccuracyColor = (accuracy: number) => { + if (accuracy >= 70) return 'text-green-400'; + if (accuracy >= 50) return 'text-yellow-400'; + return 'text-red-400'; + }; + + const getAccuracyBg = (accuracy: number) => { + if (accuracy >= 70) return 'bg-green-500'; + if (accuracy >= 50) return 'bg-yellow-500'; + return 'bg-red-500'; + }; + + const getStatusBadge = (status: string) => { + switch (status) { + case 'active': + return Active; + case 'training': + return Training; + default: + return Inactive; + } + }; + + const toggleCompare = (modelId: string) => { + if (compareModels.includes(modelId)) { + setCompareModels(compareModels.filter((id) => id !== modelId)); + } else if (compareModels.length < 2) { + setCompareModels([...compareModels, modelId]); + } + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + }; + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

Model Accuracy

+

{aggregateStats.activeModels} active models

+
+
+ +
+ + {onRefresh && ( + + )} +
+
+ + {/* Aggregate Stats */} +
+
+
+ {aggregateStats.avgAccuracy.toFixed(1)}% +
+
Avg Accuracy
+
+
+
{aggregateStats.avgWinRate.toFixed(1)}%
+
Avg Win Rate
+
+
+
{aggregateStats.totalPredictions.toLocaleString()}
+
Total Predictions
+
+
+
+ {aggregateStats.bestModel?.name || '-'} +
+
Best Model
+
+
+ + {/* Filters */} + {!compact && ( +
+ + + +
+ )} + + {/* Compare Section */} + {compareMode && compareModels.length === 2 && ( +
+

Model Comparison

+
+ {compareModels.map((modelId) => { + const model = models.find((m) => m.id === modelId); + if (!model) return null; + return ( +
+
{model.name}
+
+
+ Accuracy + {model.accuracy.toFixed(1)}% +
+
+ Win Rate + {model.winRate.toFixed(1)}% +
+
+ Predictions + {model.totalPredictions} +
+ {model.profitFactor && ( +
+ Profit Factor + {model.profitFactor.toFixed(2)} +
+ )} +
+
+ ); + })} +
+
+ )} + + {/* Models List */} +
+ {filteredModels.map((model) => { + const isExpanded = expandedModelId === model.id; + const isSelected = compareModels.includes(model.id); + + return ( +
+ {/* Header */} +
{ + if (compareMode) { + toggleCompare(model.id); + } else { + setExpandedModelId(isExpanded ? null : model.id); + onModelSelect?.(model.id); + } + }} + className="flex items-center justify-between cursor-pointer" + > +
+ {compareMode && ( +
+ {isSelected && } +
+ )} +
+ +
+
+
+ {model.name} + {getStatusBadge(model.status)} +
+
+ {model.type} • {model.totalPredictions} predictions +
+
+
+ +
+ {/* Accuracy Bar */} +
+
+ Accuracy + {model.accuracy.toFixed(1)}% +
+
+
+
+
+ {!compareMode && (isExpanded ? : )} +
+
+ + {/* Expanded Details */} + {isExpanded && !compareMode && ( +
+ {/* Detailed Metrics */} +
+
+
{model.winRate.toFixed(1)}%
+
Win Rate
+
+
+
{model.avgConfidence.toFixed(1)}%
+
Avg Confidence
+
+ {model.profitFactor && ( +
+
{model.profitFactor.toFixed(2)}
+
Profit Factor
+
+ )} + {model.sharpeRatio && ( +
+
{model.sharpeRatio.toFixed(2)}
+
Sharpe Ratio
+
+ )} +
+ + {/* Performance by Symbol */} + {model.performanceBySymbol && Object.keys(model.performanceBySymbol).length > 0 && ( +
+
Performance by Symbol
+
+ {Object.entries(model.performanceBySymbol).slice(0, 5).map(([symbol, data]) => ( +
+ {symbol}: + {data.accuracy.toFixed(0)}% +
+ ))} +
+
+ )} + + {/* Performance by Timeframe */} + {model.performanceByTimeframe && Object.keys(model.performanceByTimeframe).length > 0 && ( +
+
Performance by Timeframe
+
+ {Object.entries(model.performanceByTimeframe).map(([tf, data]) => ( +
+ {tf}: + {data.accuracy.toFixed(0)}% +
+ ))} +
+
+ )} + + {/* Last Updated */} +
+ + Last updated: {formatDate(model.lastUpdated)} +
+
+ )} +
+ ); + })} +
+
+ ); +}; + +export default ModelAccuracyDashboard; diff --git a/src/modules/ml/components/SignalPerformanceTracker.tsx b/src/modules/ml/components/SignalPerformanceTracker.tsx new file mode 100644 index 0000000..b2da890 --- /dev/null +++ b/src/modules/ml/components/SignalPerformanceTracker.tsx @@ -0,0 +1,412 @@ +/** + * SignalPerformanceTracker Component + * Signal history and performance analysis + * OQI-006: Senales ML + */ + +import React, { useState, useMemo } from 'react'; +import { + History, + TrendingUp, + TrendingDown, + Target, + Shield, + Clock, + Filter, + Search, + ChevronDown, + ChevronUp, + CheckCircle, + XCircle, + AlertCircle, + DollarSign, + Percent, + Calendar, + BarChart3, + Download, +} from 'lucide-react'; + +export type SignalStatus = 'active' | 'executed' | 'tp_hit' | 'sl_hit' | 'expired' | 'cancelled'; + +export interface SignalHistoryEntry { + id: string; + symbol: string; + direction: 'BUY' | 'SELL'; + confidence: number; + entryPrice: number; + currentPrice?: number; + stopLoss: number; + takeProfit: number; + status: SignalStatus; + createdAt: string; + executedAt?: string; + closedAt?: string; + actualProfit?: number; + actualProfitPercent?: number; + expectedRR: number; + actualRR?: number; + modelName: string; + reasoning?: string; +} + +interface SignalStats { + totalSignals: number; + executed: number; + tpHit: number; + slHit: number; + expired: number; + winRate: number; + totalProfit: number; + avgProfit: number; + avgRR: number; + profitFactor: number; +} + +interface SignalPerformanceTrackerProps { + signals: SignalHistoryEntry[]; + onSignalSelect?: (signal: SignalHistoryEntry) => void; + onExport?: () => void; + maxItems?: number; + compact?: boolean; +} + +const SignalPerformanceTracker: React.FC = ({ + signals, + onSignalSelect, + onExport, + maxItems = 50, + compact = false, +}) => { + const [filterSymbol, setFilterSymbol] = useState(''); + const [filterStatus, setFilterStatus] = useState('all'); + const [filterDirection, setFilterDirection] = useState<'BUY' | 'SELL' | 'all'>('all'); + const [sortBy, setSortBy] = useState<'date' | 'profit' | 'confidence'>('date'); + const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc'); + const [expandedId, setExpandedId] = useState(null); + + // Calculate stats + const stats: SignalStats = useMemo(() => { + const executed = signals.filter((s) => s.status !== 'active' && s.status !== 'cancelled'); + const winners = signals.filter((s) => s.status === 'tp_hit'); + const losers = signals.filter((s) => s.status === 'sl_hit'); + + const totalProfit = executed.reduce((sum, s) => sum + (s.actualProfit || 0), 0); + const totalWins = winners.reduce((sum, s) => sum + (s.actualProfit || 0), 0); + const totalLosses = Math.abs(losers.reduce((sum, s) => sum + (s.actualProfit || 0), 0)); + + return { + totalSignals: signals.length, + executed: executed.length, + tpHit: winners.length, + slHit: losers.length, + expired: signals.filter((s) => s.status === 'expired').length, + winRate: executed.length > 0 ? (winners.length / executed.length) * 100 : 0, + totalProfit, + avgProfit: executed.length > 0 ? totalProfit / executed.length : 0, + avgRR: executed.filter((s) => s.actualRR).reduce((sum, s) => sum + (s.actualRR || 0), 0) / (executed.filter((s) => s.actualRR).length || 1), + profitFactor: totalLosses > 0 ? totalWins / totalLosses : totalWins, + }; + }, [signals]); + + // Filter and sort signals + const filteredSignals = useMemo(() => { + let result = [...signals]; + + if (filterSymbol) { + result = result.filter((s) => s.symbol.toLowerCase().includes(filterSymbol.toLowerCase())); + } + if (filterStatus !== 'all') { + result = result.filter((s) => s.status === filterStatus); + } + if (filterDirection !== 'all') { + result = result.filter((s) => s.direction === filterDirection); + } + + result.sort((a, b) => { + let comparison = 0; + switch (sortBy) { + case 'date': + comparison = new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); + break; + case 'profit': + comparison = (b.actualProfit || 0) - (a.actualProfit || 0); + break; + case 'confidence': + comparison = b.confidence - a.confidence; + break; + } + return sortDir === 'desc' ? comparison : -comparison; + }); + + return result.slice(0, maxItems); + }, [signals, filterSymbol, filterStatus, filterDirection, sortBy, sortDir, maxItems]); + + const getStatusIcon = (status: SignalStatus) => { + switch (status) { + case 'tp_hit': + return ; + case 'sl_hit': + return ; + case 'expired': + return ; + case 'active': + return ; + case 'executed': + return ; + default: + return ; + } + }; + + const getStatusLabel = (status: SignalStatus) => { + switch (status) { + case 'tp_hit': + return 'TP Hit'; + case 'sl_hit': + return 'SL Hit'; + case 'expired': + return 'Expired'; + case 'active': + return 'Active'; + case 'executed': + return 'Running'; + case 'cancelled': + return 'Cancelled'; + default: + return status; + } + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + }; + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

Signal Performance

+

{stats.totalSignals} signals tracked

+
+
+ {onExport && ( + + )} +
+ + {/* Stats Grid */} +
+
+
{stats.winRate.toFixed(1)}%
+
Win Rate
+
+
+
= 0 ? 'text-green-400' : 'text-red-400'}`}> + ${stats.totalProfit.toFixed(2)} +
+
Total P&L
+
+
+
{stats.profitFactor.toFixed(2)}
+
Profit Factor
+
+
+
+ {stats.tpHit} + / + {stats.slHit} +
+
TP/SL Hits
+
+
+ + {/* Filters */} + {!compact && ( +
+
+ + setFilterSymbol(e.target.value)} + placeholder="Filter symbol..." + className="w-full pl-10 pr-3 py-1.5 bg-gray-900 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500" + /> +
+ + + +
+ )} + + {/* Signals List */} + {filteredSignals.length === 0 ? ( +
+ +

No signals found

+
+ ) : ( +
+ {filteredSignals.map((signal) => { + const isExpanded = expandedId === signal.id; + + return ( +
{ + setExpandedId(isExpanded ? null : signal.id); + onSignalSelect?.(signal); + }} + className={`p-3 bg-gray-900/50 rounded-lg border cursor-pointer transition-all ${ + isExpanded ? 'border-blue-500/50' : 'border-gray-700/50 hover:border-gray-600' + }`} + > + {/* Main Row */} +
+
+
+ {signal.direction === 'BUY' ? ( + + ) : ( + + )} +
+
+
+ {signal.symbol} + + {getStatusLabel(signal.status)} + +
+
+ {formatDate(signal.createdAt)} • {signal.modelName} +
+
+
+ +
+
+ {signal.actualProfit !== undefined ? ( +
= 0 ? 'text-green-400' : 'text-red-400'}`}> + {signal.actualProfit >= 0 ? '+' : ''}${signal.actualProfit.toFixed(2)} +
+ ) : ( +
--
+ )} +
+ Conf: {signal.confidence.toFixed(0)}% +
+
+ {isExpanded ? ( + + ) : ( + + )} +
+
+ + {/* Expanded Details */} + {isExpanded && ( +
+
+
+ Entry: + {signal.entryPrice.toFixed(5)} +
+
+ SL: + {signal.stopLoss.toFixed(5)} +
+
+ TP: + {signal.takeProfit.toFixed(5)} +
+
+ +
+
+ Expected RR: + 1:{signal.expectedRR.toFixed(2)} +
+ {signal.actualRR && ( +
+ Actual RR: + = 0 ? 'text-green-400' : 'text-red-400'}`}> + 1:{Math.abs(signal.actualRR).toFixed(2)} + +
+ )} +
+ + {signal.reasoning && ( +
+ Reasoning: {signal.reasoning} +
+ )} +
+ )} +
+ ); + })} +
+ )} +
+ ); +}; + +export default SignalPerformanceTracker; diff --git a/src/modules/ml/components/index.ts b/src/modules/ml/components/index.ts index 5e7f063..35286ad 100644 --- a/src/modules/ml/components/index.ts +++ b/src/modules/ml/components/index.ts @@ -10,3 +10,13 @@ export { AccuracyMetrics } from './AccuracyMetrics'; export { ICTAnalysisCard } from './ICTAnalysisCard'; export { EnsembleSignalCard } from './EnsembleSignalCard'; export { TradeExecutionModal } from './TradeExecutionModal'; + +// Confidence & Performance Components (OQI-006) +export { default as ConfidenceMeter } from './ConfidenceMeter'; +export type { ConfidenceData } from './ConfidenceMeter'; +export { default as SignalPerformanceTracker } from './SignalPerformanceTracker'; +export type { SignalHistoryEntry, SignalStatus, SignalStats } from './SignalPerformanceTracker'; +export { default as ModelAccuracyDashboard } from './ModelAccuracyDashboard'; +export type { ModelMetrics } from './ModelAccuracyDashboard'; +export { default as BacktestResultsVisualization } from './BacktestResultsVisualization'; +export type { BacktestResult, BacktestTrade } from './BacktestResultsVisualization';