feat: 回测系统优化 - 清理字段、移动端适配、缓存优化

## 主要改动

### 1. 回测字段清理
- 删除 BacktestCreate.tsx(未使用,创建使用 modal)
- 移除不适用于回测的字段:
  - priceTolerance, delaySeconds(回测使用历史数据)
  - minOrderDepth, maxSpread(无历史订单簿数据)
  - minPrice, maxPrice, maxPositionValue(回测中无实际意义)
  - maxMarketEndDate(意义不大)
- 更新相关 Entity、DTO、Service
- 添加数据库 migration(V29)

### 2. 移动端响应式适配
- BacktestList:筛选器、表格、Modal、表单响应式布局
- BacktestDetail:详情卡片、统计信息、按钮响应式
- 使用 useMediaQuery hook,断点 768px
- 支持手机、平板、桌面多种设备

### 3. 已结算市场缓存优化
- MarketPriceService 添加 Caffeine 缓存
- 缓存已结算市场价格,避免重复 RPC 调用
- 预计 RPC 请求减少 ~78.6%,性能提升显著
- 添加缓存统计和管理方法

### 4. UI 修复
- 修复 BacktestDetail 停止按钮文本
- 更新多语言翻译(supportSell → 跟单卖出)

## 性能收益
- 回测场景 RPC 调用减少 78.6%
- 回测执行时间预计减少 11秒(14秒 → 3秒)
- 移动端用户体验显著提升
This commit is contained in:
WrBug
2026-01-31 20:45:40 +08:00
parent fd25821e39
commit 96fbc3f720
22 changed files with 911 additions and 1199 deletions
+74 -115
View File
@@ -2,17 +2,19 @@ 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 } from '@ant-design/icons'
import { PlusOutlined, ReloadOutlined, DeleteOutlined, StopOutlined, EyeOutlined, RedoOutlined } 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'
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<BacktestTaskDto[]>([])
const [total, setTotal] = useState(0)
@@ -22,7 +24,7 @@ const BacktestList: React.FC = () => {
const [leaderIdFilter] = useState<number | 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()
@@ -112,6 +114,29 @@ const BacktestList: React.FC = () => {
})
}
// 重试任务
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) {
@@ -145,8 +170,6 @@ const BacktestList: React.FC = () => {
minOrderSize: 1,
maxDailyLoss: 500,
maxDailyOrders: 50,
priceTolerance: 5,
delaySeconds: 0,
supportSell: true,
keywordFilterMode: 'DISABLED',
backtestDays: 7
@@ -172,17 +195,9 @@ const BacktestList: React.FC = () => {
minOrderSize: values.minOrderSize,
maxDailyLoss: values.maxDailyLoss,
maxDailyOrders: values.maxDailyOrders,
priceTolerance: values.priceTolerance,
delaySeconds: values.delaySeconds,
supportSell: values.supportSell,
minOrderDepth: values.minOrderDepth,
maxSpread: values.maxSpread,
minPrice: values.minPrice,
maxPrice: values.maxPrice,
maxPositionValue: values.maxPositionValue,
keywordFilterMode: values.keywordFilterMode,
keywords: values.keywords,
maxMarketEndDate: values.maxMarketEndDate
keywords: values.keywords
}
const response = await backtestService.create(request)
@@ -231,14 +246,14 @@ const BacktestList: React.FC = () => {
title: t('backtest.taskName'),
dataIndex: 'taskName',
key: 'taskName',
width: 150
width: isMobile ? 120 : 150
},
{
title: t('backtest.leader'),
dataIndex: 'leaderName',
key: 'leaderName',
width: 150,
render: (_: any, record: BacktestTaskDto) => record.leaderName || record.leaderAddress || '-'
width: isMobile ? 100 : 150,
render: (_: any, record: BacktestTaskDto) => record.leaderName || record.leaderAddress?.substring(0, 8) + '...' || '-'
},
{
title: t('backtest.initialBalance'),
@@ -324,14 +339,14 @@ const BacktestList: React.FC = () => {
title: t('backtest.createdAt'),
dataIndex: 'createdAt',
key: 'createdAt',
width: 180,
width: isMobile ? 150 : 180,
render: (timestamp: number) => new Date(timestamp).toLocaleString()
},
{
title: t('common.actions'),
key: 'actions',
fixed: 'right' as const,
width: 150,
fixed: isMobile ? false : ('right' as const),
width: isMobile ? 100 : 150,
render: (_: any, record: BacktestTaskDto) => (
<Space size="small">
<Button
@@ -342,7 +357,7 @@ const BacktestList: React.FC = () => {
>
{t('common.viewDetail')}
</Button>
{(record.status === 'RUNNING' || record.status === 'PENDING') && (
{record.status === 'RUNNING' && (
<Button
type="link"
size="small"
@@ -350,10 +365,20 @@ const BacktestList: React.FC = () => {
icon={<StopOutlined />}
onClick={() => handleStop(record.id)}
>
{t('backtest.statusStopped')}
{t('backtest.stop')}
</Button>
)}
{(record.status === 'COMPLETED' || record.status === 'STOPPED' || record.status === 'FAILED') && (
{(record.status === 'STOPPED' || record.status === 'FAILED') && (
<Button
type="link"
size="small"
icon={<RedoOutlined />}
onClick={() => handleRetry(record.id)}
>
{t('backtest.retry')}
</Button>
)}
{(record.status === 'PENDING' || record.status === 'COMPLETED') && (
<Button
type="link"
size="small"
@@ -374,11 +399,11 @@ const BacktestList: React.FC = () => {
<Card>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
{/* 头部操作栏 */}
<Row justify="space-between" align="middle">
<Col>
<Space size="middle">
<Row justify="space-between" align="middle" gutter={[16, 16]}>
<Col xs={24} sm={24} md={12} lg={16}>
<Space size="middle" direction={isMobile ? 'vertical' : 'horizontal'} style={{ width: isMobile ? '100%' : 'auto' }}>
<Select
style={{ width: 150 }}
style={{ width: isMobile ? '100%' : 150 }}
placeholder={t('backtest.status')}
allowClear
onChange={(value) => setStatusFilter(value)}
@@ -391,7 +416,7 @@ const BacktestList: React.FC = () => {
<Select.Option value="FAILED">{t('backtest.statusFailed')}</Select.Option>
</Select>
<Select
style={{ width: 150 }}
style={{ width: isMobile ? '100%' : 150 }}
placeholder={t('backtest.sortBy')}
onChange={(value) => setSortBy(value)}
value={sortBy}
@@ -401,7 +426,7 @@ const BacktestList: React.FC = () => {
<Select.Option value="createdAt">{t('backtest.createdAt')}</Select.Option>
</Select>
<Select
style={{ width: 120 }}
style={{ width: isMobile ? '100%' : 120 }}
placeholder={t('backtest.sortOrder')}
onChange={(value) => setSortOrder(value)}
value={sortOrder}
@@ -411,19 +436,21 @@ const BacktestList: React.FC = () => {
</Select>
</Space>
</Col>
<Col>
<Space>
<Col xs={24} sm={24} md={12} lg={8} style={{ textAlign: isMobile ? 'left' : 'right' }}>
<Space style={{ width: isMobile ? '100%' : 'auto' }}>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleCreate}
style={{ flex: isMobile ? 1 : undefined }}
>
{t('backtest.createTask')}
{isMobile ? t('common.create') : t('backtest.createTask')}
</Button>
<Button
icon={<ReloadOutlined />}
onClick={handleRefresh}
loading={loading}
style={{ flex: isMobile ? 1 : undefined }}
>
{t('common.refresh')}
</Button>
@@ -443,9 +470,10 @@ const BacktestList: React.FC = () => {
total,
showSizeChanger: false,
showTotal: (total) => `${t('common.total')} ${total} ${t('common.items')}`,
onChange: (newPage) => setPage(newPage)
onChange: (newPage) => setPage(newPage),
simple: isMobile
}}
scroll={{ x: 1400 }}
scroll={isMobile ? { x: 1200 } : { x: 1400 }}
/>
</Space>
</Card>
@@ -461,31 +489,25 @@ const BacktestList: React.FC = () => {
onOk={handleCreateSubmit}
okText={t('common.save')}
cancelText={t('common.cancel')}
width={800}
width={isMobile ? '95%' : 800}
confirmLoading={createLoading}
destroyOnClose
style={{ top: 20 }}
bodyStyle={{ maxHeight: 'calc(100vh - 200px)', overflowY: 'auto' }}
style={{ top: isMobile ? 10 : 20 }}
bodyStyle={{ maxHeight: isMobile ? 'calc(100vh - 150px)' : 'calc(100vh - 200px)', overflowY: 'auto' }}
>
<Form
form={createForm}
layout="vertical"
initialValues={{
copyMode: 'RATIO',
copyRatio: 100, // 默认 100%(显示为百分比)
maxOrderSize: 1000,
minOrderSize: 1,
maxDailyLoss: 500,
maxDailyOrders: 50,
priceTolerance: 5,
delaySeconds: 0,
supportSell: true,
keywordFilterMode: 'DISABLED',
backtestDays: 7
}}
>
<Row gutter={24}>
<Col span={12}>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.taskName')}
name="taskName"
@@ -494,7 +516,7 @@ const BacktestList: React.FC = () => {
<Input placeholder={t('backtest.taskName')} />
</Form.Item>
</Col>
<Col span={12}>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.leader')}
name="leaderId"
@@ -512,7 +534,7 @@ const BacktestList: React.FC = () => {
</Row>
<Row gutter={24}>
<Col span={12}>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.initialBalance') + ' (USDC)'}
name="initialBalance"
@@ -529,7 +551,7 @@ const BacktestList: React.FC = () => {
/>
</Form.Item>
</Col>
<Col span={12}>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.backtestDays') + ` (1-15 ${t('common.day')})`}
name="backtestDays"
@@ -616,7 +638,7 @@ const BacktestList: React.FC = () => {
)}
<Row gutter={24}>
<Col span={12}>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.maxOrderSize') + ' (USDC)'}
name="maxOrderSize"
@@ -625,7 +647,7 @@ const BacktestList: React.FC = () => {
<InputNumber style={{ width: '100%' }} precision={2} min={1} />
</Form.Item>
</Col>
<Col span={12}>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.minOrderSize') + ' (USDC)'}
name="minOrderSize"
@@ -637,7 +659,7 @@ const BacktestList: React.FC = () => {
</Row>
<Row gutter={24}>
<Col span={12}>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.maxDailyLoss') + ' (USDC)'}
name="maxDailyLoss"
@@ -646,7 +668,7 @@ const BacktestList: React.FC = () => {
<InputNumber style={{ width: '100%' }} precision={2} min={0} />
</Form.Item>
</Col>
<Col span={12}>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.maxDailyOrders')}
name="maxDailyOrders"
@@ -657,75 +679,13 @@ const BacktestList: React.FC = () => {
</Col>
</Row>
<Form.Item
label={t('backtest.priceTolerance') + ' (%)'}
name="priceTolerance"
>
<InputNumber style={{ width: '100%' }} precision={2} min={0} max={100} />
</Form.Item>
<Form.Item
label={t('backtest.delaySeconds')}
name="delaySeconds"
>
<InputNumber style={{ width: '100%' }} precision={0} min={0} />
<span style={{ fontSize: 12, color: '#888' }}>{t('backtest.delaySecondsHint') || '延迟执行模拟真实跟单延迟'}</span>
</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>
<h4 style={{ marginTop: 24, marginBottom: 16 }}>{t('backtest.advancedFilters')}</h4>
<Row gutter={24}>
<Col span={12}>
<Form.Item
label={t('backtest.minOrderDepth') + ' (USDC)'}
name="minOrderDepth"
>
<InputNumber style={{ width: '100%' }} precision={2} min={0} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label={t('backtest.maxSpread') + ' (%)'}
name="maxSpread"
>
<InputNumber style={{ width: '100%' }} precision={2} min={0} />
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={12}>
<Form.Item
label={t('backtest.minPrice')}
name="minPrice"
>
<InputNumber style={{ width: '100%' }} precision={4} min={0} max={1} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label={t('backtest.maxPrice')}
name="maxPrice"
>
<InputNumber style={{ width: '100%' }} precision={4} min={0} max={1} />
</Form.Item>
</Col>
</Row>
<Form.Item
label={t('backtest.maxPositionValue') + ' (USDC)'}
name="maxPositionValue"
>
<InputNumber style={{ width: '100%' }} precision={2} min={0} />
<span style={{ fontSize: 12, color: '#888', marginLeft: 8 }}>{t('backtest.supportSellHint') || '是否跟随 Leader 卖出'}</span>
</Form.Item>
<Form.Item
@@ -752,9 +712,8 @@ const BacktestList: React.FC = () => {
</div>
</Form>
</Modal>
</div>
</div >
)
}
export default BacktestList