[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:
parent
cbb6637966
commit
e9aa29fccd
432
src/modules/ml/components/BacktestResultsVisualization.tsx
Normal file
432
src/modules/ml/components/BacktestResultsVisualization.tsx
Normal 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;
|
||||||
302
src/modules/ml/components/ConfidenceMeter.tsx
Normal file
302
src/modules/ml/components/ConfidenceMeter.tsx
Normal 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;
|
||||||
410
src/modules/ml/components/ModelAccuracyDashboard.tsx
Normal file
410
src/modules/ml/components/ModelAccuracyDashboard.tsx
Normal 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;
|
||||||
412
src/modules/ml/components/SignalPerformanceTracker.tsx
Normal file
412
src/modules/ml/components/SignalPerformanceTracker.tsx
Normal 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;
|
||||||
@ -10,3 +10,13 @@ export { AccuracyMetrics } from './AccuracyMetrics';
|
|||||||
export { ICTAnalysisCard } from './ICTAnalysisCard';
|
export { ICTAnalysisCard } from './ICTAnalysisCard';
|
||||||
export { EnsembleSignalCard } from './EnsembleSignalCard';
|
export { EnsembleSignalCard } from './EnsembleSignalCard';
|
||||||
export { TradeExecutionModal } from './TradeExecutionModal';
|
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';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user