| |
|
| | import React, { useState, useEffect } from 'react'; |
| | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; |
| | import { Alert, AlertDescription } from "@/components/ui/alert"; |
| | import { Badge } from "@/components/ui/badge"; |
| | import { Progress } from "@/components/ui/progress"; |
| | import { CheckCircle, AlertTriangle, Clock, ArrowRight, RefreshCw } from 'lucide-react'; |
| |
|
| | interface TrainingMonitorProps { |
| | type: string; |
| | schedule: string; |
| | loading: boolean; |
| | engine: 'AI' | 'Crypto'; |
| | } |
| |
|
| | const TrainingMonitor: React.FC<TrainingMonitorProps> = ({ type, schedule, loading, engine }) => { |
| | |
| | const [trainingState, setTrainingState] = useState<{ |
| | status: 'running' | 'completed' | 'validating' | 'deploying' | 'idle'; |
| | progress: number; |
| | currentStep: number; |
| | lastRun: string; |
| | nextRun: string; |
| | metrics: Record<string, number>; |
| | alerts: { message: string; type: 'warning' | 'error' | 'success' }[]; |
| | }>({ |
| | status: 'idle', |
| | progress: 0, |
| | currentStep: 0, |
| | lastRun: '2023-05-08 14:30', |
| | nextRun: '2023-05-09 14:30', |
| | metrics: engine === 'AI' ? |
| | { accuracy: 96.7, precision: 94.3, recall: 95.8, drift: -0.5 } : |
| | { sharpeRatio: 1.32, maxDrawdown: 3.8, winRate: 68.5, profitFactor: 1.45 }, |
| | alerts: [] |
| | }); |
| |
|
| | |
| | useEffect(() => { |
| | if (loading) return; |
| | |
| | const startTraining = () => { |
| | |
| | setTrainingState(prev => ({ |
| | ...prev, |
| | status: 'running', |
| | progress: 0, |
| | currentStep: 1, |
| | alerts: [] |
| | })); |
| | |
| | |
| | const timer1 = setTimeout(() => { |
| | setTrainingState(prev => ({ |
| | ...prev, |
| | progress: 25, |
| | currentStep: 2 |
| | })); |
| | |
| | |
| | const timer2 = setTimeout(() => { |
| | setTrainingState(prev => ({ |
| | ...prev, |
| | progress: 70, |
| | currentStep: 3 |
| | })); |
| | |
| | |
| | const timer3 = setTimeout(() => { |
| | setTrainingState(prev => ({ |
| | ...prev, |
| | status: 'validating', |
| | progress: 85, |
| | currentStep: 4, |
| | alerts: Math.random() > 0.8 ? |
| | [...prev.alerts, { |
| | message: engine === 'AI' ? |
| | 'Accuracy below threshold (94.8%). Rolling back.' : |
| | 'Sharpe ratio below threshold (0.95). Rolling back.', |
| | type: 'error' |
| | }] : prev.alerts |
| | })); |
| | |
| | |
| | const timer4 = setTimeout(() => { |
| | const success = Math.random() > 0.2; |
| | setTrainingState(prev => ({ |
| | ...prev, |
| | status: 'completed', |
| | progress: 100, |
| | currentStep: 0, |
| | lastRun: new Date().toLocaleString(), |
| | nextRun: new Date(Date.now() + (engine === 'AI' ? 24 : 1) * 60 * 60 * 1000).toLocaleString(), |
| | metrics: engine === 'AI' ? |
| | { |
| | accuracy: success ? 96.7 + (Math.random() * 0.6 - 0.3) : 94.8, |
| | precision: success ? 94.3 + (Math.random() * 0.6 - 0.3) : 93.1, |
| | recall: success ? 95.8 + (Math.random() * 0.6 - 0.3) : 94.5, |
| | drift: success ? -0.5 + (Math.random() * 0.4 - 0.2) : -1.2 |
| | } : |
| | { |
| | sharpeRatio: success ? 1.32 + (Math.random() * 0.2 - 0.1) : 0.95, |
| | maxDrawdown: success ? 3.8 + (Math.random() * 0.6 - 0.3) : 5.2, |
| | winRate: success ? 68.5 + (Math.random() * 1 - 0.5) : 65.2, |
| | profitFactor: success ? 1.45 + (Math.random() * 0.1 - 0.05) : 1.2 |
| | }, |
| | alerts: success ? prev.alerts : [ |
| | ...prev.alerts, |
| | { |
| | message: success ? |
| | 'Model deployed successfully to production endpoint.' : |
| | 'Validation failed. Rolled back to previous stable version.', |
| | type: success ? 'success' : 'error' |
| | } |
| | ] |
| | })); |
| | }, 3000); |
| | |
| | return () => clearTimeout(timer4); |
| | }, 2000); |
| | |
| | return () => clearTimeout(timer3); |
| | }, 4000); |
| | |
| | return () => clearTimeout(timer2); |
| | }, 2000); |
| | |
| | return () => clearTimeout(timer1); |
| | }; |
| | |
| | |
| | const initialTimer = setTimeout(() => { |
| | startTraining(); |
| | }, 2000); |
| | |
| | |
| | const recurringTimer = setInterval(() => { |
| | if (trainingState.status === 'idle') { |
| | startTraining(); |
| | } |
| | }, 15000); |
| | |
| | return () => { |
| | clearTimeout(initialTimer); |
| | clearInterval(recurringTimer); |
| | }; |
| | }, [loading, engine]); |
| |
|
| | |
| | useEffect(() => { |
| | if (trainingState.status === 'completed') { |
| | const timer = setTimeout(() => { |
| | setTrainingState(prev => ({ |
| | ...prev, |
| | status: 'idle' |
| | })); |
| | }, 5000); |
| | |
| | return () => clearTimeout(timer); |
| | } |
| | }, [trainingState.status]); |
| |
|
| | const steps = engine === 'AI' ? [ |
| | "Export new rows since last run", |
| | "Retrain AI model using in-db training", |
| | "Validate: ≥95% accuracy & no negative drift", |
| | "Deploy to /api/mes/predict endpoint", |
| | "Log metrics to monitoring DB" |
| | ] : [ |
| | "Fetch latest minute-level features", |
| | "Retrain RL agent with TD3 algorithm", |
| | "Backtest on last 7 days (Sharpe ≥1.0)", |
| | "Deploy updated policy to trading service", |
| | "Enforce risk constraints (drawdown, position)" |
| | ]; |
| | |
| | return ( |
| | <div className="space-y-6"> |
| | <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> |
| | <Card> |
| | <CardHeader className="pb-2"> |
| | <div className="flex justify-between items-center"> |
| | <CardTitle className="text-xl"> |
| | {engine === 'AI' ? 'MES Predictor' : 'Crypto Trader'} Training |
| | </CardTitle> |
| | <Badge className={trainingState.status === 'running' || trainingState.status === 'validating' ? |
| | 'bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-300' : |
| | trainingState.status === 'completed' ? |
| | 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300' : |
| | 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-300'}> |
| | {trainingState.status === 'running' ? 'Training' : |
| | trainingState.status === 'validating' ? 'Validating' : |
| | trainingState.status === 'completed' ? 'Completed' : 'Idle'} |
| | </Badge> |
| | </div> |
| | <CardDescription> |
| | Schedule: Every {schedule} |
| | </CardDescription> |
| | </CardHeader> |
| | <CardContent> |
| | {loading ? ( |
| | <div className="space-y-4"> |
| | <div className="animate-pulse h-4 bg-gray-200 dark:bg-factory-blue-light rounded w-full mb-2"></div> |
| | <div className="space-y-2"> |
| | {Array(5).fill(0).map((_, idx) => ( |
| | <div key={idx} className="animate-pulse h-5 bg-gray-200 dark:bg-factory-blue-light rounded w-full"></div> |
| | ))} |
| | </div> |
| | </div> |
| | ) : ( |
| | <> |
| | <div className="mb-4"> |
| | <div className="flex justify-between text-xs mb-1"> |
| | <span>Training Progress</span> |
| | <span>{trainingState.progress}%</span> |
| | </div> |
| | <Progress value={trainingState.progress} className="h-2" /> |
| | </div> |
| | |
| | <div className="mb-4"> |
| | <div className="text-sm font-medium mb-2">Training Pipeline Steps:</div> |
| | <div className="space-y-2"> |
| | {steps.map((step, idx) => ( |
| | <div |
| | key={idx} |
| | className={`flex items-start p-2 rounded-sm text-sm ${ |
| | trainingState.currentStep === idx + 1 |
| | ? 'bg-blue-100 dark:bg-blue-900/20 border-l-4 border-blue-500' |
| | : trainingState.currentStep > idx + 1 || (trainingState.status === 'completed' && trainingState.progress === 100) |
| | ? 'text-gray-500 dark:text-gray-400' |
| | : '' |
| | }`} |
| | > |
| | <div className="mr-3 mt-0.5"> |
| | {trainingState.currentStep > idx + 1 || (trainingState.status === 'completed' && trainingState.progress === 100) ? ( |
| | <CheckCircle className="h-4 w-4 text-green-500" /> |
| | ) : trainingState.currentStep === idx + 1 ? ( |
| | <RefreshCw className="h-4 w-4 text-blue-500 animate-spin" /> |
| | ) : ( |
| | <div className="h-4 w-4 rounded-full border border-gray-300 dark:border-gray-600"></div> |
| | )} |
| | </div> |
| | <div>{step}</div> |
| | </div> |
| | ))} |
| | </div> |
| | </div> |
| | |
| | <div className="grid grid-cols-2 gap-2 text-sm"> |
| | <div className="flex items-center"> |
| | <Clock className="h-4 w-4 mr-2 text-gray-500 dark:text-gray-400" /> |
| | <span className="text-gray-500 dark:text-gray-400">Last run:</span> |
| | <span className="ml-1">{trainingState.lastRun}</span> |
| | </div> |
| | <div className="flex items-center"> |
| | <Clock className="h-4 w-4 mr-2 text-gray-500 dark:text-gray-400" /> |
| | <span className="text-gray-500 dark:text-gray-400">Next run:</span> |
| | <span className="ml-1">{trainingState.nextRun}</span> |
| | </div> |
| | </div> |
| | </> |
| | )} |
| | </CardContent> |
| | </Card> |
| | |
| | <Card> |
| | <CardHeader> |
| | <CardTitle className="text-xl">Performance Metrics</CardTitle> |
| | <CardDescription> |
| | Latest model evaluation results |
| | </CardDescription> |
| | </CardHeader> |
| | <CardContent> |
| | {loading ? ( |
| | <div className="grid grid-cols-2 gap-4"> |
| | {Array(4).fill(0).map((_, idx) => ( |
| | <div key={idx} className="animate-pulse space-y-2"> |
| | <div className="h-4 bg-gray-200 dark:bg-factory-blue-light rounded w-20"></div> |
| | <div className="h-6 bg-gray-200 dark:bg-factory-blue-light rounded w-16"></div> |
| | </div> |
| | ))} |
| | </div> |
| | ) : ( |
| | <> |
| | <div className="grid grid-cols-2 gap-y-6 gap-x-4"> |
| | {Object.entries(trainingState.metrics).map(([key, value], idx) => ( |
| | <div key={key}> |
| | <div className="text-sm text-gray-500 dark:text-gray-400 capitalize mb-1"> |
| | {key === 'sharpeRatio' ? 'Sharpe Ratio' : |
| | key === 'maxDrawdown' ? 'Max Drawdown' : |
| | key === 'winRate' ? 'Win Rate' : |
| | key === 'profitFactor' ? 'Profit Factor' : key} |
| | </div> |
| | <div className="text-2xl font-semibold"> |
| | {typeof value === 'number' ? |
| | key === 'maxDrawdown' ? `${value}%` : |
| | key === 'winRate' ? `${value}%` : |
| | key === 'drift' ? `${value > 0 ? '+' : ''}${value}%` : |
| | key === 'accuracy' || key === 'precision' || key === 'recall' ? `${value}%` : |
| | value.toFixed(2) |
| | : value} |
| | |
| | {key === 'accuracy' && value < 95 && ( |
| | <span className="text-sm text-red-500 ml-2">Below threshold</span> |
| | )} |
| | {key === 'sharpeRatio' && value < 1.0 && ( |
| | <span className="text-sm text-red-500 ml-2">Below threshold</span> |
| | )} |
| | </div> |
| | </div> |
| | ))} |
| | </div> |
| | |
| | <div className="mt-6 border-t dark:border-factory-blue-light pt-4"> |
| | <div className="text-sm font-medium mb-2">Threshold Requirements:</div> |
| | <div className="text-sm"> |
| | {engine === 'AI' ? ( |
| | <div className="flex items-center"> |
| | <ArrowRight className="h-4 w-4 mr-2" /> |
| | <span>Accuracy ≥ 95%, No negative drift over 1%</span> |
| | </div> |
| | ) : ( |
| | <div className="flex items-center"> |
| | <ArrowRight className="h-4 w-4 mr-2" /> |
| | <span>Sharpe Ratio ≥ 1.0, Max drawdown ≤ 5%</span> |
| | </div> |
| | )} |
| | </div> |
| | </div> |
| | </> |
| | )} |
| | </CardContent> |
| | </Card> |
| | </div> |
| | |
| | {trainingState.alerts.length > 0 && !loading && ( |
| | <div className="space-y-3"> |
| | {trainingState.alerts.map((alert, idx) => ( |
| | <Alert |
| | key={idx} |
| | className={alert.type === 'error' ? 'border-red-500 dark:border-red-700 bg-red-50 dark:bg-red-900/20' : |
| | alert.type === 'warning' ? 'border-yellow-500 dark:border-yellow-700 bg-yellow-50 dark:bg-yellow-900/20' : |
| | 'border-green-500 dark:border-green-700 bg-green-50 dark:bg-green-900/20'} |
| | > |
| | <AlertDescription className="flex items-center"> |
| | {alert.type === 'error' ? ( |
| | <AlertTriangle className="h-4 w-4 mr-2 text-red-500 dark:text-red-400" /> |
| | ) : alert.type === 'warning' ? ( |
| | <AlertTriangle className="h-4 w-4 mr-2 text-yellow-500 dark:text-yellow-400" /> |
| | ) : ( |
| | <CheckCircle className="h-4 w-4 mr-2 text-green-500 dark:text-green-400" /> |
| | )} |
| | {alert.message} |
| | </AlertDescription> |
| | </Alert> |
| | ))} |
| | </div> |
| | )} |
| | </div> |
| | ); |
| | }; |
| |
|
| | export default TrainingMonitor; |
| |
|