feat(backtest): 回测结算持久化、按配置重新测试、执行时以当前时间为窗口基准
- 持久化: BUY/SELL 与 SETTLEMENT(WIN/LOSE/UNKNOWN/CLOSED) 均写入 backtest_trade - 重新测试: 已完成任务支持「按当前配置重新测试」,新任务名称可编辑,后端 POST /tasks/rerun + 前端按钮与确认弹窗 - 回测窗口: 首次执行以当前时间为终点、startTime = endTime - backtestDays(局部变量,不修改实体) - 新增错误码 BACKTEST_TASK_NOT_COMPLETED、SERVER_BACKTEST_RERUN_FAILED 及多语言 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import React from 'react'
|
||||
import { Select } from 'antd'
|
||||
import type { Leader } from '../types'
|
||||
|
||||
const { Option } = Select
|
||||
|
||||
interface LeaderSelectProps {
|
||||
value?: number
|
||||
onChange?: (value: number | undefined) => void
|
||||
onSelectChange?: (value: number | undefined) => void // 选择变化时的回调
|
||||
leaders: Leader[]
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
showSearch?: boolean
|
||||
allowClear?: boolean
|
||||
notFoundContent?: React.ReactNode
|
||||
}
|
||||
|
||||
const LeaderSelect: React.FC<LeaderSelectProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
onSelectChange,
|
||||
leaders,
|
||||
placeholder,
|
||||
disabled,
|
||||
showSearch = true,
|
||||
allowClear = false,
|
||||
notFoundContent
|
||||
}) => {
|
||||
// 处理选择变化
|
||||
const handleChange = (val: number | undefined) => {
|
||||
if (onChange) {
|
||||
onChange(val)
|
||||
}
|
||||
if (onSelectChange) {
|
||||
onSelectChange(val)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
showSearch={showSearch}
|
||||
allowClear={allowClear}
|
||||
notFoundContent={notFoundContent}
|
||||
optionFilterProp="label"
|
||||
optionLabelProp="label"
|
||||
>
|
||||
{leaders.map(leader => {
|
||||
const label = leader.leaderName || `Leader ${leader.id}`
|
||||
return (
|
||||
<Option key={leader.id} value={leader.id} label={label}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<span>{label}</span>
|
||||
<span style={{ fontSize: '12px', color: '#999' }}>{leader.leaderAddress}</span>
|
||||
</div>
|
||||
</Option>
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
|
||||
export default LeaderSelect
|
||||
@@ -1363,6 +1363,11 @@
|
||||
"supportSellHint": "Whether to follow Leader sell orders",
|
||||
"sortBy": "Sort By",
|
||||
"sortOrder": "Sort Order",
|
||||
"createdAt": "Created At"
|
||||
"createdAt": "Created At",
|
||||
"rerun": "Re-run",
|
||||
"rerunConfirm": "Create a new backtest task with the same config?",
|
||||
"rerunTaskNamePlaceholder": "New task name (leave empty for \"Original name (copy)\")",
|
||||
"rerunSuccess": "New backtest task created",
|
||||
"rerunFailed": "Re-run failed"
|
||||
}
|
||||
}
|
||||
@@ -1363,6 +1363,11 @@
|
||||
"supportSellHint": "是否跟随 Leader 卖出",
|
||||
"sortBy": "排序字段",
|
||||
"sortOrder": "排序顺序",
|
||||
"createdAt": "创建时间"
|
||||
"createdAt": "创建时间",
|
||||
"rerun": "重新测试",
|
||||
"rerunConfirm": "将按当前配置创建新的回测任务,是否继续?",
|
||||
"rerunTaskNamePlaceholder": "新任务名称(留空使用「原名称 (副本)」)",
|
||||
"rerunSuccess": "已创建新回测任务",
|
||||
"rerunFailed": "重新测试失败"
|
||||
}
|
||||
}
|
||||
@@ -1363,6 +1363,11 @@
|
||||
"supportSellHint": "是否跟隨 Leader 賣出",
|
||||
"sortBy": "排序欄位",
|
||||
"sortOrder": "排序順序",
|
||||
"createdAt": "創建時間"
|
||||
"createdAt": "創建時間",
|
||||
"rerun": "重新測試",
|
||||
"rerunConfirm": "將按當前配置創建新的回測任務,是否繼續?",
|
||||
"rerunTaskNamePlaceholder": "新任務名稱(留空使用「原名稱 (副本)」)",
|
||||
"rerunSuccess": "已創建新回測任務",
|
||||
"rerunFailed": "重新測試失敗"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
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 { PlusOutlined, ReloadOutlined, DeleteOutlined, StopOutlined, EyeOutlined, RedoOutlined, CopyOutlined } from '@ant-design/icons'
|
||||
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'
|
||||
@@ -9,8 +9,7 @@ import type { Leader } from '../types'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import AddCopyTradingModal from './CopyTradingOrders/AddModal'
|
||||
import BacktestChart from './BacktestChart'
|
||||
|
||||
const { Option } = Select
|
||||
import LeaderSelect from '../components/LeaderSelect'
|
||||
|
||||
const BacktestList: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
@@ -36,6 +35,12 @@ const BacktestList: React.FC = () => {
|
||||
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)
|
||||
@@ -130,6 +135,38 @@ const BacktestList: React.FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 按配置重新测试(仅已完成任务)
|
||||
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({
|
||||
@@ -545,14 +582,24 @@ const BacktestList: React.FC = () => {
|
||||
{t('common.viewDetail')}
|
||||
</Button>
|
||||
{record.status === 'COMPLETED' && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCreateCopyTrading(record)}
|
||||
>
|
||||
{t('backtest.createCopyTrading')}
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<SyncOutlined />}
|
||||
onClick={() => handleRerun(record)}
|
||||
>
|
||||
{t('backtest.rerun')}
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCreateCopyTrading(record)}
|
||||
>
|
||||
{t('backtest.createCopyTrading')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{record.status === 'RUNNING' && (
|
||||
<Button
|
||||
@@ -675,6 +722,30 @@ const BacktestList: React.FC = () => {
|
||||
</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')}
|
||||
@@ -719,16 +790,10 @@ const BacktestList: React.FC = () => {
|
||||
name="leaderId"
|
||||
rules={[{ required: true, message: t('backtest.leaderRequired') || '请选择 Leader' }]}
|
||||
>
|
||||
<Select placeholder={t('backtest.leader')} showSearch>
|
||||
{leaders.map((leader) => (
|
||||
<Option key={leader.id} value={leader.id}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<span>{leader.leaderName || `Leader ${leader.id}`}</span>
|
||||
<span style={{ fontSize: '12px', color: '#999' }}>{leader.leaderAddress}</span>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<LeaderSelect
|
||||
leaders={leaders}
|
||||
placeholder={t('backtest.leader')}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -945,6 +1010,12 @@ const BacktestList: React.FC = () => {
|
||||
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,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import AccountImportForm from '../../components/AccountImportForm'
|
||||
import LeaderAddForm from '../../components/LeaderAddForm'
|
||||
import LeaderSelect from '../../components/LeaderSelect'
|
||||
|
||||
const { Option } = Select
|
||||
|
||||
@@ -467,8 +468,10 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
name="leaderId"
|
||||
rules={[{ required: true, message: t('copyTradingAdd.leaderRequired') || '请选择 Leader' }]}
|
||||
>
|
||||
<Select
|
||||
<LeaderSelect
|
||||
leaders={leaders}
|
||||
placeholder={t('copyTradingAdd.selectLeaderPlaceholder') || '请选择 Leader'}
|
||||
onSelectChange={(value) => fetchLeaderAssetInfo(value)}
|
||||
notFoundContent={
|
||||
leaders.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '12px' }}>
|
||||
@@ -484,14 +487,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
onChange={(value) => fetchLeaderAssetInfo(value)}
|
||||
>
|
||||
{leaders.map(leader => (
|
||||
<Option key={leader.id} value={leader.id}>
|
||||
{leader.leaderName || `Leader ${leader.id}`} ({leader.leaderAddress.slice(0, 6)}...{leader.leaderAddress.slice(-4)})
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* Leader 资产信息 */}
|
||||
|
||||
@@ -323,7 +323,10 @@ const EditModal: React.FC<EditModalProps> = ({
|
||||
>
|
||||
<Select disabled>
|
||||
<Option value={copyTrading.leaderId}>
|
||||
{copyTrading.leaderName || `Leader ${copyTrading.leaderId}`} ({copyTrading.leaderAddress.slice(0, 6)}...{copyTrading.leaderAddress.slice(-4)})
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<span>{copyTrading.leaderName || `Leader ${copyTrading.leaderId}`}</span>
|
||||
<span style={{ fontSize: '12px', color: '#999' }}>{copyTrading.leaderAddress}</span>
|
||||
</div>
|
||||
</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
@@ -797,6 +797,11 @@ export const backtestService = {
|
||||
/**
|
||||
* 重试回测任务
|
||||
*/
|
||||
retry: (data: { id: number }) => apiClient.post('/backtest/tasks/retry', data)
|
||||
retry: (data: { id: number }) => apiClient.post('/backtest/tasks/retry', data),
|
||||
|
||||
/**
|
||||
* 按当前配置重新测试(仅支持已完成任务,创建同名配置的新任务)
|
||||
*/
|
||||
rerun: (data: { id: number; taskName?: string }) => apiClient.post('/backtest/tasks/rerun', data)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user