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:
@@ -22,6 +22,8 @@ data class BacktestCreateRequest(
|
|||||||
val keywordFilterMode: String? = null, // 关键字过滤模式:DISABLED(不启用)、WHITELIST(白名单)、BLACKLIST(黑名单)
|
val keywordFilterMode: String? = null, // 关键字过滤模式:DISABLED(不启用)、WHITELIST(白名单)、BLACKLIST(黑名单)
|
||||||
val keywords: List<String>? = null, // 关键字列表
|
val keywords: List<String>? = null, // 关键字列表
|
||||||
val maxPositionValue: String? = null, // 最大仓位金额(USDC),NULL表示不启用
|
val maxPositionValue: String? = null, // 最大仓位金额(USDC),NULL表示不启用
|
||||||
|
val minPrice: String? = null, // 最低价格(可选),NULL表示不限制最低价
|
||||||
|
val maxPrice: String? = null, // 最高价格(可选),NULL表示不限制最高价
|
||||||
val pageForResume: Int? = null // 用于恢复中断任务,从指定页码开始获取历史数据(从1开始)
|
val pageForResume: Int? = null // 用于恢复中断任务,从指定页码开始获取历史数据(从1开始)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -166,7 +168,9 @@ data class BacktestConfigDto(
|
|||||||
val supportSell: Boolean,
|
val supportSell: Boolean,
|
||||||
val keywordFilterMode: String?,
|
val keywordFilterMode: String?,
|
||||||
val keywords: List<String>?,
|
val keywords: List<String>?,
|
||||||
val maxPositionValue: String?
|
val maxPositionValue: String?,
|
||||||
|
val minPrice: String?, // 最低价格(可选),NULL表示不限制最低价
|
||||||
|
val maxPrice: String? // 最高价格(可选),NULL表示不限制最高价
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -76,6 +76,12 @@ data class BacktestTask(
|
|||||||
@Column(name = "max_position_value", precision = 20, scale = 8)
|
@Column(name = "max_position_value", precision = 20, scale = 8)
|
||||||
val maxPositionValue: BigDecimal? = null, // 最大仓位金额(USDC),NULL表示不启用
|
val maxPositionValue: BigDecimal? = null, // 最大仓位金额(USDC),NULL表示不启用
|
||||||
|
|
||||||
|
@Column(name = "min_price", precision = 20, scale = 8)
|
||||||
|
val minPrice: BigDecimal? = null, // 最低价格(可选),NULL表示不限制最低价
|
||||||
|
|
||||||
|
@Column(name = "max_price", precision = 20, scale = 8)
|
||||||
|
val maxPrice: BigDecimal? = null, // 最高价格(可选),NULL表示不限制最高价
|
||||||
|
|
||||||
// 统计字段
|
// 统计字段
|
||||||
@Column(name = "avg_holding_time")
|
@Column(name = "avg_holding_time")
|
||||||
var avgHoldingTime: Long? = null, // 平均持仓时间(毫秒)
|
var avgHoldingTime: Long? = null, // 平均持仓时间(毫秒)
|
||||||
|
|||||||
+2
@@ -73,6 +73,8 @@ class BacktestExecutionService(
|
|||||||
minOrderDepth = null, // 回测无实时订单簿数据
|
minOrderDepth = null, // 回测无实时订单簿数据
|
||||||
maxSpread = null, // 回测无实时价差数据
|
maxSpread = null, // 回测无实时价差数据
|
||||||
maxPositionValue = task.maxPositionValue,
|
maxPositionValue = task.maxPositionValue,
|
||||||
|
minPrice = task.minPrice, // 最低价格
|
||||||
|
maxPrice = task.maxPrice, // 最高价格
|
||||||
keywordFilterMode = task.keywordFilterMode,
|
keywordFilterMode = task.keywordFilterMode,
|
||||||
keywords = task.keywords,
|
keywords = task.keywords,
|
||||||
configName = null,
|
configName = null,
|
||||||
|
|||||||
@@ -82,7 +82,9 @@ class BacktestService(
|
|||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
},
|
},
|
||||||
maxPositionValue = request.maxPositionValue?.toSafeBigDecimal()
|
maxPositionValue = request.maxPositionValue?.toSafeBigDecimal(),
|
||||||
|
minPrice = request.minPrice?.toSafeBigDecimal(),
|
||||||
|
maxPrice = request.maxPrice?.toSafeBigDecimal()
|
||||||
)
|
)
|
||||||
|
|
||||||
backtestTaskRepository.save(task)
|
backtestTaskRepository.save(task)
|
||||||
@@ -192,7 +194,9 @@ class BacktestService(
|
|||||||
} else {
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
},
|
},
|
||||||
maxPositionValue = task.maxPositionValue?.toPlainString()
|
maxPositionValue = task.maxPositionValue?.toPlainString(),
|
||||||
|
minPrice = task.minPrice?.toPlainString(),
|
||||||
|
maxPrice = task.maxPrice?.toPlainString()
|
||||||
)
|
)
|
||||||
|
|
||||||
val statistics = BacktestStatisticsDto(
|
val statistics = BacktestStatisticsDto(
|
||||||
@@ -379,7 +383,9 @@ class BacktestService(
|
|||||||
supportSell = source.supportSell,
|
supportSell = source.supportSell,
|
||||||
keywordFilterMode = source.keywordFilterMode,
|
keywordFilterMode = source.keywordFilterMode,
|
||||||
keywords = source.keywords,
|
keywords = source.keywords,
|
||||||
maxPositionValue = source.maxPositionValue
|
maxPositionValue = source.maxPositionValue,
|
||||||
|
minPrice = source.minPrice,
|
||||||
|
maxPrice = source.maxPrice
|
||||||
)
|
)
|
||||||
|
|
||||||
backtestTaskRepository.save(newTask)
|
backtestTaskRepository.save(newTask)
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- V32: 添加回测价格区间过滤字段
|
||||||
|
-- 用于配置价格区间,仅在指定价格区间内的订单才会跟单
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 添加价格区间字段到回测任务表
|
||||||
|
ALTER TABLE backtest_task
|
||||||
|
ADD COLUMN min_price DECIMAL(20, 8) NULL COMMENT '最低价格(可选),NULL表示不限制最低价',
|
||||||
|
ADD COLUMN max_price DECIMAL(20, 8) NULL COMMENT '最高价格(可选),NULL表示不限制最高价';
|
||||||
@@ -1363,6 +1363,10 @@
|
|||||||
"fixedAmountRequired": "Please enter fixed amount",
|
"fixedAmountRequired": "Please enter fixed amount",
|
||||||
"fixedAmountInvalid": "Fixed amount must be greater than 0",
|
"fixedAmountInvalid": "Fixed amount must be greater than 0",
|
||||||
"priceFilters": "Price Filters",
|
"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",
|
"keywordsPlaceholder": "Please enter keywords, press Enter to add",
|
||||||
"maxPositionValuePlaceholder": "Leave empty to disable max position limit",
|
"maxPositionValuePlaceholder": "Leave empty to disable max position limit",
|
||||||
"delaySecondsHint": "Delay execution to simulate real copy trading delay",
|
"delaySecondsHint": "Delay execution to simulate real copy trading delay",
|
||||||
|
|||||||
@@ -1363,6 +1363,10 @@
|
|||||||
"fixedAmountRequired": "请输入固定金额",
|
"fixedAmountRequired": "请输入固定金额",
|
||||||
"fixedAmountInvalid": "固定金额必须大于 0",
|
"fixedAmountInvalid": "固定金额必须大于 0",
|
||||||
"priceFilters": "价格过滤",
|
"priceFilters": "价格过滤",
|
||||||
|
"priceRange": "价格区间",
|
||||||
|
"priceRangeTooltip": "仅跟单 Leader 交易价格在指定区间内的订单。不填写表示不限制。示例:填写 0.11 和 0.89 表示仅跟单价格在 0.11 到 0.89 之间的订单;只填写最高价 0.89 表示仅跟单价格在 0.89 以下的订单;只填写最低价 0.11 表示仅跟单价格在 0.11 以上的订单。",
|
||||||
|
"minPricePlaceholder": "最低价(留空不限制)",
|
||||||
|
"maxPricePlaceholder": "最高价(留空不限制)",
|
||||||
"keywordsPlaceholder": "请输入关键字,按回车添加",
|
"keywordsPlaceholder": "请输入关键字,按回车添加",
|
||||||
"maxPositionValuePlaceholder": "留空表示不启用最大仓位限制",
|
"maxPositionValuePlaceholder": "留空表示不启用最大仓位限制",
|
||||||
"delaySecondsHint": "延迟执行模拟真实跟单延迟",
|
"delaySecondsHint": "延迟执行模拟真实跟单延迟",
|
||||||
|
|||||||
@@ -1363,6 +1363,10 @@
|
|||||||
"fixedAmountRequired": "請輸入固定金額",
|
"fixedAmountRequired": "請輸入固定金額",
|
||||||
"fixedAmountInvalid": "固定金額必須大於 0",
|
"fixedAmountInvalid": "固定金額必須大於 0",
|
||||||
"priceFilters": "價格過濾",
|
"priceFilters": "價格過濾",
|
||||||
|
"priceRange": "價格區間",
|
||||||
|
"priceRangeTooltip": "僅跟單 Leader 交易價格在指定區間內的訂單。不填寫表示不限制。示例:填寫 0.11 和 0.89 表示僅跟單價格在 0.11 到 0.89 之間的訂單;只填寫最高價 0.89 表示僅跟單價格在 0.89 以下的訂單;只填寫最低價 0.11 表示僅跟單價格在 0.11 以上的訂單。",
|
||||||
|
"minPricePlaceholder": "最低價(留空不限制)",
|
||||||
|
"maxPricePlaceholder": "最高價(留空不限制)",
|
||||||
"keywordsPlaceholder": "請輸入關鍵字,按回車添加",
|
"keywordsPlaceholder": "請輸入關鍵字,按回車添加",
|
||||||
"maxPositionValuePlaceholder": "留空表示不啟用最大倉位限制",
|
"maxPositionValuePlaceholder": "留空表示不啟用最大倉位限制",
|
||||||
"delaySecondsHint": "延遲執行模擬真實跟單延遲",
|
"delaySecondsHint": "延遲執行模擬真實跟單延遲",
|
||||||
|
|||||||
@@ -272,7 +272,9 @@ const BacktestList: React.FC = () => {
|
|||||||
supportSell: values.supportSell,
|
supportSell: values.supportSell,
|
||||||
keywordFilterMode: values.keywordFilterMode,
|
keywordFilterMode: values.keywordFilterMode,
|
||||||
keywords: values.keywords,
|
keywords: values.keywords,
|
||||||
maxPositionValue: values.maxPositionValue
|
maxPositionValue: values.maxPositionValue,
|
||||||
|
minPrice: values.minPrice,
|
||||||
|
maxPrice: values.maxPrice
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await backtestService.create(request)
|
const response = await backtestService.create(request)
|
||||||
@@ -324,6 +326,8 @@ const BacktestList: React.FC = () => {
|
|||||||
keywordFilterMode: taskConfig.keywordFilterMode || 'DISABLED',
|
keywordFilterMode: taskConfig.keywordFilterMode || 'DISABLED',
|
||||||
keywords: taskConfig.keywords || [],
|
keywords: taskConfig.keywords || [],
|
||||||
maxPositionValue: taskConfig.maxPositionValue,
|
maxPositionValue: taskConfig.maxPositionValue,
|
||||||
|
minPrice: taskConfig.minPrice,
|
||||||
|
maxPrice: taskConfig.maxPrice,
|
||||||
configName: `回测任务-${taskDetail.taskName}`
|
configName: `回测任务-${taskDetail.taskName}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -992,6 +996,50 @@ const BacktestList: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</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
|
<Form.Item
|
||||||
label={t('backtest.supportSell')}
|
label={t('backtest.supportSell')}
|
||||||
name="supportSell"
|
name="supportSell"
|
||||||
@@ -1078,6 +1126,8 @@ const BacktestList: React.FC = () => {
|
|||||||
keywordFilterMode: detailConfig.keywordFilterMode,
|
keywordFilterMode: detailConfig.keywordFilterMode,
|
||||||
keywords: detailConfig.keywords || [],
|
keywords: detailConfig.keywords || [],
|
||||||
maxPositionValue: detailConfig.maxPositionValue,
|
maxPositionValue: detailConfig.maxPositionValue,
|
||||||
|
minPrice: detailConfig.minPrice,
|
||||||
|
maxPrice: detailConfig.maxPrice,
|
||||||
configName: `回测任务-${detailTask.taskName}`
|
configName: `回测任务-${detailTask.taskName}`
|
||||||
}
|
}
|
||||||
setPreFilledConfig(preFilled)
|
setPreFilledConfig(preFilled)
|
||||||
@@ -1268,6 +1318,20 @@ const BacktestList: React.FC = () => {
|
|||||||
{formatUSDC(detailConfig.maxPositionValue)} USDC
|
{formatUSDC(detailConfig.maxPositionValue)} USDC
|
||||||
</Descriptions.Item>
|
</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>
|
</Descriptions>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user