fix: 优化回测功能UI和修复问题
## 主要修改 ### UI 优化 - 将回测功能移到跟单交易菜单下作为子菜单 - 创建回测改为使用 Modal 方式(移除独立页面) - 优化导航路径,使用 useNavigate 替代 window.location.href ### 功能修复 - 修复 API 路径问题:移除重复的 /api 前缀(/api/backtest -> /backtest) - 修复 copyRatio 输入框单位:改为百分比输入(100 表示 100%),与跟单配置保持一致 - 修复回测列表中 leader 列显示为空的问题:优先显示 leaderName,为空时显示 leaderAddress ### 多语言完善 - 补充缺失的多语言 key: - common.ascending / common.descending / common.day - backtest.sortBy / backtest.sortOrder / backtest.createdAt - backtest.copyRatioTooltip / backtest.copyRatioPlaceholder - 以及其他表单验证相关的 key - 为中文简体、中文繁体、英文三种语言都添加了完整的翻译 ## 修改的文件 - frontend/src/App.tsx - 移除 BacktestCreate 路由 - frontend/src/components/Layout.tsx - 调整菜单结构 - frontend/src/pages/BacktestList.tsx - Modal 创建、修复 leader 显示 - frontend/src/pages/BacktestDetail.tsx - 优化导航 - frontend/src/pages/BacktestCreate.tsx - 同步 copyRatio 修改 - frontend/src/services/api.ts - 修复 API 路径 - frontend/src/locales/* - 补充多语言配置
This commit is contained in:
@@ -33,7 +33,6 @@ import ApiHealthStatus from './pages/ApiHealthStatus'
|
||||
import RpcNodeSettings from './pages/RpcNodeSettings'
|
||||
import Announcements from './pages/Announcements'
|
||||
import BacktestList from './pages/BacktestList'
|
||||
import BacktestCreate from './pages/BacktestCreate'
|
||||
import BacktestDetail from './pages/BacktestDetail'
|
||||
import { wsManager } from './services/websocket'
|
||||
import type { OrderPushMessage } from './types'
|
||||
@@ -258,7 +257,6 @@ function App() {
|
||||
<Route path="/copy-trading/orders/matched/:copyTradingId" element={<ProtectedRoute><CopyTradingMatchedOrders /></ProtectedRoute>} />
|
||||
<Route path="/copy-trading/filtered-orders/:id" element={<ProtectedRoute><FilteredOrdersList /></ProtectedRoute>} />
|
||||
<Route path="/backtest" element={<ProtectedRoute><BacktestList /></ProtectedRoute>} />
|
||||
<Route path="/backtest/create" element={<ProtectedRoute><BacktestCreate /></ProtectedRoute>} />
|
||||
<Route path="/backtest/detail/:id" element={<ProtectedRoute><BacktestDetail /></ProtectedRoute>} />
|
||||
<Route path="/config" element={<ProtectedRoute><ConfigPage /></ProtectedRoute>} />
|
||||
<Route path="/positions" element={<ProtectedRoute><PositionList /></ProtectedRoute>} />
|
||||
|
||||
@@ -72,7 +72,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
const getInitialOpenKeys = (): string[] => {
|
||||
const path = location.pathname
|
||||
const keys: string[] = []
|
||||
if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading')) {
|
||||
if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading') || path.startsWith('/backtest')) {
|
||||
keys.push('/copy-trading-management')
|
||||
}
|
||||
if (path.startsWith('/system-settings')) {
|
||||
@@ -87,7 +87,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
useEffect(() => {
|
||||
const path = location.pathname
|
||||
const keys: string[] = []
|
||||
if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading')) {
|
||||
if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading') || path.startsWith('/backtest')) {
|
||||
keys.push('/copy-trading-management')
|
||||
}
|
||||
if (path.startsWith('/system-settings')) {
|
||||
@@ -149,6 +149,11 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
key: '/templates',
|
||||
icon: <FileTextOutlined />,
|
||||
label: t('menu.templates')
|
||||
},
|
||||
{
|
||||
key: '/backtest',
|
||||
icon: <LineChartOutlined />,
|
||||
label: t('menu.backtest') || '回测'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -157,11 +162,6 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
icon: <UnorderedListOutlined />,
|
||||
label: t('menu.positions')
|
||||
},
|
||||
{
|
||||
key: '/backtest',
|
||||
icon: <LineChartOutlined />,
|
||||
label: t('menu.backtest') || '回测'
|
||||
},
|
||||
{
|
||||
key: '/statistics',
|
||||
icon: <BarChartOutlined />,
|
||||
|
||||
@@ -34,7 +34,10 @@
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"page": "Page",
|
||||
"pageOf": "Page"
|
||||
"pageOf": "Page",
|
||||
"ascending": "Ascending",
|
||||
"descending": "Descending",
|
||||
"day": "day"
|
||||
},
|
||||
"account": {
|
||||
"title": "Account Management",
|
||||
@@ -1326,6 +1329,25 @@
|
||||
"starting": "Starting...",
|
||||
"stopping": "Stopping...",
|
||||
"errorOccurred": "Error occurred",
|
||||
"retry": "Retry"
|
||||
"retry": "Retry",
|
||||
"taskNameRequired": "Please enter task name",
|
||||
"leaderRequired": "Please select Leader",
|
||||
"initialBalanceRequired": "Please enter initial balance",
|
||||
"initialBalanceInvalid": "Initial balance must be greater than 0",
|
||||
"backtestDaysRequired": "Please enter backtest days",
|
||||
"backtestDaysInvalid": "Backtest days must be between 1-15",
|
||||
"copyRatioRequired": "Please enter copy ratio",
|
||||
"copyRatioInvalid": "Copy ratio must be between 0.01-10000",
|
||||
"copyRatioPlaceholder": "For example: 100 means 100% (1:1 copy), default 100%",
|
||||
"copyRatioTooltip": "Copy ratio represents the percentage of copy amount relative to Leader order amount. For example: 100% means 1:1 copy, 50% means half position copy, 200% means double copy",
|
||||
"fixedAmountRequired": "Please enter fixed amount",
|
||||
"fixedAmountInvalid": "Fixed amount must be greater than 0",
|
||||
"advancedFilters": "Advanced Filters",
|
||||
"keywordsPlaceholder": "Please enter keywords, press Enter to add",
|
||||
"delaySecondsHint": "Delay execution to simulate real copy trading delay",
|
||||
"supportSellHint": "Whether to follow Leader's sell operations",
|
||||
"sortBy": "Sort By",
|
||||
"sortOrder": "Sort Order",
|
||||
"createdAt": "Created At"
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,10 @@
|
||||
"pageOf": "第",
|
||||
"success": "成功",
|
||||
"failed": "失败",
|
||||
"close": "关闭"
|
||||
"close": "关闭",
|
||||
"ascending": "升序",
|
||||
"descending": "降序",
|
||||
"day": "天"
|
||||
},
|
||||
"login": {
|
||||
"title": "登录",
|
||||
@@ -1326,6 +1329,25 @@
|
||||
"starting": "启动中...",
|
||||
"stopping": "停止中...",
|
||||
"errorOccurred": "发生错误",
|
||||
"retry": "重试"
|
||||
"retry": "重试",
|
||||
"taskNameRequired": "请输入任务名称",
|
||||
"leaderRequired": "请选择 Leader",
|
||||
"initialBalanceRequired": "请输入初始资金",
|
||||
"initialBalanceInvalid": "初始资金必须大于 0",
|
||||
"backtestDaysRequired": "请输入回测天数",
|
||||
"backtestDaysInvalid": "回测天数必须在 1-15 之间",
|
||||
"copyRatioRequired": "请输入跟单比例",
|
||||
"copyRatioInvalid": "跟单比例必须在 0.01-10000 之间",
|
||||
"copyRatioPlaceholder": "例如:100 表示 100%(1:1 跟单),默认 100%",
|
||||
"copyRatioTooltip": "跟单比例表示跟单金额相对于 Leader 订单金额的百分比。例如:100% 表示 1:1 跟单,50% 表示半仓跟单,200% 表示双倍跟单",
|
||||
"fixedAmountRequired": "请输入固定金额",
|
||||
"fixedAmountInvalid": "固定金额必须大于 0",
|
||||
"advancedFilters": "高级过滤",
|
||||
"keywordsPlaceholder": "请输入关键字,按回车添加",
|
||||
"delaySecondsHint": "延迟执行模拟真实跟单延迟",
|
||||
"supportSellHint": "是否跟随 Leader 的卖出操作",
|
||||
"sortBy": "排序字段",
|
||||
"sortOrder": "排序顺序",
|
||||
"createdAt": "创建时间"
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,10 @@
|
||||
"previous": "上一頁",
|
||||
"next": "下一頁",
|
||||
"page": "頁",
|
||||
"pageOf": "第"
|
||||
"pageOf": "第",
|
||||
"ascending": "升序",
|
||||
"descending": "降序",
|
||||
"day": "天"
|
||||
},
|
||||
"account": {
|
||||
"title": "賬戶管理",
|
||||
@@ -1326,6 +1329,25 @@
|
||||
"starting": "啟動中...",
|
||||
"stopping": "停止中...",
|
||||
"errorOccurred": "發生錯誤",
|
||||
"retry": "重試"
|
||||
"retry": "重試",
|
||||
"taskNameRequired": "請輸入任務名稱",
|
||||
"leaderRequired": "請選擇 Leader",
|
||||
"initialBalanceRequired": "請輸入初始資金",
|
||||
"initialBalanceInvalid": "初始資金必須大於 0",
|
||||
"backtestDaysRequired": "請輸入回測天數",
|
||||
"backtestDaysInvalid": "回測天數必須在 1-15 之間",
|
||||
"copyRatioRequired": "請輸入跟單比例",
|
||||
"copyRatioInvalid": "跟單比例必須在 0.01-10000 之間",
|
||||
"copyRatioPlaceholder": "例如:100 表示 100%(1:1 跟單),預設 100%",
|
||||
"copyRatioTooltip": "跟單比例表示跟單金額相對於 Leader 訂單金額的百分比。例如:100% 表示 1:1 跟單,50% 表示半倉跟單,200% 表示雙倍跟單",
|
||||
"fixedAmountRequired": "請輸入固定金額",
|
||||
"fixedAmountInvalid": "固定金額必須大於 0",
|
||||
"advancedFilters": "進階過濾",
|
||||
"keywordsPlaceholder": "請輸入關鍵字,按回車添加",
|
||||
"delaySecondsHint": "延遲執行模擬真實跟單延遲",
|
||||
"supportSellHint": "是否跟隨 Leader 的賣出操作",
|
||||
"sortBy": "排序欄位",
|
||||
"sortOrder": "排序順序",
|
||||
"createdAt": "創建時間"
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ const BacktestCreate: React.FC = () => {
|
||||
initialBalance: values.initialBalance,
|
||||
backtestDays: values.backtestDays,
|
||||
copyMode: values.copyMode || 'RATIO',
|
||||
copyRatio: values.copyMode === 'RATIO' ? values.copyRatio : undefined,
|
||||
copyRatio: values.copyMode === 'RATIO' && values.copyRatio ? (values.copyRatio / 100).toString() : undefined,
|
||||
fixedAmount: values.copyMode === 'FIXED' ? values.fixedAmount : undefined,
|
||||
maxOrderSize: values.maxOrderSize,
|
||||
minOrderSize: values.minOrderSize,
|
||||
@@ -88,7 +88,7 @@ const BacktestCreate: React.FC = () => {
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({
|
||||
copyMode: 'RATIO',
|
||||
copyRatio: 1.0,
|
||||
copyRatio: 100, // 默认 100%(显示为百分比)
|
||||
maxOrderSize: 1000,
|
||||
minOrderSize: 1,
|
||||
maxDailyLoss: 500,
|
||||
@@ -117,7 +117,7 @@ const BacktestCreate: React.FC = () => {
|
||||
onFinish={handleSubmit}
|
||||
initialValues={{
|
||||
copyMode: 'RATIO',
|
||||
copyRatio: 1.0,
|
||||
copyRatio: 100, // 默认 100%(显示为百分比)
|
||||
maxOrderSize: 1000,
|
||||
minOrderSize: 1,
|
||||
maxDailyLoss: 500,
|
||||
@@ -212,18 +212,32 @@ const BacktestCreate: React.FC = () => {
|
||||
<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: 10, message: t('backtest.copyRatioInvalid') || '跟单比例必须在 0.01-10 之间' }
|
||||
{ type: 'number', min: 0.01, max: 10000, message: t('backtest.copyRatioInvalid') || '跟单比例必须在 0.01-10000 之间' }
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('backtest.copyRatio')}
|
||||
precision={2}
|
||||
min={0.01}
|
||||
max={10}
|
||||
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>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { Card, Descriptions, Button, Tag, Space, Table, message, Row, Col, Statistic, Spin } from 'antd'
|
||||
import { ArrowLeftOutlined, ReloadOutlined, DeleteOutlined, StopOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -10,6 +10,7 @@ import BacktestChart from './BacktestChart'
|
||||
|
||||
const BacktestDetail: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [task, setTask] = useState<BacktestTaskDto | null>(null)
|
||||
@@ -102,7 +103,7 @@ const BacktestDetail: React.FC = () => {
|
||||
|
||||
// 返回
|
||||
const handleBack = () => {
|
||||
window.location.href = '/backtest/list'
|
||||
navigate('/backtest')
|
||||
}
|
||||
|
||||
// 停止任务
|
||||
@@ -136,7 +137,7 @@ const BacktestDetail: React.FC = () => {
|
||||
const response = await backtestService.delete({ id: parseInt(id!) })
|
||||
if (response.data.code === 0) {
|
||||
message.success(t('backtest.deleteSuccess'))
|
||||
window.location.href = '/backtest/list'
|
||||
navigate('/backtest')
|
||||
} else {
|
||||
message.error(response.data.msg || t('backtest.deleteFailed'))
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Table, Card, Button, Select, Tag, Space, Modal, message, Row, Col } from 'antd'
|
||||
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 { formatUSDC } from '../utils'
|
||||
import { backtestService } from '../services/api'
|
||||
import type { BacktestTaskDto, BacktestListRequest } from '../types/backtest'
|
||||
import { backtestService, apiService } from '../services/api'
|
||||
import type { BacktestTaskDto, BacktestListRequest, BacktestCreateRequest } from '../types/backtest'
|
||||
import type { Leader } from '../types'
|
||||
|
||||
const { Option } = Select
|
||||
|
||||
const BacktestList: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [tasks, setTasks] = useState<BacktestTaskDto[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
@@ -17,6 +22,13 @@ 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()
|
||||
const [createLoading, setCreateLoading] = useState(false)
|
||||
const [leaders, setLeaders] = useState<Leader[]>([])
|
||||
const [copyMode, setCopyMode] = useState<'RATIO' | 'FIXED'>('RATIO')
|
||||
|
||||
// 获取回测任务列表
|
||||
const fetchTasks = async () => {
|
||||
@@ -100,14 +112,94 @@ const BacktestList: React.FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 获取 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) => {
|
||||
window.location.href = `/backtest/detail?id=${id}`
|
||||
navigate(`/backtest/detail/${id}`)
|
||||
}
|
||||
|
||||
// 创建新任务
|
||||
// 打开创建 modal
|
||||
const handleCreate = () => {
|
||||
window.location.href = '/backtest/create'
|
||||
setCreateModalVisible(true)
|
||||
createForm.resetFields()
|
||||
createForm.setFieldsValue({
|
||||
copyMode: 'RATIO',
|
||||
copyRatio: 100, // 默认 100%(显示为百分比)
|
||||
maxOrderSize: 1000,
|
||||
minOrderSize: 1,
|
||||
maxDailyLoss: 500,
|
||||
maxDailyOrders: 50,
|
||||
priceTolerance: 5,
|
||||
delaySeconds: 0,
|
||||
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,
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 状态标签颜色
|
||||
@@ -145,7 +237,8 @@ const BacktestList: React.FC = () => {
|
||||
title: t('backtest.leader'),
|
||||
dataIndex: 'leaderName',
|
||||
key: 'leaderName',
|
||||
width: 150
|
||||
width: 150,
|
||||
render: (_: any, record: BacktestTaskDto) => record.leaderName || record.leaderAddress || '-'
|
||||
},
|
||||
{
|
||||
title: t('backtest.initialBalance'),
|
||||
@@ -356,6 +449,309 @@ const BacktestList: React.FC = () => {
|
||||
/>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* 创建回测任务 Modal */}
|
||||
<Modal
|
||||
title={t('backtest.createTask')}
|
||||
open={createModalVisible}
|
||||
onCancel={() => {
|
||||
setCreateModalVisible(false)
|
||||
createForm.resetFields()
|
||||
}}
|
||||
onOk={handleCreateSubmit}
|
||||
okText={t('common.save')}
|
||||
cancelText={t('common.cancel')}
|
||||
width={800}
|
||||
confirmLoading={createLoading}
|
||||
destroyOnClose
|
||||
style={{ top: 20 }}
|
||||
bodyStyle={{ maxHeight: '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}>
|
||||
<Form.Item
|
||||
label={t('backtest.taskName')}
|
||||
name="taskName"
|
||||
rules={[{ required: true, message: t('backtest.taskNameRequired') || '请输入任务名称' }]}
|
||||
>
|
||||
<Input placeholder={t('backtest.taskName')} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label={t('backtest.leader')}
|
||||
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}>
|
||||
{leader.leaderName || leader.leaderAddress}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label={t('backtest.initialBalance') + ' (USDC)'}
|
||||
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 span={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)}>
|
||||
<Option value="RATIO">{t('backtest.copyModeRatio')}</Option>
|
||||
<Option value="FIXED">{t('backtest.copyModeFixed')}</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') + ' (USDC)'}
|
||||
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 span={12}>
|
||||
<Form.Item
|
||||
label={t('backtest.maxOrderSize') + ' (USDC)'}
|
||||
name="maxOrderSize"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<InputNumber style={{ width: '100%' }} precision={2} min={1} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label={t('backtest.minOrderSize') + ' (USDC)'}
|
||||
name="minOrderSize"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<InputNumber style={{ width: '100%' }} precision={2} min={1} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label={t('backtest.maxDailyLoss') + ' (USDC)'}
|
||||
name="maxDailyLoss"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<InputNumber style={{ width: '100%' }} precision={2} min={0} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={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.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} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('backtest.keywordFilterMode')}
|
||||
name="keywordFilterMode"
|
||||
>
|
||||
<Select>
|
||||
<Option value="DISABLED">{t('backtest.keywordFilterModeDisabled')}</Option>
|
||||
<Option value="WHITELIST">{t('backtest.keywordFilterModeWhitelist')}</Option>
|
||||
<Option value="BLACKLIST">{t('backtest.keywordFilterModeBlacklist')}</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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -756,7 +756,7 @@ export const backtestService = {
|
||||
keywordFilterMode?: 'DISABLED' | 'WHITELIST' | 'BLACKLIST'
|
||||
keywords?: string[]
|
||||
maxMarketEndDate?: number | null
|
||||
}) => apiClient.post('/api/backtest/tasks', data),
|
||||
}) => apiClient.post('/backtest/tasks', data),
|
||||
|
||||
/**
|
||||
* 查询回测任务列表
|
||||
@@ -768,12 +768,12 @@ export const backtestService = {
|
||||
sortOrder?: 'asc' | 'desc'
|
||||
page: number
|
||||
size: number
|
||||
}) => apiClient.post('/api/backtest/tasks/list', data),
|
||||
}) => apiClient.post('/backtest/tasks/list', data),
|
||||
|
||||
/**
|
||||
* 查询回测任务详情
|
||||
*/
|
||||
detail: (data: { id: number }) => apiClient.post('/api/backtest/tasks/detail', data),
|
||||
detail: (data: { id: number }) => apiClient.post('/backtest/tasks/detail', data),
|
||||
|
||||
/**
|
||||
* 查询回测交易记录
|
||||
@@ -782,16 +782,16 @@ export const backtestService = {
|
||||
taskId: number
|
||||
page: number
|
||||
size: number
|
||||
}) => apiClient.post('/api/backtest/tasks/trades', data),
|
||||
}) => apiClient.post('/backtest/tasks/trades', data),
|
||||
|
||||
/**
|
||||
* 停止回测任务
|
||||
*/
|
||||
stop: (data: { id: number }) => apiClient.post('/api/backtest/tasks/stop', data),
|
||||
stop: (data: { id: number }) => apiClient.post('/backtest/tasks/stop', data),
|
||||
|
||||
/**
|
||||
* 删除回测任务
|
||||
*/
|
||||
delete: (data: { id: number }) => apiClient.post('/api/backtest/tasks/delete', data)
|
||||
delete: (data: { id: number }) => apiClient.post('/backtest/tasks/delete', data)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user