diff --git a/frontend/src/pages/BacktestList.tsx b/frontend/src/pages/BacktestList.tsx index 1a5a13b..5154cd2 100644 --- a/frontend/src/pages/BacktestList.tsx +++ b/frontend/src/pages/BacktestList.tsx @@ -1,20 +1,19 @@ import { useState, useEffect } from 'react' -import { Table, Card, Button, Select, Tag, Space, Modal, message, Row, Col, Form, Input, InputNumber, Switch } from 'antd' +import { Table, Card, Button, Select, Tag, Space, Modal, message, Row, Col, Form, Input, InputNumber, Switch, Statistic, Descriptions } 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 { BacktestTaskDto, BacktestListRequest, BacktestCreateRequest, BacktestTradeDto } from '../types/backtest' import type { Leader } from '../types' import { useMediaQuery } from 'react-responsive' import AddCopyTradingModal from './CopyTradingOrders/AddModal' +import BacktestChart from './BacktestChart' 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([]) @@ -37,6 +36,18 @@ const BacktestList: React.FC = () => { const [addCopyTradingModalVisible, setAddCopyTradingModalVisible] = useState(false) const [preFilledConfig, setPreFilledConfig] = useState(null) + // 任务详情 Modal 相关状态 + const [detailModalVisible, setDetailModalVisible] = useState(false) + const [detailTask, setDetailTask] = useState(null) + const [detailConfig, setDetailConfig] = useState(null) + const [detailStatistics, setDetailStatistics] = useState(null) + const [detailTrades, setDetailTrades] = useState([]) + const [detailAllTrades, setDetailAllTrades] = useState([]) + const [detailTradesLoading, setDetailTradesLoading] = useState(false) + const [detailTradesTotal, setDetailTradesTotal] = useState(0) + const [detailTradesPage, setDetailTradesPage] = useState(1) + const [detailTradesSize] = useState(20) + // 获取回测任务列表 const fetchTasks = async () => { setLoading(true) @@ -159,11 +170,6 @@ const BacktestList: React.FC = () => { } }, [createModalVisible]) - // 查看详情 - const handleViewDetail = (id: number) => { - navigate(`/backtest/detail/${id}`) - } - // 打开创建 modal const handleCreate = () => { setCreateModalVisible(true) @@ -293,6 +299,135 @@ const BacktestList: React.FC = () => { } } + // 获取交易记录 + const fetchDetailTrades = async (taskId: number, page: number) => { + setDetailTradesLoading(true) + try { + const response = await backtestService.trades({ + taskId, + page, + size: detailTradesSize + }) + if (response.data.code === 0 && response.data.data) { + setDetailTrades(response.data.data.list) + setDetailTradesTotal(response.data.data.total) + } + } catch (error) { + console.error('Failed to fetch trades:', error) + } finally { + setDetailTradesLoading(false) + } + } + + // 获取所有交易记录(用于图表显示) + const fetchDetailAllTrades = async (taskId: number) => { + try { + const response = await backtestService.trades({ + taskId, + page: 1, + size: 10000 // 获取所有数据 + }) + if (response.data.code === 0 && response.data.data) { + setDetailAllTrades(response.data.data.list) + } + } catch (error) { + console.error('Failed to fetch all trades for chart:', error) + } + } + + // 查看详情 - 打开 Modal 而不是跳转页面 + const handleViewDetail = async (id: number) => { + try { + const response = await backtestService.detail({ id }) + if (response.data.code === 0 && response.data.data) { + console.log('[BacktestList] Fetched task detail:', response.data.data) + console.log('[BacktestList] Statistics:', response.data.data.statistics) + setDetailTask(response.data.data.task) + setDetailConfig(response.data.data.config) + setDetailStatistics(response.data.data.statistics) + setDetailModalVisible(true) + // 获取交易记录 + fetchDetailTrades(id, 1) + fetchDetailAllTrades(id) + } else { + message.error(response.data.msg || t('backtest.fetchTaskDetailFailed')) + } + } catch (error) { + console.error('Failed to fetch backtest task detail:', error) + message.error(t('backtest.fetchTaskDetailFailed')) + } + } + + // 交易记录表格列定义 + const tradeColumns = [ + { + title: t('backtest.tradeTime'), + dataIndex: 'tradeTime', + key: 'tradeTime', + width: 180, + render: (timestamp: number) => new Date(timestamp).toLocaleString() + }, + { + title: t('backtest.marketTitle'), + dataIndex: 'marketTitle', + key: 'marketTitle', + width: 250, + ellipsis: true + }, + { + title: t('backtest.side'), + dataIndex: 'side', + key: 'side', + width: 100, + render: (side: string) => ( + + {side === 'BUY' ? t('backtest.sideBuy') : side === 'SELL' ? t('backtest.sideSell') : t('backtest.sideSettlement')} + + ) + }, + { + title: t('backtest.outcome'), + dataIndex: 'outcome', + key: 'outcome', + width: 100 + }, + { + title: t('backtest.quantity'), + dataIndex: 'quantity', + key: 'quantity', + width: 100, + render: (value: string) => parseFloat(value).toFixed(4) + }, + { + title: t('backtest.price'), + dataIndex: 'price', + key: 'price', + width: 100, + render: (value: string) => parseFloat(value).toFixed(4) + }, + { + title: t('backtest.amount') + ' (USDC)', + dataIndex: 'amount', + key: 'amount', + width: 120, + render: (value: string) => formatUSDC(value) + }, + { + title: t('backtest.balanceAfter') + ' (USDC)', + dataIndex: 'balanceAfter', + key: 'balanceAfter', + width: 120, + render: (value: string) => formatUSDC(value) + }, + { + title: t('backtest.leaderTradeId'), + dataIndex: 'leaderTradeId', + key: 'leaderTradeId', + width: 150, + ellipsis: true + } + ] + const columns = [ { title: t('backtest.taskName'), @@ -789,6 +924,260 @@ const BacktestList: React.FC = () => { }} preFilledConfig={preFilledConfig} /> + + {/* 任务详情 Modal */} + { + setDetailModalVisible(false) + setDetailTask(null) + setDetailConfig(null) + setDetailStatistics(null) + setDetailTrades([]) + setDetailAllTrades([]) + setDetailTradesPage(1) + setDetailTradesTotal(0) + }} + footer={ + detailTask && detailTask.status === 'COMPLETED' && detailConfig ? ( + + + + ) : null + } + width={isMobile ? '95%' : '80%'} + style={{ top: isMobile ? 10 : 20 }} + styles={{ + body: { + maxHeight: 'calc(100vh - 250px)', + overflowY: 'auto', + padding: '24px' + } + }} + destroyOnClose + > + {detailTask && ( + + {/* 任务基本信息 */} + + + {detailTask.taskName} + + {detailTask.leaderName || detailTask.leaderAddress} + + + {formatUSDC(detailTask.initialBalance)} USDC + + + {detailTask.finalBalance ? formatUSDC(detailTask.finalBalance) + ' USDC' : '-'} + + + = 0 ? '#52c41a' : '#ff4d4f' }}> + {detailTask.profitAmount ? formatUSDC(detailTask.profitAmount) + ' USDC' : '-'} + + + + = 0 ? '#52c41a' : '#ff4d4f' }}> + {detailTask.profitRate ? detailTask.profitRate + '%' : '-'} + + + + {detailTask.backtestDays} {t('common.day')} + + + {getStatusText(detailTask.status)} + + + {detailTask.progress}% + + + {detailTask.totalTrades} + + + {new Date(detailTask.startTime).toLocaleString()} + + + {detailTask.endTime ? new Date(detailTask.endTime).toLocaleString() : '-'} + + + + + {/* 统计信息 */} + {detailStatistics && ( + + + + + + + + + + + + + + + + + + + = 50 ? '#52c41a' : '#ff4d4f' + }} + /> + + + + + + + + + + + {detailStatistics.avgHoldingTime && ( + + + + )} + + + )} + + {/* 配置信息 */} + {detailConfig && ( + + + + {detailConfig.copyMode === 'RATIO' + ? `${t('backtest.copyModeRatio')} ${parseFloat(detailConfig.copyRatio) * 100}%` + : `${t('backtest.copyModeFixed')} ${formatUSDC(detailConfig.fixedAmount)} USDC` + } + + + {formatUSDC(detailConfig.maxOrderSize)} USDC + + + {formatUSDC(detailConfig.minOrderSize)} USDC + + + {formatUSDC(detailConfig.maxDailyLoss)} USDC + + + {detailConfig.maxDailyOrders} + + + {detailConfig.supportSell ? t('common.yes') : t('common.no')} + + + {detailConfig.keywordFilterMode === 'WHITELIST' + ? t('backtest.keywordFilterModeWhitelist') + : detailConfig.keywordFilterMode === 'BLACKLIST' + ? t('backtest.keywordFilterModeBlacklist') + : t('backtest.keywordFilterModeDisabled')} + + {detailConfig.keywords && detailConfig.keywords.length > 0 && ( + + {detailConfig.keywords.join(', ')} + + )} + + + )} + + {/* 资金变化图表 */} + {detailAllTrades.length > 0 && ( + + + + )} + + {/* 交易记录 */} + + `${t('common.total')} ${total} ${t('common.items')}`, + onChange: (newPage) => { + setDetailTradesPage(newPage) + if (detailTask) { + fetchDetailTrades(detailTask.id, newPage) + } + } + }} + scroll={isMobile ? { x: 1200 } : { x: 1800 }} + size="small" + /> + + + )} + ) } diff --git a/frontend/src/pages/CopyTradingOrders/AddModal.tsx b/frontend/src/pages/CopyTradingOrders/AddModal.tsx index 5fcc300..b567b7c 100644 --- a/frontend/src/pages/CopyTradingOrders/AddModal.tsx +++ b/frontend/src/pages/CopyTradingOrders/AddModal.tsx @@ -78,7 +78,7 @@ const AddModal: React.FC = ({ }) return `跟单配置-${dateStr}-${timeStr}` } - + // 获取 Leader 资产信息 const fetchLeaderAssetInfo = async (leaderId: number) => { if (!leaderId) return @@ -149,7 +149,7 @@ const AddModal: React.FC = ({ fetchAccounts() fetchLeaders() fetchTemplates() - + // 如果有预填充配置,填充表单(延迟执行确保数据已加载) if (preFilledConfig) { console.log('[AddModal] preFilledConfig exists, will fill form after 100ms') @@ -161,7 +161,7 @@ const AddModal: React.FC = ({ } else { console.log('[AddModal] No preFilledConfig, using default values') // 没有预填充配置时,生成默认配置名 - const defaultConfigName = generateDefaultConfigName() + const defaultConfigName = generateDefaultConfigName() form.setFieldsValue({ configName: defaultConfigName, copyMode: 'RATIO', @@ -174,7 +174,7 @@ const AddModal: React.FC = ({ keywordFilterMode: 'DISABLED' }) setCopyMode('RATIO') - setKeywords([]) + setKeywords([]) } } else { console.log('[AddModal] Modal closed, resetting form')