Files
PolyHermes/frontend/src/pages/BacktestList.tsx
T
WrBugandClaude Opus 4.7 8ab8ae5205 style: 全局将 USDC 展示替换为 $ 符号
前端所有金额展示从 "100.00 USDC" 改为 "$100.00" 格式,
后端 Telegram 通知模板和错误提示同步更新。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 22:23:49 +08:00

1534 lines
62 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react'
import { useSearchParams } from 'react-router-dom'
import { Table, Card, Button, Select, Tag, Space, Modal, message, Row, Col, Form, Input, InputNumber, Switch, Statistic, Descriptions, List, Empty, Spin, Tooltip, Popconfirm } from 'antd'
import { useTranslation } from 'react-i18next'
import { PlusOutlined, ReloadOutlined, DeleteOutlined, StopOutlined, EyeOutlined, RedoOutlined, CopyOutlined, SyncOutlined } from '@ant-design/icons'
import { formatUSDC } from '../utils'
import { backtestService, apiService } from '../services/api'
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'
import LeaderSelect from '../components/LeaderSelect'
const BacktestList: React.FC = () => {
const { t } = useTranslation()
const [searchParams] = useSearchParams()
const isMobile = useMediaQuery({ maxWidth: 768 })
const [loading, setLoading] = useState(false)
const [tasks, setTasks] = useState<BacktestTaskDto[]>([])
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [size] = useState(10)
const [statusFilter, setStatusFilter] = useState<string | undefined>()
const [leaderIdFilter, setLeaderIdFilter] = useState<number | undefined>(() => {
const leaderIdParam = searchParams.get('leaderId')
if (leaderIdParam) {
const id = parseInt(leaderIdParam, 10)
return isNaN(id) ? undefined : id
}
return undefined
})
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<Leader[]>([])
const [copyMode, setCopyMode] = useState<'RATIO' | 'FIXED'>('RATIO')
// 创建跟单配置 Modal
const [addCopyTradingModalVisible, setAddCopyTradingModalVisible] = useState(false)
const [preFilledConfig, setPreFilledConfig] = useState<any>(null)
// 重新测试 Modal 相关状态
const [rerunModalVisible, setRerunModalVisible] = useState(false)
const [rerunTask, setRerunTask] = useState<BacktestTaskDto | null>(null)
const [rerunTaskName, setRerunTaskName] = useState('')
const [rerunLoading, setRerunLoading] = useState(false)
// 任务详情 Modal 相关状态
const [detailModalVisible, setDetailModalVisible] = useState(false)
const [detailTask, setDetailTask] = useState<BacktestTaskDto | null>(null)
const [detailConfig, setDetailConfig] = useState<any>(null)
const [detailStatistics, setDetailStatistics] = useState<any>(null)
const [detailTrades, setDetailTrades] = useState<BacktestTradeDto[]>([])
const [detailAllTrades, setDetailAllTrades] = useState<BacktestTradeDto[]>([])
const [detailTradesLoading, setDetailTradesLoading] = useState(false)
const [detailTradesTotal, setDetailTradesTotal] = useState(0)
const [detailTradesPage, setDetailTradesPage] = useState(1)
const [detailTradesSize] = useState(20)
// 获取回测任务列表(silent 为 true 时不显示 loading,用于轮询刷新)
const fetchTasks = async (silent = false) => {
if (!silent) 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 if (!silent) {
message.error(response.data.msg || t('backtest.fetchTasksFailed'))
}
} catch (error) {
console.error('Failed to fetch backtest tasks:', error)
if (!silent) message.error(t('backtest.fetchTasksFailed'))
} finally {
if (!silent) setLoading(false)
}
}
// 从 URL 读取 leaderId 并应用筛选(如从 Leader 管理页跳转过来)
useEffect(() => {
const leaderIdParam = searchParams.get('leaderId')
if (leaderIdParam) {
const id = parseInt(leaderIdParam, 10)
setLeaderIdFilter(isNaN(id) ? undefined : id)
}
}, [searchParams])
useEffect(() => {
fetchTasks()
}, [page, statusFilter, leaderIdFilter, sortBy, sortOrder])
// 存在非终态任务(PENDING/RUNNING)时每 3s 轮询刷新进度
const hasNonTerminalTask = tasks.some(
(task) => task.status === 'PENDING' || task.status === 'RUNNING'
)
useEffect(() => {
if (!hasNonTerminalTask) return
const timer = setInterval(() => fetchTasks(true), 3000)
return () => clearInterval(timer)
}, [hasNonTerminalTask, page, statusFilter, leaderIdFilter, sortBy, sortOrder])
// 刷新
const handleRefresh = () => {
fetchTasks()
}
// 删除任务
const handleDelete = async (id: number) => {
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 = async (id: number) => {
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 handleRerun = (task: BacktestTaskDto) => {
setRerunTask(task)
setRerunTaskName(`${task.taskName} (副本)`)
setRerunModalVisible(true)
}
const handleRerunSubmit = async () => {
if (!rerunTask) return
setRerunLoading(true)
try {
const response = await backtestService.rerun({
id: rerunTask.id,
taskName: rerunTaskName.trim() || undefined
})
if (response.data.code === 0) {
message.success(t('backtest.rerunSuccess'))
setRerunModalVisible(false)
setRerunTask(null)
setRerunTaskName('')
fetchTasks()
} else {
message.error(response.data.msg || t('backtest.rerunFailed'))
}
} catch (error) {
console.error('Rerun backtest failed:', error)
message.error(t('backtest.rerunFailed'))
} finally {
setRerunLoading(false)
}
}
// 重试任务
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(() => {
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()
}, [])
// 打开创建 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,
maxPositionValue: values.maxPositionValue,
minPrice: values.minPrice,
maxPrice: values.maxPrice
}
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 || [],
maxPositionValue: taskConfig.maxPositionValue,
minPrice: taskConfig.minPrice,
maxPrice: taskConfig.maxPrice,
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 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) => (
<Tag color={side === 'BUY' ? 'green' : side === 'SELL' ? 'orange' : 'blue'}>
{side === 'BUY' ? t('backtest.sideBuy') : side === 'SELL' ? t('backtest.sideSell') : t('backtest.sideSettlement')}
</Tag>
)
},
{
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') + ' ($)',
dataIndex: 'amount',
key: 'amount',
width: 120,
render: (value: string) => formatUSDC(value)
},
{
title: t('backtest.balanceAfter') + ' ($)',
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'),
dataIndex: 'taskName',
key: 'taskName',
width: isMobile ? 120 : 140,
ellipsis: true
},
{
title: t('backtest.leader'),
dataIndex: 'leaderName',
key: 'leaderName',
width: isMobile ? 100 : 120,
ellipsis: true,
render: (_: any, record: BacktestTaskDto) => record.leaderName || `Leader ${record.leaderId}`
},
{
title: t('backtest.balance'),
key: 'balance',
width: 160,
render: (_: any, record: BacktestTaskDto) => (
<div>
<div style={{ fontSize: 12, color: '#8c8c8c' }}>{formatUSDC(record.initialBalance)}</div>
<div style={{ fontWeight: 500 }}>{record.finalBalance ? formatUSDC(record.finalBalance) : '-'}</div>
</div>
)
},
{
title: t('backtest.profit'),
key: 'profit',
width: 140,
render: (_: any, record: BacktestTaskDto) => {
const profitAmount = record.profitAmount ? parseFloat(record.profitAmount) : null
const profitRate = record.profitRate ? parseFloat(record.profitRate) : null
const color = profitAmount !== null ? (profitAmount >= 0 ? '#52c41a' : '#ff4d4f') : undefined
return (
<div>
<div style={{ fontWeight: 500, color }}>
{profitAmount !== null ? formatUSDC(record.profitAmount) : '-'}
</div>
<div style={{ fontSize: 12, color }}>
{profitRate !== null ? `${record.profitRate}%` : '-'}
</div>
</div>
)
}
},
{
title: t('backtest.status'),
dataIndex: 'status',
key: 'status',
width: 130,
render: (status: string, record: BacktestTaskDto) => {
const isRunning = status === 'RUNNING' || status === 'PENDING'
return (
<div>
<Tag color={getStatusColor(status)}>{getStatusText(status)}</Tag>
{isRunning && record.progress !== undefined && (
<div style={{ marginTop: 4, fontSize: 11, color: '#8c8c8c' }}>
{record.progress}%
</div>
)}
</div>
)
}
},
{
title: t('backtest.totalTrades'),
dataIndex: 'totalTrades',
key: 'totalTrades',
width: 80,
align: 'center' as const
},
{
title: t('backtest.createdAt'),
dataIndex: 'createdAt',
key: 'createdAt',
width: isMobile ? 150 : 150,
render: (timestamp: number) => new Date(timestamp).toLocaleString()
},
{
title: t('common.actions'),
key: 'actions',
fixed: isMobile ? false : ('right' as const),
width: isMobile ? 100 : 180,
render: (_: any, record: BacktestTaskDto) => (
<Space size={4}>
<Tooltip title={t('common.viewDetail')}>
<div
onClick={() => handleViewDetail(record.id)}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '32px',
height: '32px',
cursor: 'pointer',
borderRadius: '6px',
transition: 'background-color 0.2s'
}}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f0f0f0'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
>
<EyeOutlined style={{ fontSize: '16px', color: '#1890ff' }} />
</div>
</Tooltip>
{record.status === 'COMPLETED' && (
<>
<Tooltip title={t('backtest.rerun')}>
<div
onClick={() => handleRerun(record)}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '32px',
height: '32px',
cursor: 'pointer',
borderRadius: '6px',
transition: 'background-color 0.2s'
}}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f0f0f0'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
>
<SyncOutlined style={{ fontSize: '16px', color: '#1890ff' }} />
</div>
</Tooltip>
<Tooltip title={t('backtest.createCopyTrading')}>
<div
onClick={() => handleCreateCopyTrading(record)}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '32px',
height: '32px',
cursor: 'pointer',
borderRadius: '6px',
transition: 'background-color 0.2s'
}}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f0f0f0'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
>
<CopyOutlined style={{ fontSize: '16px', color: '#1890ff' }} />
</div>
</Tooltip>
</>
)}
{record.status === 'RUNNING' && (
<Popconfirm
title={t('backtest.stopConfirm')}
onConfirm={() => handleStop(record.id)}
okText={t('common.confirm')}
cancelText={t('common.cancel')}
>
<Tooltip title={t('backtest.stop')}>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '32px',
height: '32px',
cursor: 'pointer',
borderRadius: '6px',
transition: 'background-color 0.2s'
}}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f0f0f0'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
>
<StopOutlined style={{ fontSize: '16px', color: '#1890ff' }} />
</div>
</Tooltip>
</Popconfirm>
)}
{(record.status === 'STOPPED' || record.status === 'FAILED') && (
<Tooltip title={t('backtest.retry')}>
<div
onClick={() => handleRetry(record.id)}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '32px',
height: '32px',
cursor: 'pointer',
borderRadius: '6px',
transition: 'background-color 0.2s'
}}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f0f0f0'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
>
<RedoOutlined style={{ fontSize: '16px', color: '#1890ff' }} />
</div>
</Tooltip>
)}
{(record.status === 'PENDING' || record.status === 'COMPLETED' || record.status === 'STOPPED' || record.status === 'FAILED') && (
<Popconfirm
title={t('backtest.deleteConfirm')}
onConfirm={() => handleDelete(record.id)}
okText={t('common.confirm')}
cancelText={t('common.cancel')}
>
<Tooltip title={t('common.delete')}>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '32px',
height: '32px',
cursor: 'pointer',
borderRadius: '6px',
transition: 'background-color 0.2s'
}}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#fff1f0'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
>
<DeleteOutlined style={{ fontSize: '16px', color: '#ff4d4f' }} />
</div>
</Tooltip>
</Popconfirm>
)}
</Space>
)
}
]
return (
<div style={{ padding: isMobile ? 0 : 24 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px', flexWrap: 'wrap', gap: '12px', padding: isMobile ? '0 8px' : 0 }}>
<h2 style={{ margin: 0, fontSize: isMobile ? '20px' : '24px' }}>{t('backtest.title')}</h2>
<Space size={8}>
<Tooltip title={t('backtest.createTask')}>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleCreate}
size={isMobile ? 'middle' : 'large'}
style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px', fontSize: isMobile ? '14px' : '16px' }}
/>
</Tooltip>
<Tooltip title={t('common.refresh')}>
<Button icon={<ReloadOutlined />} onClick={handleRefresh} loading={loading} size={isMobile ? 'middle' : 'large'} style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px' }} />
</Tooltip>
</Space>
</div>
<Card style={{ borderRadius: '12px', boxShadow: '0 2px 8px rgba(0,0,0,0.08)', border: '1px solid #e8e8e8' }} bodyStyle={{ padding: isMobile ? '12px' : '24px' }}>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
{/* 筛选栏 */}
<Row gutter={[16, 16]} style={{ marginBottom: isMobile ? 12 : 0 }}>
<Col xs={24} sm={12} md={6}>
<LeaderSelect
style={{ width: '100%' }}
placeholder={t('backtest.leader')}
allowClear
value={leaderIdFilter}
onChange={(value) => setLeaderIdFilter(value)}
leaders={leaders}
/>
</Col>
<Col xs={24} sm={12} md={6}>
<Select
style={{ width: '100%' }}
placeholder={t('backtest.status')}
allowClear
onChange={(value) => setStatusFilter(value)}
value={statusFilter}
>
<Select.Option value="PENDING">{t('backtest.statusPending')}</Select.Option>
<Select.Option value="RUNNING">{t('backtest.statusRunning')}</Select.Option>
<Select.Option value="COMPLETED">{t('backtest.statusCompleted')}</Select.Option>
<Select.Option value="STOPPED">{t('backtest.statusStopped')}</Select.Option>
<Select.Option value="FAILED">{t('backtest.statusFailed')}</Select.Option>
</Select>
</Col>
<Col xs={24} sm={12} md={6}>
<Select style={{ width: '100%' }} placeholder={t('backtest.sortBy')} onChange={(value) => setSortBy(value)} value={sortBy}>
<Select.Option value="profitAmount">{t('backtest.profitAmount')}</Select.Option>
<Select.Option value="profitRate">{t('backtest.profitRate')}</Select.Option>
<Select.Option value="createdAt">{t('backtest.createdAt')}</Select.Option>
</Select>
</Col>
<Col xs={24} sm={12} md={6}>
<Select style={{ width: '100%' }} placeholder={t('backtest.sortOrder')} onChange={(value) => setSortOrder(value)} value={sortOrder}>
<Select.Option value="asc">{t('common.ascending')}</Select.Option>
<Select.Option value="desc">{t('common.descending')}</Select.Option>
</Select>
</Col>
</Row>
{/* 数据:移动端卡片 / 桌面端表格 */}
{isMobile ? (
<>
{loading ? (
<div style={{ textAlign: 'center', padding: '40px' }}>
<Spin size="large" />
</div>
) : tasks.length === 0 ? (
<Empty description={t('backtest.noData') || t('common.noData')} />
) : (
<List
dataSource={tasks}
renderItem={(task) => (
<Card
key={task.id}
style={{
marginBottom: '10px',
borderRadius: '10px',
boxShadow: '0 1px 3px rgba(0,0,0,0.08)',
border: '1px solid #e8e8e8',
overflow: 'hidden'
}}
bodyStyle={{ padding: 0 }}
>
<div style={{
padding: '10px 12px',
background: 'var(--ant-color-primary, #1677ff)',
color: '#fff'
}}>
<div style={{ fontSize: '15px', fontWeight: '600', marginBottom: '2px' }}>{task.taskName}</div>
<div style={{ fontSize: '12px', opacity: '0.9' }}>{task.leaderName || `Leader ${task.leaderId}`}</div>
</div>
<div style={{ padding: '8px 12px', backgroundColor: '#fafafa', borderBottom: '1px solid #f0f0f0', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ fontSize: '10px', color: '#8c8c8c' }}>{t('backtest.profitAmount')}</div>
{task.profitAmount != null ? (
<div style={{ fontSize: '14px', fontWeight: '600', color: parseFloat(task.profitAmount) >= 0 ? '#52c41a' : '#ff4d4f' }}>${formatUSDC(task.profitAmount)}</div>
) : (
<div style={{ fontSize: '14px', color: '#8c8c8c' }}>-</div>
)}
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontSize: '10px', color: '#8c8c8c' }}>{t('backtest.profitRate')}</div>
{task.profitRate != null ? (
<div style={{ fontSize: '14px', fontWeight: '600', color: parseFloat(task.profitRate) >= 0 ? '#52c41a' : '#ff4d4f' }}>{task.profitRate}%</div>
) : (
<div style={{ fontSize: '14px', color: '#8c8c8c' }}>-</div>
)}
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontSize: '10px', color: '#8c8c8c' }}>{t('backtest.status')}</div>
<Tag color={getStatusColor(task.status)}>{getStatusText(task.status)}</Tag>
</div>
</div>
<div style={{ padding: '6px 12px', fontSize: '11px', color: '#8c8c8c', borderBottom: '1px solid #f0f0f0' }}>
{t('backtest.initialBalance')}: {formatUSDC(task.initialBalance)} · {t('backtest.backtestDays')}: {task.backtestDays} {t('common.day')} · {t('backtest.progress')}: {task.progress}%
</div>
<div style={{ padding: '8px 12px', display: 'flex', justifyContent: 'space-around', alignItems: 'center', flexWrap: 'wrap', gap: 4 }}>
<Tooltip title={t('common.viewDetail')}>
<div onClick={() => handleViewDetail(task.id)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
<EyeOutlined style={{ fontSize: '18px', color: '#1890ff' }} />
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('common.viewDetail')}</span>
</div>
</Tooltip>
{task.status === 'COMPLETED' && (
<>
<Tooltip title={t('backtest.rerun')}>
<div onClick={() => handleRerun(task)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
<SyncOutlined style={{ fontSize: '18px', color: '#1890ff' }} />
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('backtest.rerun')}</span>
</div>
</Tooltip>
<Tooltip title={t('backtest.createCopyTrading')}>
<div onClick={() => handleCreateCopyTrading(task)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
<CopyOutlined style={{ fontSize: '18px', color: '#1890ff' }} />
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('backtest.createCopyTrading')}</span>
</div>
</Tooltip>
</>
)}
{task.status === 'RUNNING' && (
<Tooltip title={t('backtest.stop')}>
<div onClick={() => handleStop(task.id)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
<StopOutlined style={{ fontSize: '18px', color: '#1890ff' }} />
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('backtest.stop')}</span>
</div>
</Tooltip>
)}
{(task.status === 'STOPPED' || task.status === 'FAILED') && (
<Tooltip title={t('backtest.retry')}>
<div onClick={() => handleRetry(task.id)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
<RedoOutlined style={{ fontSize: '18px', color: '#1890ff' }} />
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('backtest.retry')}</span>
</div>
</Tooltip>
)}
{(task.status === 'PENDING' || task.status === 'COMPLETED' || task.status === 'STOPPED' || task.status === 'FAILED') && (
<Tooltip title={t('common.delete')}>
<div onClick={() => handleDelete(task.id)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
<DeleteOutlined style={{ fontSize: '18px', color: '#ff4d4f' }} />
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('common.delete')}</span>
</div>
</Tooltip>
)}
</div>
</Card>
)}
/>
)}
{/* 移动端分页 */}
{!loading && tasks.length > 0 && total > size && (
<div style={{ display: 'flex', justifyContent: 'center', marginTop: 16 }}>
<Button size="small" disabled={page <= 1} onClick={() => setPage(p => Math.max(1, p - 1))}>{t('common.prev')}</Button>
<span style={{ margin: '0 12px', lineHeight: '24px' }}>{page} / {Math.ceil(total / size)}</span>
<Button size="small" disabled={page >= Math.ceil(total / size)} onClick={() => setPage(p => p + 1)}>{t('common.next')}</Button>
</div>
)}
</>
) : (
<Table
columns={columns}
dataSource={tasks}
rowKey="id"
loading={loading}
pagination={{
current: page,
pageSize: size,
total,
showSizeChanger: false,
showTotal: (totalCount) => `${t('common.total')} ${totalCount} ${t('common.items')}`,
onChange: (newPage) => setPage(newPage),
simple: isMobile
}}
scroll={{ x: 1000 }}
/>
)}
</Space>
</Card>
{/* 重新测试 Modal */}
<Modal
title={t('backtest.rerun')}
open={rerunModalVisible}
onCancel={() => {
setRerunModalVisible(false)
setRerunTask(null)
setRerunTaskName('')
}}
onOk={handleRerunSubmit}
okText={t('common.confirm')}
cancelText={t('common.cancel')}
confirmLoading={rerunLoading}
destroyOnClose
>
<p style={{ marginBottom: 8 }}>{t('backtest.rerunConfirm')}</p>
<Input
value={rerunTaskName}
onChange={(e) => setRerunTaskName(e.target.value)}
placeholder={t('backtest.rerunTaskNamePlaceholder')}
maxLength={100}
/>
</Modal>
{/* 创建回测任务 Modal */}
<Modal
title={t('backtest.createTask')}
open={createModalVisible}
onCancel={() => {
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' }}
>
<Form
form={createForm}
layout="vertical"
initialValues={{
maxDailyLoss: 500,
maxDailyOrders: 100,
supportSell: true,
keywordFilterMode: 'DISABLED',
backtestDays: 7
}}
>
<Row gutter={24}>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.taskName')}
name="taskName"
rules={[{ required: true, message: t('backtest.taskNameRequired') || '请输入任务名称' }]}
>
<Input placeholder={t('backtest.taskName')} />
</Form.Item>
</Col>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.leader')}
name="leaderId"
rules={[{ required: true, message: t('backtest.leaderRequired') || '请选择 Leader' }]}
>
<LeaderSelect
leaders={leaders}
placeholder={t('backtest.leader')}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.initialBalance') + ' ($)'}
name="initialBalance"
rules={[
{ required: true, message: t('backtest.initialBalanceRequired') || '请输入初始资金' },
{ type: 'number', min: 1, message: t('backtest.initialBalanceInvalid') || '初始资金必须大于 0' }
]}
>
<InputNumber
style={{ width: '100%' }}
placeholder={t('backtest.initialBalance')}
precision={2}
min={1}
/>
</Form.Item>
</Col>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.backtestDays') + ` (1-15 ${t('common.day')})`}
name="backtestDays"
rules={[
{ required: true, message: t('backtest.backtestDaysRequired') || '请输入回测天数' },
{ type: 'number', min: 1, max: 15, message: t('backtest.backtestDaysInvalid') || '回测天数必须在 1-15 之间' }
]}
>
<InputNumber
style={{ width: '100%' }}
placeholder={t('backtest.backtestDays')}
precision={0}
min={1}
max={15}
/>
</Form.Item>
</Col>
</Row>
{/* 跟单配置 */}
<div style={{ marginBottom: 24 }}>
<h3 style={{ marginBottom: 16 }}>{t('backtest.config')}</h3>
<Form.Item
label={t('backtest.copyMode')}
name="copyMode"
>
<Select onChange={(value) => setCopyMode(value)}>
<Select.Option value="RATIO">{t('backtest.copyModeRatio')}</Select.Option>
<Select.Option value="FIXED">{t('backtest.copyModeFixed')}</Select.Option>
</Select>
</Form.Item>
{copyMode === 'RATIO' && (
<Form.Item
label={t('backtest.copyRatio')}
name="copyRatio"
tooltip={t('backtest.copyRatioTooltip') || '跟单比例表示跟单金额相对于 Leader 订单金额的百分比。例如:100% 表示 1:1 跟单,50% 表示半仓跟单,200% 表示双倍跟单'}
rules={[
{ required: true, message: t('backtest.copyRatioRequired') || '请输入跟单比例' },
{ type: 'number', min: 0.01, max: 10000, message: t('backtest.copyRatioInvalid') || '跟单比例必须在 0.01-10000 之间' }
]}
>
<InputNumber
min={0.01}
max={10000}
step={0.01}
precision={2}
style={{ width: '100%' }}
addonAfter="%"
placeholder={t('backtest.copyRatioPlaceholder') || '例如:100 表示 100%1:1 跟单),默认 100%'}
parser={(value) => {
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+$/, '')
}}
/>
</Form.Item>
)}
{copyMode === 'FIXED' && (
<Form.Item
label={t('backtest.fixedAmount') + ' ($)'}
name="fixedAmount"
rules={[
{ required: true, message: t('backtest.fixedAmountRequired') || '请输入固定金额' },
{ type: 'number', min: 1, message: t('backtest.fixedAmountInvalid') || '固定金额必须大于 0' }
]}
>
<InputNumber
style={{ width: '100%' }}
placeholder={t('backtest.fixedAmount')}
precision={2}
min={1}
/>
</Form.Item>
)}
<Row gutter={24}>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.maxOrderSize') + ' ($)'}
name="maxOrderSize"
rules={[{ required: true }]}
>
<InputNumber style={{ width: '100%' }} precision={2} min={1} />
</Form.Item>
</Col>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.minOrderSize') + ' ($)'}
name="minOrderSize"
rules={[{ required: true }]}
>
<InputNumber style={{ width: '100%' }} precision={2} min={1} />
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.maxDailyLoss') + ' ($)'}
name="maxDailyLoss"
rules={[{ required: true }]}
>
<InputNumber style={{ width: '100%' }} precision={2} min={0} />
</Form.Item>
</Col>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.maxDailyOrders')}
name="maxDailyOrders"
rules={[{ required: true }]}
>
<InputNumber style={{ width: '100%' }} precision={0} min={1} />
</Form.Item>
</Col>
</Row>
<Form.Item
label={t('backtest.maxPositionValue') + ' ($)'}
name="maxPositionValue"
>
<InputNumber
style={{ width: '100%' }}
placeholder={t('backtest.maxPositionValuePlaceholder') || '留空表示不启用最大仓位限制'}
precision={2}
min={0}
/>
</Form.Item>
<Form.Item
label={t('backtest.priceRange')}
tooltip={t('backtest.priceRangeTooltip')}
>
<Row gutter={12}>
<Col span={12}>
<Form.Item name="minPrice" noStyle>
<InputNumber
style={{ width: '100%' }}
placeholder={t('backtest.minPricePlaceholder') || '最低价(留空不限制)'}
min={0.01}
max={0.99}
step={0.0001}
precision={4}
formatter={(value) => {
if (!value && value !== 0) return ''
const num = parseFloat(value.toString())
if (isNaN(num)) return ''
return num.toString().replace(/\.0+$/, '')
}}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="maxPrice" noStyle>
<InputNumber
style={{ width: '100%' }}
placeholder={t('backtest.maxPricePlaceholder') || '最高价(留空不限制)'}
min={0.01}
max={0.99}
step={0.0001}
precision={4}
formatter={(value) => {
if (!value && value !== 0) return ''
const num = parseFloat(value.toString())
if (isNaN(num)) return ''
return num.toString().replace(/\.0+$/, '')
}}
/>
</Form.Item>
</Col>
</Row>
</Form.Item>
<Form.Item
label={t('backtest.supportSell')}
name="supportSell"
valuePropName="checked"
>
<Switch />
<span style={{ fontSize: 12, color: '#888', marginLeft: 8 }}>{t('backtest.supportSellHint') || '是否跟随 Leader 卖出'}</span>
</Form.Item>
<Form.Item
label={t('backtest.keywordFilterMode')}
name="keywordFilterMode"
>
<Select>
<Select.Option value="DISABLED">{t('backtest.keywordFilterModeDisabled')}</Select.Option>
<Select.Option value="WHITELIST">{t('backtest.keywordFilterModeWhitelist')}</Select.Option>
<Select.Option value="BLACKLIST">{t('backtest.keywordFilterModeBlacklist')}</Select.Option>
</Select>
</Form.Item>
<Form.Item
label={t('backtest.keywords')}
name="keywords"
>
<Select
mode="tags"
style={{ width: '100%' }}
placeholder={t('backtest.keywordsPlaceholder') || '请输入关键字,按回车添加'}
/>
</Form.Item>
</div>
</Form>
</Modal>
{/* 创建跟单配置 Modal */}
<AddCopyTradingModal
open={addCopyTradingModalVisible}
onClose={() => {
setAddCopyTradingModalVisible(false)
setPreFilledConfig(null)
}}
onSuccess={() => {
message.success(t('backtest.createCopyTradingSuccess'))
setAddCopyTradingModalVisible(false)
setPreFilledConfig(null)
}}
preFilledConfig={preFilledConfig}
/>
{/* 任务详情 Modal */}
<Modal
title={t('backtest.taskDetail')}
open={detailModalVisible}
onCancel={() => {
setDetailModalVisible(false)
setDetailTask(null)
setDetailConfig(null)
setDetailStatistics(null)
setDetailTrades([])
setDetailAllTrades([])
setDetailTradesPage(1)
setDetailTradesTotal(0)
}}
footer={
detailTask && detailTask.status === 'COMPLETED' && detailConfig ? (
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button icon={<SyncOutlined />} onClick={() => {
setDetailModalVisible(false)
handleRerun(detailTask)
}}>
{t('backtest.rerun')}
</Button>
<Button type="primary" icon={<CopyOutlined />} onClick={() => {
const preFilled = {
leaderId: detailTask.leaderId,
copyMode: detailConfig.copyMode,
copyRatio: detailConfig.copyMode === 'RATIO' ? parseFloat(detailConfig.copyRatio) * 100 : undefined,
fixedAmount: detailConfig.copyMode === 'FIXED' ? detailConfig.fixedAmount : undefined,
maxOrderSize: parseFloat(detailConfig.maxOrderSize),
minOrderSize: parseFloat(detailConfig.minOrderSize),
maxDailyLoss: parseFloat(detailConfig.maxDailyLoss),
maxDailyOrders: detailConfig.maxDailyOrders,
supportSell: detailConfig.supportSell,
keywordFilterMode: detailConfig.keywordFilterMode,
keywords: detailConfig.keywords || [],
maxPositionValue: detailConfig.maxPositionValue,
minPrice: detailConfig.minPrice,
maxPrice: detailConfig.maxPrice,
configName: `回测任务-${detailTask.taskName}`
}
setPreFilledConfig(preFilled)
setAddCopyTradingModalVisible(true)
setDetailModalVisible(false)
}}>
{t('backtest.createCopyTrading')}
</Button>
</Space>
) : null
}
width={isMobile ? '95%' : '80%'}
style={{ top: isMobile ? 10 : 20 }}
styles={{
body: {
maxHeight: 'calc(100vh - 250px)',
overflowY: 'auto',
padding: '24px'
}
}}
destroyOnClose
>
{detailTask && (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
{/* 任务基本信息 */}
<Card title={t('backtest.taskDetail')} size="small">
<Descriptions column={isMobile ? 1 : 2} bordered size="small">
<Descriptions.Item label={t('backtest.taskName')}>{detailTask.taskName}</Descriptions.Item>
<Descriptions.Item label={t('backtest.leader')}>
{detailTask.leaderName || `Leader ${detailTask.leaderId}`}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.initialBalance')}>
${formatUSDC(detailTask.initialBalance)}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.finalBalance')}>
{detailTask.finalBalance ? '$' + formatUSDC(detailTask.finalBalance) : '-'}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.profitAmount')}>
<span style={{ color: detailTask.profitAmount && parseFloat(detailTask.profitAmount) >= 0 ? '#52c41a' : '#ff4d4f' }}>
{detailTask.profitAmount ? '$' + formatUSDC(detailTask.profitAmount) : '-'}
</span>
</Descriptions.Item>
<Descriptions.Item label={t('backtest.profitRate')}>
<span style={{ color: detailTask.profitRate && parseFloat(detailTask.profitRate) >= 0 ? '#52c41a' : '#ff4d4f' }}>
{detailTask.profitRate ? detailTask.profitRate + '%' : '-'}
</span>
</Descriptions.Item>
<Descriptions.Item label={t('backtest.backtestDays')}>
{detailTask.backtestDays} {t('common.day')}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.status')}>
<Tag color={getStatusColor(detailTask.status)}>{getStatusText(detailTask.status)}</Tag>
</Descriptions.Item>
<Descriptions.Item label={t('backtest.progress')}>
{detailTask.progress}%
</Descriptions.Item>
<Descriptions.Item label={t('backtest.totalTrades')}>
{detailTask.totalTrades}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.startTime')}>
{new Date(detailTask.startTime).toLocaleString()}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.endTime')}>
{detailTask.endTime ? new Date(detailTask.endTime).toLocaleString() : '-'}
</Descriptions.Item>
</Descriptions>
</Card>
{/* 统计信息 */}
{detailStatistics && (
<Card title={t('backtest.statistics')} size="small">
<Row gutter={16}>
<Col xs={24} sm={12} md={6}>
<Statistic
title={t('backtest.totalTrades')}
value={detailStatistics.totalTrades || 0}
/>
</Col>
<Col xs={24} sm={12} md={6}>
<Statistic
title={t('backtest.buyTrades')}
value={detailStatistics.buyTrades || 0}
/>
</Col>
<Col xs={24} sm={12} md={6}>
<Statistic
title={t('backtest.sellTrades')}
value={detailStatistics.sellTrades}
/>
</Col>
<Col xs={24} sm={12} md={6}>
<Statistic
title={t('backtest.winTrades')}
value={detailStatistics.winTrades}
valueStyle={{ color: '#52c41a' }}
/>
</Col>
<Col xs={24} sm={12} md={6}>
<Statistic
title={t('backtest.lossTrades')}
value={detailStatistics.lossTrades}
valueStyle={{ color: '#ff4d4f' }}
/>
</Col>
<Col xs={24} sm={12} md={6}>
<Statistic
title={t('backtest.winRate')}
value={detailStatistics.winRate || '0.00'}
suffix="%"
valueStyle={{
color: detailStatistics.winRate && !isNaN(parseFloat(detailStatistics.winRate)) && parseFloat(detailStatistics.winRate) >= 50 ? '#52c41a' : '#ff4d4f'
}}
/>
</Col>
<Col xs={24} sm={12} md={6}>
<Statistic
title={t('backtest.maxProfit')}
value={formatUSDC(detailStatistics.maxProfit)}
valueStyle={{ color: '#52c41a' }}
/>
</Col>
<Col xs={24} sm={12} md={6}>
<Statistic
title={t('backtest.maxLoss')}
value={formatUSDC(detailStatistics.maxLoss)}
valueStyle={{ color: '#ff4d4f' }}
/>
</Col>
<Col xs={24} sm={12} md={6}>
<Statistic
title={t('backtest.maxDrawdown')}
value={formatUSDC(detailStatistics.maxDrawdown)}
valueStyle={{ color: '#ff4d4f' }}
/>
</Col>
{detailStatistics.avgHoldingTime && (
<Col xs={24} sm={12} md={6}>
<Statistic
title={t('backtest.avgHoldingTime')}
value={(detailStatistics.avgHoldingTime / 1000 / 60).toFixed(2)}
suffix=" min"
/>
</Col>
)}
</Row>
</Card>
)}
{/* 配置信息 */}
{detailConfig && (
<Card title={t('backtest.config')} size="small">
<Descriptions column={isMobile ? 1 : 2} bordered size="small">
<Descriptions.Item label={t('backtest.copyMode')}>
{detailConfig.copyMode === 'RATIO'
? `${t('backtest.copyModeRatio')} ${parseFloat(detailConfig.copyRatio) * 100}%`
: `${t('backtest.copyModeFixed')} $${formatUSDC(detailConfig.fixedAmount)}`
}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.maxOrderSize')}>
{'$' + formatUSDC(detailConfig.maxOrderSize)}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.minOrderSize')}>
{'$' + formatUSDC(detailConfig.minOrderSize)}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.maxDailyLoss')}>
{'$' + formatUSDC(detailConfig.maxDailyLoss)}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.maxDailyOrders')}>
{detailConfig.maxDailyOrders}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.supportSell')}>
{detailConfig.supportSell ? t('common.yes') : t('common.no')}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.keywordFilterMode')}>
{detailConfig.keywordFilterMode === 'WHITELIST'
? t('backtest.keywordFilterModeWhitelist')
: detailConfig.keywordFilterMode === 'BLACKLIST'
? t('backtest.keywordFilterModeBlacklist')
: t('backtest.keywordFilterModeDisabled')}
</Descriptions.Item>
{detailConfig.keywords && detailConfig.keywords.length > 0 && (
<Descriptions.Item label={t('backtest.keywords')}>
{detailConfig.keywords.join(', ')}
</Descriptions.Item>
)}
{detailConfig.maxPositionValue && (
<Descriptions.Item label={t('backtest.maxPositionValue')}>
{'$' + formatUSDC(detailConfig.maxPositionValue)}
</Descriptions.Item>
)}
{(detailConfig.minPrice || detailConfig.maxPrice) && (
<Descriptions.Item label={t('backtest.priceRange')}>
{detailConfig.minPrice !== undefined && detailConfig.minPrice !== null && detailConfig.minPrice !== ''
? `≥ ${parseFloat(detailConfig.minPrice).toFixed(4)}`
: ''}
{(detailConfig.minPrice !== undefined && detailConfig.minPrice !== null && detailConfig.minPrice !== '') &&
(detailConfig.maxPrice !== undefined && detailConfig.maxPrice !== null && detailConfig.maxPrice !== '')
? ' ~ '
: ''}
{detailConfig.maxPrice !== undefined && detailConfig.maxPrice !== null && detailConfig.maxPrice !== ''
? `≤ ${parseFloat(detailConfig.maxPrice).toFixed(4)}`
: ''}
</Descriptions.Item>
)}
</Descriptions>
</Card>
)}
{/* 资金变化图表 */}
{detailAllTrades.length > 0 && (
<Card title={t('backtest.balanceChart')} size="small">
<BacktestChart trades={detailAllTrades} />
</Card>
)}
{/* 交易记录 */}
<Card title={t('backtest.tradeRecords')} size="small">
<Table
columns={tradeColumns}
dataSource={detailTrades}
rowKey="id"
loading={detailTradesLoading}
pagination={{
current: detailTradesPage,
pageSize: detailTradesSize,
total: detailTradesTotal,
showSizeChanger: false,
showTotal: (total) => `${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"
/>
</Card>
</Space>
)}
</Modal>
</div >
)
}
export default BacktestList