[OQI-006] feat: Add ML confidence and performance tracking components

- ConfidenceMeter: Advanced confidence visualization with model agreement
- SignalPerformanceTracker: Signal history with filters and statistics
- ModelAccuracyDashboard: Individual model metrics and comparison
- BacktestResultsVisualization: Backtest results with trades and monthly returns

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-25 12:17:16 -06:00
parent cbb6637966
commit e9aa29fccd
5 changed files with 1566 additions and 0 deletions

View File

@ -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<string, number>;
}
interface BacktestResultsVisualizationProps {
result: BacktestResult;
onExport?: (format: 'csv' | 'pdf') => void;
onTradeSelect?: (trade: BacktestTrade) => void;
compact?: boolean;
}
const BacktestResultsVisualization: React.FC<BacktestResultsVisualizationProps> = ({
result,
onExport,
onTradeSelect,
compact = false,
}) => {
const [activeTab, setActiveTab] = useState<'overview' | 'trades' | 'monthly'>('overview');
const [showAllTrades, setShowAllTrades] = useState(false);
const [expandedTradeId, setExpandedTradeId] = useState<string | null>(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 (
<div className={`bg-gray-800/50 rounded-xl border border-gray-700 ${compact ? 'p-3' : 'p-4'}`}>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${isProfitable ? 'bg-green-500/20' : 'bg-red-500/20'}`}>
<LineChart className={`w-5 h-5 ${isProfitable ? 'text-green-400' : 'text-red-400'}`} />
</div>
<div>
<h3 className="font-semibold text-white">{result.strategyName}</h3>
<p className="text-xs text-gray-500">
{formatDate(result.period.start)} - {formatDate(result.period.end)}
</p>
</div>
</div>
{onExport && (
<div className="flex items-center gap-2">
<button
onClick={() => onExport('csv')}
className="px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-white rounded text-xs"
>
Export CSV
</button>
<button
onClick={() => onExport('pdf')}
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white rounded text-xs"
>
Export PDF
</button>
</div>
)}
</div>
{/* Main Result Card */}
<div className={`p-6 rounded-xl mb-6 ${
isProfitable
? 'bg-gradient-to-r from-green-500/10 to-emerald-500/10 border border-green-500/30'
: 'bg-gradient-to-r from-red-500/10 to-rose-500/10 border border-red-500/30'
}`}>
<div className="flex items-center justify-between">
<div>
<div className="text-gray-400 text-sm mb-1">Total Return</div>
<div className={`text-4xl font-bold ${isProfitable ? 'text-green-400' : 'text-red-400'}`}>
{formatCurrency(result.totalReturn)}
</div>
<div className={`text-lg ${isProfitable ? 'text-green-400' : 'text-red-400'}`}>
{formatPercent(result.totalReturnPercent)}
</div>
</div>
<div className="text-right">
<div className="flex items-center gap-2 mb-2">
{isGoodPerformance ? (
<Award className="w-6 h-6 text-yellow-400" />
) : (
<AlertTriangle className="w-6 h-6 text-yellow-400" />
)}
<span className="text-gray-400 text-sm">
{isGoodPerformance ? 'Strong Performance' : 'Needs Optimization'}
</span>
</div>
<div className="text-xs text-gray-500">
{formatCurrency(result.initialCapital)} {formatCurrency(result.finalCapital)}
</div>
</div>
</div>
</div>
{/* Tabs */}
<div className="flex items-center gap-2 mb-4">
{(['overview', 'trades', 'monthly'] as const).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-4 py-2 rounded text-sm font-medium transition-colors ${
activeTab === tab
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
{tab.charAt(0).toUpperCase() + tab.slice(1)}
</button>
))}
</div>
{/* Overview Tab */}
{activeTab === 'overview' && (
<div className="space-y-4">
{/* Key Metrics Grid */}
<div className="grid grid-cols-4 gap-3">
<div className="p-3 bg-gray-900/50 rounded-lg">
<div className="flex items-center gap-2 mb-1">
<Target className="w-4 h-4 text-blue-400" />
<span className="text-xs text-gray-400">Win Rate</span>
</div>
<div className={`text-xl font-bold ${getMetricColor(result.winRate, { good: 55, bad: 40 })}`}>
{result.winRate.toFixed(1)}%
</div>
</div>
<div className="p-3 bg-gray-900/50 rounded-lg">
<div className="flex items-center gap-2 mb-1">
<Zap className="w-4 h-4 text-yellow-400" />
<span className="text-xs text-gray-400">Profit Factor</span>
</div>
<div className={`text-xl font-bold ${getMetricColor(result.profitFactor, { good: 1.5, bad: 1 })}`}>
{result.profitFactor.toFixed(2)}
</div>
</div>
<div className="p-3 bg-gray-900/50 rounded-lg">
<div className="flex items-center gap-2 mb-1">
<Activity className="w-4 h-4 text-purple-400" />
<span className="text-xs text-gray-400">Sharpe Ratio</span>
</div>
<div className={`text-xl font-bold ${getMetricColor(result.sharpeRatio, { good: 1.5, bad: 0.5 })}`}>
{result.sharpeRatio.toFixed(2)}
</div>
</div>
<div className="p-3 bg-gray-900/50 rounded-lg">
<div className="flex items-center gap-2 mb-1">
<Shield className="w-4 h-4 text-red-400" />
<span className="text-xs text-gray-400">Max Drawdown</span>
</div>
<div className={`text-xl font-bold ${getMetricColor(result.maxDrawdownPercent, { good: 10, bad: 25 }, true)}`}>
{result.maxDrawdownPercent.toFixed(1)}%
</div>
</div>
</div>
{/* Trade Statistics */}
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-gray-900/50 rounded-lg">
<h4 className="text-sm text-gray-400 mb-3">Trade Statistics</h4>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-500">Total Trades</span>
<span className="text-white">{result.totalTrades}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500">Winning</span>
<span className="text-green-400">{result.winningTrades}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500">Losing</span>
<span className="text-red-400">{result.losingTrades}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500">Avg Duration</span>
<span className="text-white">{formatDuration(result.avgTradeDuration)}</span>
</div>
</div>
</div>
<div className="p-4 bg-gray-900/50 rounded-lg">
<h4 className="text-sm text-gray-400 mb-3">P&L Analysis</h4>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-500">Avg Win</span>
<span className="text-green-400">{formatCurrency(result.avgWin)}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500">Avg Loss</span>
<span className="text-red-400">{formatCurrency(result.avgLoss)}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500">Avg R:R</span>
<span className="text-white">1:{result.avgRR.toFixed(2)}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500">Expectancy</span>
<span className={result.expectancy >= 0 ? 'text-green-400' : 'text-red-400'}>
{formatCurrency(result.expectancy)}
</span>
</div>
</div>
</div>
</div>
{/* Streaks & Extremes */}
<div className="p-4 bg-gray-900/50 rounded-lg">
<h4 className="text-sm text-gray-400 mb-3">Extremes & Streaks</h4>
<div className="grid grid-cols-4 gap-4">
<div className="text-center">
<div className="text-green-400 font-bold">{formatCurrency(result.largestWin)}</div>
<div className="text-xs text-gray-500">Largest Win</div>
</div>
<div className="text-center">
<div className="text-red-400 font-bold">{formatCurrency(result.largestLoss)}</div>
<div className="text-xs text-gray-500">Largest Loss</div>
</div>
<div className="text-center">
<div className="text-green-400 font-bold">{result.longestWinStreak}</div>
<div className="text-xs text-gray-500">Win Streak</div>
</div>
<div className="text-center">
<div className="text-red-400 font-bold">{result.longestLoseStreak}</div>
<div className="text-xs text-gray-500">Lose Streak</div>
</div>
</div>
</div>
</div>
)}
{/* Trades Tab */}
{activeTab === 'trades' && (
<div>
<div className="space-y-2 max-h-[400px] overflow-y-auto">
{displayedTrades.map((trade) => {
const isExpanded = expandedTradeId === trade.id;
return (
<div
key={trade.id}
onClick={() => {
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"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`p-1.5 rounded ${trade.direction === 'BUY' ? 'bg-green-500/20' : 'bg-red-500/20'}`}>
{trade.direction === 'BUY' ? (
<TrendingUp className="w-4 h-4 text-green-400" />
) : (
<TrendingDown className="w-4 h-4 text-red-400" />
)}
</div>
<div>
<span className="font-medium text-white">{trade.symbol}</span>
<div className="text-xs text-gray-500">{formatDate(trade.entryTime)}</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="text-right">
<div className={`font-mono font-medium ${trade.profit >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{trade.profit >= 0 ? '+' : ''}{formatCurrency(trade.profit)}
</div>
<div className={`text-xs ${trade.profit >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{formatPercent(trade.profitPercent)}
</div>
</div>
{isExpanded ? <ChevronUp className="w-4 h-4 text-gray-500" /> : <ChevronDown className="w-4 h-4 text-gray-500" />}
</div>
</div>
{isExpanded && (
<div className="mt-3 pt-3 border-t border-gray-700/50 grid grid-cols-4 gap-3 text-xs">
<div>
<span className="text-gray-500">Entry:</span>
<span className="text-white ml-1 font-mono">{trade.entryPrice.toFixed(5)}</span>
</div>
<div>
<span className="text-gray-500">Exit:</span>
<span className="text-white ml-1 font-mono">{trade.exitPrice.toFixed(5)}</span>
</div>
<div>
<span className="text-gray-500">Duration:</span>
<span className="text-white ml-1">{formatDuration(trade.duration)}</span>
</div>
<div>
<span className="text-gray-500">Exit:</span>
<span className="text-white ml-1">{formatDate(trade.exitTime)}</span>
</div>
</div>
)}
</div>
);
})}
</div>
{result.trades.length > 10 && (
<button
onClick={() => setShowAllTrades(!showAllTrades)}
className="w-full mt-3 py-2 text-sm text-blue-400 hover:text-blue-300 text-center"
>
{showAllTrades ? 'Show Less' : `Show All ${result.trades.length} Trades`}
</button>
)}
</div>
)}
{/* Monthly Tab */}
{activeTab === 'monthly' && result.monthlyReturns && (
<div>
<div className="grid grid-cols-4 gap-2">
{Object.entries(result.monthlyReturns).map(([month, returns]) => (
<div
key={month}
className={`p-3 rounded-lg text-center ${
returns >= 0 ? 'bg-green-500/10 border border-green-500/20' : 'bg-red-500/10 border border-red-500/20'
}`}
>
<div className="text-xs text-gray-400 mb-1">{month}</div>
<div className={`font-bold ${returns >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{formatPercent(returns)}
</div>
</div>
))}
</div>
</div>
)}
</div>
);
};
export default BacktestResultsVisualization;

View File

@ -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<ConfidenceMeterProps> = ({
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 (
<div className="flex items-center gap-3 p-3 bg-gray-800/50 rounded-lg border border-gray-700">
<div className={`p-2 rounded-lg ${getZoneColor(zone.color)}`}>
<Gauge className="w-5 h-5" />
</div>
<div className="flex-1">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-400">Confidence</span>
<span className={`text-lg font-bold ${getZoneColor(zone.color).split(' ')[0]}`}>
{confidence.overall.toFixed(0)}%
</span>
</div>
<div className="h-1.5 bg-gray-700 rounded-full mt-1 overflow-hidden">
<div className={`h-full rounded-full ${getGaugeBackground(confidence.overall)}`} style={{ width: `${confidence.overall}%` }} />
</div>
</div>
</div>
);
}
return (
<div className="bg-gray-800/50 rounded-xl border border-gray-700 p-4">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${getZoneColor(zone.color)}`}>
<Gauge className="w-5 h-5" />
</div>
<div>
<h3 className="font-semibold text-white">Confidence Meter</h3>
{symbol && <p className="text-xs text-gray-500">{symbol}</p>}
</div>
</div>
<div className={`px-3 py-1 rounded-full text-xs font-medium ${getZoneColor(zone.color)}`}>
{zone.label} Confidence
</div>
</div>
{/* Main Gauge */}
<div className="mb-6">
<div className="flex items-end justify-between mb-2">
<span className="text-4xl font-bold text-white">{confidence.overall.toFixed(0)}%</span>
<div className="flex items-center gap-1">
{confidence.trend === 'up' ? (
<TrendingUp className="w-4 h-4 text-green-400" />
) : confidence.trend === 'down' ? (
<TrendingDown className="w-4 h-4 text-red-400" />
) : (
<div className="w-4 h-0.5 bg-gray-500" />
)}
<span
className={`text-sm ${
confidence.trend === 'up'
? 'text-green-400'
: confidence.trend === 'down'
? 'text-red-400'
: 'text-gray-400'
}`}
>
{confidence.trendChange > 0 ? '+' : ''}
{confidence.trendChange.toFixed(1)}%
</span>
</div>
</div>
{/* Gauge Bar */}
<div className="relative h-4 bg-gray-700 rounded-full overflow-hidden">
{/* Zone indicators */}
<div className="absolute inset-0 flex">
<div className="w-[40%] bg-red-500/20 border-r border-gray-600" />
<div className="w-[30%] bg-yellow-500/20 border-r border-gray-600" />
<div className="w-[30%] bg-green-500/20" />
</div>
{/* Current value */}
<div
className={`absolute h-full ${getGaugeBackground(confidence.overall)} transition-all duration-500`}
style={{ width: `${confidence.overall}%` }}
/>
{/* Marker */}
<div
className="absolute top-0 bottom-0 w-1 bg-white shadow-lg"
style={{ left: `${confidence.overall}%`, transform: 'translateX(-50%)' }}
/>
</div>
{/* Zone labels */}
<div className="flex justify-between mt-1 text-xs text-gray-500">
<span>Low (0-40%)</span>
<span>Medium (40-70%)</span>
<span>High (70-100%)</span>
</div>
</div>
{/* Model Agreement */}
<div className="grid grid-cols-2 gap-4 mb-4">
<div className="p-3 bg-gray-900/50 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Users className="w-4 h-4 text-blue-400" />
<span className="text-sm text-gray-400">Model Agreement</span>
</div>
<div className="text-xl font-bold text-white">{formatPercent(confidence.modelAgreement)}</div>
</div>
{confidence.volatility !== undefined && (
<div className="p-3 bg-gray-900/50 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Zap className="w-4 h-4 text-yellow-400" />
<span className="text-sm text-gray-400">Volatility Risk</span>
</div>
<div
className={`text-xl font-bold ${
confidence.volatility >= 70 ? 'text-red-400' : confidence.volatility >= 40 ? 'text-yellow-400' : 'text-green-400'
}`}
>
{confidence.volatility.toFixed(0)}%
</div>
</div>
)}
</div>
{/* Model Votes */}
<div className="mb-4">
<div className="text-sm text-gray-400 mb-2">Model Votes ({totalVotes} models)</div>
<div className="flex h-3 rounded-full overflow-hidden bg-gray-700">
<div
className="bg-green-500 transition-all"
style={{ width: `${(confidence.modelVotes.bullish / totalVotes) * 100}%` }}
title={`Bullish: ${confidence.modelVotes.bullish}`}
/>
<div
className="bg-gray-500 transition-all"
style={{ width: `${(confidence.modelVotes.neutral / totalVotes) * 100}%` }}
title={`Neutral: ${confidence.modelVotes.neutral}`}
/>
<div
className="bg-red-500 transition-all"
style={{ width: `${(confidence.modelVotes.bearish / totalVotes) * 100}%` }}
title={`Bearish: ${confidence.modelVotes.bearish}`}
/>
</div>
<div className="flex justify-between mt-1 text-xs">
<span className="text-green-400">{confidence.modelVotes.bullish} Bullish</span>
<span className="text-gray-400">{confidence.modelVotes.neutral} Neutral</span>
<span className="text-red-400">{confidence.modelVotes.bearish} Bearish</span>
</div>
</div>
{/* Feature Importance (Expandable) */}
{confidence.featureImportance && confidence.featureImportance.length > 0 && (
<div>
<button
onClick={() => setIsExpanded(!isExpanded)}
className="flex items-center justify-between w-full p-2 text-sm text-gray-400 hover:bg-gray-800 rounded-lg transition-colors"
>
<div className="flex items-center gap-2">
<BarChart3 className="w-4 h-4" />
<span>Feature Importance</span>
</div>
{isExpanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
{isExpanded && (
<div className="mt-2 space-y-2">
{confidence.featureImportance.map((feature, idx) => (
<div key={idx} className="flex items-center justify-between p-2 bg-gray-900/50 rounded">
<div className="flex items-center gap-2">
{feature.direction === 'positive' ? (
<CheckCircle className="w-3 h-3 text-green-400" />
) : (
<AlertTriangle className="w-3 h-3 text-red-400" />
)}
<span className="text-sm text-gray-300">{feature.feature}</span>
</div>
<div
className={`text-sm font-medium ${
feature.direction === 'positive' ? 'text-green-400' : 'text-red-400'
}`}
>
{feature.direction === 'positive' ? '+' : '-'}
{Math.abs(feature.contribution).toFixed(1)}%
</div>
</div>
))}
</div>
)}
</div>
)}
{/* Risk Advisory */}
{confidence.overall < 40 && (
<div className="mt-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg flex items-start gap-2">
<AlertTriangle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
<div>
<p className="text-red-400 font-medium text-sm">Low Confidence Warning</p>
<p className="text-xs text-gray-400 mt-1">
This signal has low model agreement. Consider waiting for stronger confirmation.
</p>
</div>
</div>
)}
</div>
);
};
export default ConfidenceMeter;

View File

@ -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<string, { accuracy: number; predictions: number }>;
performanceByTimeframe?: Record<string, { accuracy: number; predictions: number }>;
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<ModelAccuracyDashboardProps> = ({
models,
selectedModelId,
onModelSelect,
onRefresh,
isLoading = false,
compact = false,
}) => {
const [expandedModelId, setExpandedModelId] = useState<string | null>(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<string[]>([]);
// 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 <span className="px-2 py-0.5 bg-green-500/20 text-green-400 rounded text-xs">Active</span>;
case 'training':
return <span className="px-2 py-0.5 bg-yellow-500/20 text-yellow-400 rounded text-xs">Training</span>;
default:
return <span className="px-2 py-0.5 bg-gray-500/20 text-gray-400 rounded text-xs">Inactive</span>;
}
};
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 (
<div className={`bg-gray-800/50 rounded-xl border border-gray-700 ${compact ? 'p-3' : 'p-4'}`}>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-500/20 rounded-lg">
<Brain className="w-5 h-5 text-blue-400" />
</div>
<div>
<h3 className="font-semibold text-white">Model Accuracy</h3>
<p className="text-xs text-gray-500">{aggregateStats.activeModels} active models</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setCompareMode(!compareMode)}
className={`px-3 py-1.5 rounded text-xs font-medium transition-colors ${
compareMode
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
Compare
</button>
{onRefresh && (
<button
onClick={onRefresh}
disabled={isLoading}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded-lg transition-colors disabled:opacity-50"
>
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
</button>
)}
</div>
</div>
{/* Aggregate Stats */}
<div className="grid grid-cols-4 gap-3 mb-4">
<div className="p-3 bg-gray-900/50 rounded-lg text-center">
<div className={`text-lg font-bold ${getAccuracyColor(aggregateStats.avgAccuracy)}`}>
{aggregateStats.avgAccuracy.toFixed(1)}%
</div>
<div className="text-xs text-gray-500">Avg Accuracy</div>
</div>
<div className="p-3 bg-gray-900/50 rounded-lg text-center">
<div className="text-lg font-bold text-white">{aggregateStats.avgWinRate.toFixed(1)}%</div>
<div className="text-xs text-gray-500">Avg Win Rate</div>
</div>
<div className="p-3 bg-gray-900/50 rounded-lg text-center">
<div className="text-lg font-bold text-white">{aggregateStats.totalPredictions.toLocaleString()}</div>
<div className="text-xs text-gray-500">Total Predictions</div>
</div>
<div className="p-3 bg-gray-900/50 rounded-lg text-center">
<div className="text-lg font-bold text-yellow-400 truncate" title={aggregateStats.bestModel?.name}>
{aggregateStats.bestModel?.name || '-'}
</div>
<div className="text-xs text-gray-500">Best Model</div>
</div>
</div>
{/* Filters */}
{!compact && (
<div className="flex items-center gap-2 mb-4">
<Filter className="w-4 h-4 text-gray-500" />
<select
value={filterType}
onChange={(e) => setFilterType(e.target.value as any)}
className="px-2 py-1 bg-gray-900 border border-gray-700 rounded text-xs text-white"
>
<option value="all">All Types</option>
<option value="classification">Classification</option>
<option value="regression">Regression</option>
<option value="ensemble">Ensemble</option>
</select>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="px-2 py-1 bg-gray-900 border border-gray-700 rounded text-xs text-white"
>
<option value="accuracy">Sort by Accuracy</option>
<option value="predictions">Sort by Predictions</option>
<option value="winRate">Sort by Win Rate</option>
</select>
</div>
)}
{/* Compare Section */}
{compareMode && compareModels.length === 2 && (
<div className="mb-4 p-4 bg-blue-500/10 border border-blue-500/30 rounded-lg">
<h4 className="text-sm text-blue-400 font-medium mb-3">Model Comparison</h4>
<div className="grid grid-cols-2 gap-4">
{compareModels.map((modelId) => {
const model = models.find((m) => m.id === modelId);
if (!model) return null;
return (
<div key={model.id} className="p-3 bg-gray-900/50 rounded-lg">
<div className="font-medium text-white mb-2">{model.name}</div>
<div className="space-y-1 text-xs">
<div className="flex justify-between">
<span className="text-gray-500">Accuracy</span>
<span className={getAccuracyColor(model.accuracy)}>{model.accuracy.toFixed(1)}%</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Win Rate</span>
<span className="text-white">{model.winRate.toFixed(1)}%</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Predictions</span>
<span className="text-white">{model.totalPredictions}</span>
</div>
{model.profitFactor && (
<div className="flex justify-between">
<span className="text-gray-500">Profit Factor</span>
<span className="text-white">{model.profitFactor.toFixed(2)}</span>
</div>
)}
</div>
</div>
);
})}
</div>
</div>
)}
{/* Models List */}
<div className="space-y-2 max-h-[400px] overflow-y-auto">
{filteredModels.map((model) => {
const isExpanded = expandedModelId === model.id;
const isSelected = compareModels.includes(model.id);
return (
<div
key={model.id}
className={`p-3 bg-gray-900/50 rounded-lg border transition-all ${
isExpanded
? 'border-blue-500/50'
: isSelected
? 'border-yellow-500/50'
: 'border-gray-700/50 hover:border-gray-600'
}`}
>
{/* Header */}
<div
onClick={() => {
if (compareMode) {
toggleCompare(model.id);
} else {
setExpandedModelId(isExpanded ? null : model.id);
onModelSelect?.(model.id);
}
}}
className="flex items-center justify-between cursor-pointer"
>
<div className="flex items-center gap-3">
{compareMode && (
<div
className={`w-5 h-5 rounded border-2 flex items-center justify-center ${
isSelected ? 'border-yellow-500 bg-yellow-500/20' : 'border-gray-600'
}`}
>
{isSelected && <CheckCircle className="w-3 h-3 text-yellow-400" />}
</div>
)}
<div className={`p-1.5 rounded ${model.status === 'active' ? 'bg-blue-500/20' : 'bg-gray-500/20'}`}>
<Brain className={`w-4 h-4 ${model.status === 'active' ? 'text-blue-400' : 'text-gray-400'}`} />
</div>
<div>
<div className="flex items-center gap-2">
<span className="font-medium text-white">{model.name}</span>
{getStatusBadge(model.status)}
</div>
<div className="text-xs text-gray-500">
{model.type} {model.totalPredictions} predictions
</div>
</div>
</div>
<div className="flex items-center gap-4">
{/* Accuracy Bar */}
<div className="w-24">
<div className="flex items-center justify-between text-xs mb-1">
<span className="text-gray-500">Accuracy</span>
<span className={getAccuracyColor(model.accuracy)}>{model.accuracy.toFixed(1)}%</span>
</div>
<div className="h-1.5 bg-gray-700 rounded-full overflow-hidden">
<div className={`h-full ${getAccuracyBg(model.accuracy)} rounded-full`} style={{ width: `${model.accuracy}%` }} />
</div>
</div>
{!compareMode && (isExpanded ? <ChevronUp className="w-4 h-4 text-gray-500" /> : <ChevronDown className="w-4 h-4 text-gray-500" />)}
</div>
</div>
{/* Expanded Details */}
{isExpanded && !compareMode && (
<div className="mt-3 pt-3 border-t border-gray-700/50">
{/* Detailed Metrics */}
<div className="grid grid-cols-4 gap-3 mb-3">
<div className="p-2 bg-gray-800/50 rounded text-center">
<div className="text-white font-medium">{model.winRate.toFixed(1)}%</div>
<div className="text-xs text-gray-500">Win Rate</div>
</div>
<div className="p-2 bg-gray-800/50 rounded text-center">
<div className="text-white font-medium">{model.avgConfidence.toFixed(1)}%</div>
<div className="text-xs text-gray-500">Avg Confidence</div>
</div>
{model.profitFactor && (
<div className="p-2 bg-gray-800/50 rounded text-center">
<div className="text-white font-medium">{model.profitFactor.toFixed(2)}</div>
<div className="text-xs text-gray-500">Profit Factor</div>
</div>
)}
{model.sharpeRatio && (
<div className="p-2 bg-gray-800/50 rounded text-center">
<div className="text-white font-medium">{model.sharpeRatio.toFixed(2)}</div>
<div className="text-xs text-gray-500">Sharpe Ratio</div>
</div>
)}
</div>
{/* Performance by Symbol */}
{model.performanceBySymbol && Object.keys(model.performanceBySymbol).length > 0 && (
<div className="mb-3">
<div className="text-xs text-gray-400 mb-2">Performance by Symbol</div>
<div className="flex flex-wrap gap-2">
{Object.entries(model.performanceBySymbol).slice(0, 5).map(([symbol, data]) => (
<div key={symbol} className="px-2 py-1 bg-gray-800/50 rounded text-xs">
<span className="text-gray-400">{symbol}:</span>
<span className={`ml-1 ${getAccuracyColor(data.accuracy)}`}>{data.accuracy.toFixed(0)}%</span>
</div>
))}
</div>
</div>
)}
{/* Performance by Timeframe */}
{model.performanceByTimeframe && Object.keys(model.performanceByTimeframe).length > 0 && (
<div className="mb-3">
<div className="text-xs text-gray-400 mb-2">Performance by Timeframe</div>
<div className="flex flex-wrap gap-2">
{Object.entries(model.performanceByTimeframe).map(([tf, data]) => (
<div key={tf} className="px-2 py-1 bg-gray-800/50 rounded text-xs">
<span className="text-gray-400">{tf}:</span>
<span className={`ml-1 ${getAccuracyColor(data.accuracy)}`}>{data.accuracy.toFixed(0)}%</span>
</div>
))}
</div>
</div>
)}
{/* Last Updated */}
<div className="flex items-center gap-2 text-xs text-gray-500">
<Clock className="w-3 h-3" />
Last updated: {formatDate(model.lastUpdated)}
</div>
</div>
)}
</div>
);
})}
</div>
</div>
);
};
export default ModelAccuracyDashboard;

View File

@ -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<SignalPerformanceTrackerProps> = ({
signals,
onSignalSelect,
onExport,
maxItems = 50,
compact = false,
}) => {
const [filterSymbol, setFilterSymbol] = useState('');
const [filterStatus, setFilterStatus] = useState<SignalStatus | 'all'>('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<string | null>(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 <CheckCircle className="w-4 h-4 text-green-400" />;
case 'sl_hit':
return <XCircle className="w-4 h-4 text-red-400" />;
case 'expired':
return <Clock className="w-4 h-4 text-gray-400" />;
case 'active':
return <AlertCircle className="w-4 h-4 text-blue-400" />;
case 'executed':
return <Target className="w-4 h-4 text-yellow-400" />;
default:
return <AlertCircle className="w-4 h-4 text-gray-400" />;
}
};
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 (
<div className={`bg-gray-800/50 rounded-xl border border-gray-700 ${compact ? 'p-3' : 'p-4'}`}>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-purple-500/20 rounded-lg">
<History className="w-5 h-5 text-purple-400" />
</div>
<div>
<h3 className="font-semibold text-white">Signal Performance</h3>
<p className="text-xs text-gray-500">{stats.totalSignals} signals tracked</p>
</div>
</div>
{onExport && (
<button
onClick={onExport}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded-lg transition-colors"
>
<Download className="w-4 h-4" />
</button>
)}
</div>
{/* Stats Grid */}
<div className="grid grid-cols-4 gap-3 mb-4">
<div className="p-3 bg-gray-900/50 rounded-lg text-center">
<div className="text-lg font-bold text-white">{stats.winRate.toFixed(1)}%</div>
<div className="text-xs text-gray-500">Win Rate</div>
</div>
<div className="p-3 bg-gray-900/50 rounded-lg text-center">
<div className={`text-lg font-bold ${stats.totalProfit >= 0 ? 'text-green-400' : 'text-red-400'}`}>
${stats.totalProfit.toFixed(2)}
</div>
<div className="text-xs text-gray-500">Total P&L</div>
</div>
<div className="p-3 bg-gray-900/50 rounded-lg text-center">
<div className="text-lg font-bold text-white">{stats.profitFactor.toFixed(2)}</div>
<div className="text-xs text-gray-500">Profit Factor</div>
</div>
<div className="p-3 bg-gray-900/50 rounded-lg text-center">
<div className="text-lg font-bold text-white">
<span className="text-green-400">{stats.tpHit}</span>
<span className="text-gray-600">/</span>
<span className="text-red-400">{stats.slHit}</span>
</div>
<div className="text-xs text-gray-500">TP/SL Hits</div>
</div>
</div>
{/* Filters */}
{!compact && (
<div className="flex flex-wrap items-center gap-2 mb-4">
<div className="relative flex-1 min-w-[150px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
<input
type="text"
value={filterSymbol}
onChange={(e) => 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"
/>
</div>
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value as SignalStatus | 'all')}
className="px-3 py-1.5 bg-gray-900 border border-gray-700 rounded text-sm text-white"
>
<option value="all">All Status</option>
<option value="tp_hit">TP Hit</option>
<option value="sl_hit">SL Hit</option>
<option value="active">Active</option>
<option value="expired">Expired</option>
</select>
<select
value={filterDirection}
onChange={(e) => setFilterDirection(e.target.value as 'BUY' | 'SELL' | 'all')}
className="px-3 py-1.5 bg-gray-900 border border-gray-700 rounded text-sm text-white"
>
<option value="all">All Directions</option>
<option value="BUY">Buy Only</option>
<option value="SELL">Sell Only</option>
</select>
<select
value={`${sortBy}-${sortDir}`}
onChange={(e) => {
const [field, dir] = e.target.value.split('-');
setSortBy(field as 'date' | 'profit' | 'confidence');
setSortDir(dir as 'asc' | 'desc');
}}
className="px-3 py-1.5 bg-gray-900 border border-gray-700 rounded text-sm text-white"
>
<option value="date-desc">Newest First</option>
<option value="date-asc">Oldest First</option>
<option value="profit-desc">Highest Profit</option>
<option value="profit-asc">Lowest Profit</option>
<option value="confidence-desc">Highest Confidence</option>
</select>
</div>
)}
{/* Signals List */}
{filteredSignals.length === 0 ? (
<div className="text-center py-8">
<History className="w-10 h-10 text-gray-600 mx-auto mb-2" />
<p className="text-gray-400">No signals found</p>
</div>
) : (
<div className="space-y-2 max-h-[400px] overflow-y-auto">
{filteredSignals.map((signal) => {
const isExpanded = expandedId === signal.id;
return (
<div
key={signal.id}
onClick={() => {
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 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`p-1.5 rounded ${signal.direction === 'BUY' ? 'bg-green-500/20' : 'bg-red-500/20'}`}>
{signal.direction === 'BUY' ? (
<TrendingUp className="w-4 h-4 text-green-400" />
) : (
<TrendingDown className="w-4 h-4 text-red-400" />
)}
</div>
<div>
<div className="flex items-center gap-2">
<span className="font-medium text-white">{signal.symbol}</span>
<span className={`px-2 py-0.5 rounded text-xs ${
signal.status === 'tp_hit'
? 'bg-green-500/20 text-green-400'
: signal.status === 'sl_hit'
? 'bg-red-500/20 text-red-400'
: signal.status === 'active'
? 'bg-blue-500/20 text-blue-400'
: 'bg-gray-500/20 text-gray-400'
}`}>
{getStatusLabel(signal.status)}
</span>
</div>
<div className="text-xs text-gray-500">
{formatDate(signal.createdAt)} {signal.modelName}
</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="text-right">
{signal.actualProfit !== undefined ? (
<div className={`font-mono font-medium ${signal.actualProfit >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{signal.actualProfit >= 0 ? '+' : ''}${signal.actualProfit.toFixed(2)}
</div>
) : (
<div className="font-mono text-gray-400">--</div>
)}
<div className="text-xs text-gray-500">
Conf: {signal.confidence.toFixed(0)}%
</div>
</div>
{isExpanded ? (
<ChevronUp className="w-4 h-4 text-gray-500" />
) : (
<ChevronDown className="w-4 h-4 text-gray-500" />
)}
</div>
</div>
{/* Expanded Details */}
{isExpanded && (
<div className="mt-3 pt-3 border-t border-gray-700/50">
<div className="grid grid-cols-3 gap-3 text-xs mb-3">
<div>
<span className="text-gray-500">Entry:</span>
<span className="text-white ml-2 font-mono">{signal.entryPrice.toFixed(5)}</span>
</div>
<div>
<span className="text-gray-500">SL:</span>
<span className="text-red-400 ml-2 font-mono">{signal.stopLoss.toFixed(5)}</span>
</div>
<div>
<span className="text-gray-500">TP:</span>
<span className="text-green-400 ml-2 font-mono">{signal.takeProfit.toFixed(5)}</span>
</div>
</div>
<div className="flex items-center gap-4 text-xs">
<div>
<span className="text-gray-500">Expected RR:</span>
<span className="text-white ml-2">1:{signal.expectedRR.toFixed(2)}</span>
</div>
{signal.actualRR && (
<div>
<span className="text-gray-500">Actual RR:</span>
<span className={`ml-2 ${signal.actualRR >= 0 ? 'text-green-400' : 'text-red-400'}`}>
1:{Math.abs(signal.actualRR).toFixed(2)}
</span>
</div>
)}
</div>
{signal.reasoning && (
<div className="mt-2 p-2 bg-gray-800/50 rounded text-xs text-gray-400">
<strong>Reasoning:</strong> {signal.reasoning}
</div>
)}
</div>
)}
</div>
);
})}
</div>
)}
</div>
);
};
export default SignalPerformanceTracker;

View File

@ -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';