import { useState, useEffect } from 'react' import { Table, Card, Button, Select, Tag, Space, Modal, message, Row, Col, Form, Input, InputNumber, Switch } from 'antd' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { PlusOutlined, ReloadOutlined, DeleteOutlined, StopOutlined, EyeOutlined, RedoOutlined, CopyOutlined } from '@ant-design/icons' import { formatUSDC } from '../utils' import { backtestService, apiService } from '../services/api' import type { BacktestTaskDto, BacktestListRequest, BacktestCreateRequest } from '../types/backtest' import type { Leader } from '../types' import { useMediaQuery } from 'react-responsive' import AddCopyTradingModal from './CopyTradingOrders/AddModal' const { Option } = Select const BacktestList: React.FC = () => { const { t } = useTranslation() const navigate = useNavigate() const isMobile = useMediaQuery({ maxWidth: 768 }) const [loading, setLoading] = useState(false) const [tasks, setTasks] = useState([]) const [total, setTotal] = useState(0) const [page, setPage] = useState(1) const [size] = useState(10) const [statusFilter, setStatusFilter] = useState() const [leaderIdFilter] = useState() const [sortBy, setSortBy] = useState<'profitAmount' | 'profitRate' | 'createdAt'>('createdAt') const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc') // 创建回测 modal 相关状态 const [createModalVisible, setCreateModalVisible] = useState(false) const [createForm] = Form.useForm() const [createLoading, setCreateLoading] = useState(false) const [leaders, setLeaders] = useState([]) const [copyMode, setCopyMode] = useState<'RATIO' | 'FIXED'>('RATIO') // 创建跟单配置 Modal const [addCopyTradingModalVisible, setAddCopyTradingModalVisible] = useState(false) const [preFilledConfig, setPreFilledConfig] = useState(null) // 获取回测任务列表 const fetchTasks = async () => { setLoading(true) try { const request: BacktestListRequest = { leaderId: leaderIdFilter, status: statusFilter as any, sortBy, sortOrder, page, size } const response = await backtestService.list(request) if (response.data.code === 0 && response.data.data) { setTasks(response.data.data.list) setTotal(response.data.data.total) } else { message.error(response.data.msg || t('backtest.fetchTasksFailed')) } } catch (error) { console.error('Failed to fetch backtest tasks:', error) message.error(t('backtest.fetchTasksFailed')) } finally { setLoading(false) } } useEffect(() => { fetchTasks() }, [page, statusFilter, leaderIdFilter, sortBy, sortOrder]) // 刷新 const handleRefresh = () => { fetchTasks() } // 删除任务 const handleDelete = (id: number) => { Modal.confirm({ title: t('backtest.deleteConfirm'), okText: t('common.confirm'), cancelText: t('common.cancel'), onOk: async () => { try { const response = await backtestService.delete({ id }) if (response.data.code === 0) { message.success(t('backtest.deleteSuccess')) fetchTasks() } else { message.error(response.data.msg || t('backtest.deleteFailed')) } } catch (error) { console.error('Failed to delete backtest task:', error) message.error(t('backtest.deleteFailed')) } } }) } // 停止任务 const handleStop = (id: number) => { Modal.confirm({ title: t('backtest.stopConfirm'), okText: t('common.confirm'), cancelText: t('common.cancel'), onOk: async () => { try { const response = await backtestService.stop({ id }) if (response.data.code === 0) { message.success(t('backtest.stopSuccess')) fetchTasks() } else { message.error(response.data.msg || t('backtest.stopFailed')) } } catch (error) { console.error('Failed to stop backtest task:', error) message.error(t('backtest.stopFailed')) } } }) } // 重试任务 const handleRetry = (id: number) => { Modal.confirm({ title: t('backtest.retryConfirm'), okText: t('common.confirm'), cancelText: t('common.cancel'), onOk: async () => { try { const response = await backtestService.retry({ id }) if (response.data.code === 0) { message.success(t('backtest.retrySuccess')) fetchTasks() } else { message.error(response.data.msg || t('backtest.retryFailed')) } } catch (error) { console.error('Failed to retry backtest task:', error) message.error(t('backtest.retryFailed')) } } }) } // 获取 Leader 列表 useEffect(() => { if (createModalVisible) { const fetchLeaders = async () => { try { const response = await apiService.leaders.list({}) if (response.data.code === 0 && response.data.data) { setLeaders(response.data.data.list || []) } } catch (error) { console.error('Failed to fetch leaders:', error) } } fetchLeaders() } }, [createModalVisible]) // 查看详情 const handleViewDetail = (id: number) => { navigate(`/backtest/detail/${id}`) } // 打开创建 modal const handleCreate = () => { setCreateModalVisible(true) createForm.resetFields() createForm.setFieldsValue({ copyMode: 'RATIO', copyRatio: 100, // 默认 100%(显示为百分比) maxOrderSize: 1000, minOrderSize: 1, maxDailyLoss: 500, maxDailyOrders: 50, supportSell: true, keywordFilterMode: 'DISABLED', backtestDays: 7 }) setCopyMode('RATIO') } // 提交创建回测任务 const handleCreateSubmit = async () => { try { const values = await createForm.validateFields() setCreateLoading(true) const request: BacktestCreateRequest = { taskName: values.taskName, leaderId: values.leaderId, initialBalance: values.initialBalance, backtestDays: values.backtestDays, copyMode: values.copyMode || 'RATIO', copyRatio: values.copyMode === 'RATIO' && values.copyRatio ? (values.copyRatio / 100).toString() : undefined, fixedAmount: values.copyMode === 'FIXED' ? values.fixedAmount : undefined, maxOrderSize: values.maxOrderSize, minOrderSize: values.minOrderSize, maxDailyLoss: values.maxDailyLoss, maxDailyOrders: values.maxDailyOrders, supportSell: values.supportSell, keywordFilterMode: values.keywordFilterMode, keywords: values.keywords } const response = await backtestService.create(request) if (response.data.code === 0) { message.success(t('backtest.createSuccess')) setCreateModalVisible(false) createForm.resetFields() fetchTasks() } else { message.error(response.data.msg || t('backtest.createFailed')) } } catch (error) { console.error('Failed to create backtest task:', error) message.error(t('backtest.createFailed')) } finally { setCreateLoading(false) } } // 一键创建跟单配置 const handleCreateCopyTrading = async (task: BacktestTaskDto) => { console.log('[BacktestList] handleCreateCopyTrading called, task:', task) try { // 先获取任务详情以获取配置信息 const response = await backtestService.detail({ id: task.id }) if (response.data.code === 0 && response.data.data) { const taskDetail = response.data.data.task const taskConfig = response.data.data.config console.log('[BacktestList] Fetched task detail, config:', taskConfig) if (!taskConfig) { message.error(t('backtest.fetchTaskDetailFailed') || '获取任务配置失败') return } // 预填充回测任务的配置参数(从 config 中获取) const preFilled = { leaderId: taskDetail.leaderId, copyMode: taskConfig.copyMode, copyRatio: taskConfig.copyMode === 'RATIO' ? parseFloat(taskConfig.copyRatio) * 100 : undefined, fixedAmount: taskConfig.copyMode === 'FIXED' ? taskConfig.fixedAmount : undefined, maxOrderSize: parseFloat(taskConfig.maxOrderSize), minOrderSize: parseFloat(taskConfig.minOrderSize), maxDailyLoss: parseFloat(taskConfig.maxDailyLoss), maxDailyOrders: taskConfig.maxDailyOrders, supportSell: taskConfig.supportSell, keywordFilterMode: taskConfig.keywordFilterMode || 'DISABLED', keywords: taskConfig.keywords || [], configName: `回测任务-${taskDetail.taskName}` } console.log('[BacktestList] Generated preFilled config:', preFilled) console.log('[BacktestList] Setting preFilledConfig and opening modal') setPreFilledConfig(preFilled) setAddCopyTradingModalVisible(true) } else { message.error(response.data.msg || t('backtest.fetchTaskDetailFailed') || '获取任务详情失败') } } catch (error) { console.error('[BacktestList] Failed to fetch task detail:', error) message.error(t('backtest.fetchTaskDetailFailed') || '获取任务详情失败') } } // 状态标签颜色 const getStatusColor = (status: string) => { switch (status) { case 'PENDING': return 'blue' case 'RUNNING': return 'processing' case 'COMPLETED': return 'success' case 'STOPPED': return 'warning' case 'FAILED': return 'error' default: return 'default' } } // 状态标签文本 const getStatusText = (status: string) => { switch (status) { case 'PENDING': return t('backtest.statusPending') case 'RUNNING': return t('backtest.statusRunning') case 'COMPLETED': return t('backtest.statusCompleted') case 'STOPPED': return t('backtest.statusStopped') case 'FAILED': return t('backtest.statusFailed') default: return status } } const columns = [ { title: t('backtest.taskName'), dataIndex: 'taskName', key: 'taskName', width: isMobile ? 120 : 150 }, { title: t('backtest.leader'), dataIndex: 'leaderName', key: 'leaderName', width: isMobile ? 100 : 150, render: (_: any, record: BacktestTaskDto) => record.leaderName || record.leaderAddress?.substring(0, 8) + '...' || '-' }, { title: t('backtest.initialBalance'), dataIndex: 'initialBalance', key: 'initialBalance', width: 120, render: (value: string) => formatUSDC(value) }, { title: t('backtest.finalBalance'), dataIndex: 'finalBalance', key: 'finalBalance', width: 120, render: (value: string | null) => value ? formatUSDC(value) : '-' }, { title: t('backtest.profitAmount'), dataIndex: 'profitAmount', key: 'profitAmount', width: 120, render: (value: string | null) => value ? ( = 0 ? '#52c41a' : '#ff4d4f' }}> {formatUSDC(value)} ) : '-' }, { title: t('backtest.profitRate'), dataIndex: 'profitRate', key: 'profitRate', width: 100, render: (value: string | null) => value ? ( = 0 ? '#52c41a' : '#ff4d4f' }}> {value}% ) : '-' }, { title: t('backtest.backtestDays'), dataIndex: 'backtestDays', key: 'backtestDays', width: 100, render: (value: number) => `${value} ${t('common.day')}` }, { title: t('backtest.status'), dataIndex: 'status', key: 'status', width: 100, render: (status: string) => ( {getStatusText(status)} ) }, { title: t('backtest.progress'), dataIndex: 'progress', key: 'progress', width: 120, render: (progress: number) => (
{progress}%
) }, { title: t('backtest.totalTrades'), dataIndex: 'totalTrades', key: 'totalTrades', width: 100 }, { title: t('backtest.createdAt'), dataIndex: 'createdAt', key: 'createdAt', width: isMobile ? 150 : 180, render: (timestamp: number) => new Date(timestamp).toLocaleString() }, { title: t('common.actions'), key: 'actions', fixed: isMobile ? false : ('right' as const), width: isMobile ? 100 : 150, render: (_: any, record: BacktestTaskDto) => ( {record.status === 'COMPLETED' && ( )} {record.status === 'RUNNING' && ( )} {(record.status === 'STOPPED' || record.status === 'FAILED') && ( )} {(record.status === 'PENDING' || record.status === 'COMPLETED') && ( )} ) } ] return (
{/* 头部操作栏 */} {/* 数据表格 */} `${t('common.total')} ${total} ${t('common.items')}`, onChange: (newPage) => setPage(newPage), simple: isMobile }} scroll={isMobile ? { x: 1200 } : { x: 1400 }} /> {/* 创建回测任务 Modal */} { setCreateModalVisible(false) createForm.resetFields() }} onOk={handleCreateSubmit} okText={t('common.save')} cancelText={t('common.cancel')} width={isMobile ? '95%' : 800} confirmLoading={createLoading} destroyOnClose style={{ top: isMobile ? 10 : 20 }} bodyStyle={{ maxHeight: isMobile ? 'calc(100vh - 150px)' : 'calc(100vh - 200px)', overflowY: 'auto' }} >
{/* 跟单配置 */}

{t('backtest.config')}

{copyMode === 'RATIO' && ( { const parsed = parseFloat(value || '0') if (parsed > 10000) return 10000 return parsed }} formatter={(value) => { if (!value && value !== 0) return '' const num = parseFloat(value.toString()) if (isNaN(num)) return '' if (num > 10000) return '10000' return num.toString().replace(/\.0+$/, '') }} /> )} {copyMode === 'FIXED' && ( )}
{t('backtest.supportSellHint') || '是否跟随 Leader 卖出'}