stock-bot/apps/stock/web-app/src/features/backtest/BacktestListPage.tsx

168 lines
No EOL
6.6 KiB
TypeScript

import { useEffect, useState } from 'react';
import { useBacktestList } from './hooks/useBacktest';
import { Link, useNavigate } from 'react-router-dom';
import {
PlusIcon,
PlayIcon,
CheckCircleIcon,
XCircleIcon,
ClockIcon,
ExclamationTriangleIcon
} from '@heroicons/react/24/solid';
export function BacktestListPage() {
const { backtests, isLoading, error, loadBacktests } = useBacktestList();
const navigate = useNavigate();
const [refreshInterval, setRefreshInterval] = useState<NodeJS.Timeout | null>(null);
useEffect(() => {
loadBacktests();
// Refresh every 5 seconds if there are running backtests
const interval = setInterval(() => {
if (backtests.some(b => b.status === 'running' || b.status === 'pending')) {
loadBacktests();
}
}, 5000);
setRefreshInterval(interval);
return () => {
if (refreshInterval) {
clearInterval(refreshInterval);
}
};
}, [loadBacktests]);
const getStatusIcon = (status: string) => {
switch (status) {
case 'completed':
return <CheckCircleIcon className="w-4 h-4 text-success" />;
case 'running':
return <PlayIcon className="w-4 h-4 text-primary-400 animate-pulse" />;
case 'pending':
return <ClockIcon className="w-4 h-4 text-text-secondary" />;
case 'failed':
return <XCircleIcon className="w-4 h-4 text-error" />;
case 'cancelled':
return <ExclamationTriangleIcon className="w-4 h-4 text-text-muted" />;
default:
return <ClockIcon className="w-4 h-4 text-text-secondary" />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed':
return 'text-success';
case 'running':
return 'text-primary-400';
case 'failed':
return 'text-error';
case 'cancelled':
return 'text-text-muted';
default:
return 'text-text-secondary';
}
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString();
};
return (
<div className="flex flex-col h-full space-y-6">
<div className="flex-shrink-0 flex justify-between items-center">
<div>
<h1 className="text-lg font-bold text-text-primary mb-2">Backtest History</h1>
<p className="text-text-secondary text-sm">
View and manage your backtest runs
</p>
</div>
<button
onClick={() => navigate('/backtests/new')}
className="px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors flex items-center space-x-2"
>
<PlusIcon className="w-4 h-4" />
<span>New Backtest</span>
</button>
</div>
{error && (
<div className="bg-error/10 border border-error text-error px-4 py-3 rounded-lg text-sm">
{error}
</div>
)}
{isLoading ? (
<div className="flex items-center justify-center h-64">
<div className="text-text-secondary">Loading backtests...</div>
</div>
) : backtests.length === 0 ? (
<div className="bg-surface-secondary p-8 rounded-lg border border-border text-center">
<p className="text-text-secondary mb-4">No backtests found</p>
<button
onClick={() => navigate('/backtests/new')}
className="inline-flex items-center space-x-2 px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors"
>
<PlusIcon className="w-4 h-4" />
<span>Create Your First Backtest</span>
</button>
</div>
) : (
<div className="bg-surface-secondary rounded-lg border border-border overflow-hidden">
<table className="w-full">
<thead className="bg-background border-b border-border">
<tr>
<th className="px-4 py-3 text-left text-sm font-medium text-text-secondary">ID</th>
<th className="px-4 py-3 text-left text-sm font-medium text-text-secondary">Strategy</th>
<th className="px-4 py-3 text-left text-sm font-medium text-text-secondary">Symbols</th>
<th className="px-4 py-3 text-left text-sm font-medium text-text-secondary">Period</th>
<th className="px-4 py-3 text-left text-sm font-medium text-text-secondary">Status</th>
<th className="px-4 py-3 text-left text-sm font-medium text-text-secondary">Created</th>
<th className="px-4 py-3 text-left text-sm font-medium text-text-secondary">Actions</th>
</tr>
</thead>
<tbody>
{backtests.map((backtest) => (
<tr key={backtest.id} className="border-b border-border hover:bg-background/50 transition-colors">
<td className="px-4 py-3 text-sm text-text-primary font-mono">
{backtest.id.slice(0, 8)}...
</td>
<td className="px-4 py-3 text-sm text-text-primary capitalize">
{backtest.strategy}
</td>
<td className="px-4 py-3 text-sm text-text-primary">
{backtest.symbols.join(', ')}
</td>
<td className="px-4 py-3 text-sm text-text-primary">
{new Date(backtest.startDate).toLocaleDateString()} - {new Date(backtest.endDate).toLocaleDateString()}
</td>
<td className="px-4 py-3">
<div className="flex items-center space-x-2">
{getStatusIcon(backtest.status)}
<span className={`text-sm font-medium capitalize ${getStatusColor(backtest.status)}`}>
{backtest.status}
</span>
</div>
</td>
<td className="px-4 py-3 text-sm text-text-secondary">
{formatDate(backtest.createdAt)}
</td>
<td className="px-4 py-3 text-sm">
<Link
to={`/backtests/${backtest.id}`}
className="text-primary-400 hover:text-primary-300 font-medium"
>
View
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}