feat: 回测任务支持价格区间过滤

- 后端:
  - BacktestTask 实体类添加 minPrice 和 maxPrice 字段
  - BacktestDto 添加价格区间参数和配置字段
  - BacktestService 支持创建和查询价格区间配置
  - BacktestExecutionService 在 taskToCopyTrading 中设置价格区间
  - 添加数据库迁移脚本 V32__add_backtest_price_range_filter.sql

- 前端:
  - BacktestList 添加价格区间输入表单(最低价、最高价)
  - 详情页面显示价格区间配置
  - 一键创建跟单配置时包含价格区间
  - 添加中英繁三语言翻译
  - 修复价格输入框精度显示问题(0.4 不再显示为 0.40000000)

功能说明:
- 价格区间过滤与跟单配置保持一致,确保回测结果能准确反映实际跟单效果
- 支持只设置最低价、只设置最高价、或同时设置两者
- 留空表示不限制价格

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WrBug
2026-02-11 23:54:49 +08:00
co-authored by Cursor
parent 99c4328830
commit a067a20a02
9 changed files with 108 additions and 5 deletions
+4
View File
@@ -1363,6 +1363,10 @@
"fixedAmountRequired": "Please enter fixed amount",
"fixedAmountInvalid": "Fixed amount must be greater than 0",
"priceFilters": "Price Filters",
"priceRange": "Price Range",
"priceRangeTooltip": "Only copy Leader orders with price within the specified range. Leave empty to disable. Example: Enter 0.11 and 0.89 to only copy orders with price between 0.11 and 0.89; enter only max price 0.89 to only copy orders with price below 0.89; enter only min price 0.11 to only copy orders with price above 0.11.",
"minPricePlaceholder": "Min price (leave empty to disable)",
"maxPricePlaceholder": "Max price (leave empty to disable)",
"keywordsPlaceholder": "Please enter keywords, press Enter to add",
"maxPositionValuePlaceholder": "Leave empty to disable max position limit",
"delaySecondsHint": "Delay execution to simulate real copy trading delay",
+4
View File
@@ -1363,6 +1363,10 @@
"fixedAmountRequired": "请输入固定金额",
"fixedAmountInvalid": "固定金额必须大于 0",
"priceFilters": "价格过滤",
"priceRange": "价格区间",
"priceRangeTooltip": "仅跟单 Leader 交易价格在指定区间内的订单。不填写表示不限制。示例:填写 0.11 和 0.89 表示仅跟单价格在 0.11 到 0.89 之间的订单;只填写最高价 0.89 表示仅跟单价格在 0.89 以下的订单;只填写最低价 0.11 表示仅跟单价格在 0.11 以上的订单。",
"minPricePlaceholder": "最低价(留空不限制)",
"maxPricePlaceholder": "最高价(留空不限制)",
"keywordsPlaceholder": "请输入关键字,按回车添加",
"maxPositionValuePlaceholder": "留空表示不启用最大仓位限制",
"delaySecondsHint": "延迟执行模拟真实跟单延迟",
+4
View File
@@ -1363,6 +1363,10 @@
"fixedAmountRequired": "請輸入固定金額",
"fixedAmountInvalid": "固定金額必須大於 0",
"priceFilters": "價格過濾",
"priceRange": "價格區間",
"priceRangeTooltip": "僅跟單 Leader 交易價格在指定區間內的訂單。不填寫表示不限制。示例:填寫 0.11 和 0.89 表示僅跟單價格在 0.11 到 0.89 之間的訂單;只填寫最高價 0.89 表示僅跟單價格在 0.89 以下的訂單;只填寫最低價 0.11 表示僅跟單價格在 0.11 以上的訂單。",
"minPricePlaceholder": "最低價(留空不限制)",
"maxPricePlaceholder": "最高價(留空不限制)",
"keywordsPlaceholder": "請輸入關鍵字,按回車添加",
"maxPositionValuePlaceholder": "留空表示不啟用最大倉位限制",
"delaySecondsHint": "延遲執行模擬真實跟單延遲",
+65 -1
View File
@@ -272,7 +272,9 @@ const BacktestList: React.FC = () => {
supportSell: values.supportSell,
keywordFilterMode: values.keywordFilterMode,
keywords: values.keywords,
maxPositionValue: values.maxPositionValue
maxPositionValue: values.maxPositionValue,
minPrice: values.minPrice,
maxPrice: values.maxPrice
}
const response = await backtestService.create(request)
@@ -324,6 +326,8 @@ const BacktestList: React.FC = () => {
keywordFilterMode: taskConfig.keywordFilterMode || 'DISABLED',
keywords: taskConfig.keywords || [],
maxPositionValue: taskConfig.maxPositionValue,
minPrice: taskConfig.minPrice,
maxPrice: taskConfig.maxPrice,
configName: `回测任务-${taskDetail.taskName}`
}
@@ -992,6 +996,50 @@ const BacktestList: React.FC = () => {
/>
</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"
@@ -1078,6 +1126,8 @@ const BacktestList: React.FC = () => {
keywordFilterMode: detailConfig.keywordFilterMode,
keywords: detailConfig.keywords || [],
maxPositionValue: detailConfig.maxPositionValue,
minPrice: detailConfig.minPrice,
maxPrice: detailConfig.maxPrice,
configName: `回测任务-${detailTask.taskName}`
}
setPreFilledConfig(preFilled)
@@ -1268,6 +1318,20 @@ const BacktestList: React.FC = () => {
{formatUSDC(detailConfig.maxPositionValue)} USDC
</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>
)}