From a067a20a024a9fdcb9d5a370ef1f1460717334e6 Mon Sep 17 00:00:00 2001 From: WrBug Date: Wed, 11 Feb 2026 23:54:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9B=9E=E6=B5=8B=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=BB=B7=E6=A0=BC=E5=8C=BA=E9=97=B4=E8=BF=87?= =?UTF-8?q?=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端: - BacktestTask 实体类添加 minPrice 和 maxPrice 字段 - BacktestDto 添加价格区间参数和配置字段 - BacktestService 支持创建和查询价格区间配置 - BacktestExecutionService 在 taskToCopyTrading 中设置价格区间 - 添加数据库迁移脚本 V32__add_backtest_price_range_filter.sql - 前端: - BacktestList 添加价格区间输入表单(最低价、最高价) - 详情页面显示价格区间配置 - 一键创建跟单配置时包含价格区间 - 添加中英繁三语言翻译 - 修复价格输入框精度显示问题(0.4 不再显示为 0.40000000) 功能说明: - 价格区间过滤与跟单配置保持一致,确保回测结果能准确反映实际跟单效果 - 支持只设置最低价、只设置最高价、或同时设置两者 - 留空表示不限制价格 Co-authored-by: Cursor --- .../wrbug/polymarketbot/dto/BacktestDto.kt | 6 +- .../polymarketbot/entity/BacktestTask.kt | 6 ++ .../backtest/BacktestExecutionService.kt | 2 + .../service/backtest/BacktestService.kt | 12 +++- .../V32__add_backtest_price_range_filter.sql | 9 +++ frontend/src/locales/en/common.json | 4 ++ frontend/src/locales/zh-CN/common.json | 4 ++ frontend/src/locales/zh-TW/common.json | 4 ++ frontend/src/pages/BacktestList.tsx | 66 ++++++++++++++++++- 9 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V32__add_backtest_price_range_filter.sql diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/BacktestDto.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/BacktestDto.kt index 8876e85..9e81b85 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/BacktestDto.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/BacktestDto.kt @@ -22,6 +22,8 @@ data class BacktestCreateRequest( val keywordFilterMode: String? = null, // 关键字过滤模式:DISABLED(不启用)、WHITELIST(白名单)、BLACKLIST(黑名单) val keywords: List? = null, // 关键字列表 val maxPositionValue: String? = null, // 最大仓位金额(USDC),NULL表示不启用 + val minPrice: String? = null, // 最低价格(可选),NULL表示不限制最低价 + val maxPrice: String? = null, // 最高价格(可选),NULL表示不限制最高价 val pageForResume: Int? = null // 用于恢复中断任务,从指定页码开始获取历史数据(从1开始) ) @@ -166,7 +168,9 @@ data class BacktestConfigDto( val supportSell: Boolean, val keywordFilterMode: String?, val keywords: List?, - val maxPositionValue: String? + val maxPositionValue: String?, + val minPrice: String?, // 最低价格(可选),NULL表示不限制最低价 + val maxPrice: String? // 最高价格(可选),NULL表示不限制最高价 ) /** diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/BacktestTask.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/BacktestTask.kt index b54bf2a..0471dbe 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/BacktestTask.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/BacktestTask.kt @@ -76,6 +76,12 @@ data class BacktestTask( @Column(name = "max_position_value", precision = 20, scale = 8) 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") var avgHoldingTime: Long? = null, // 平均持仓时间(毫秒) diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/backtest/BacktestExecutionService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/backtest/BacktestExecutionService.kt index 41c0ad6..193eac0 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/backtest/BacktestExecutionService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/backtest/BacktestExecutionService.kt @@ -73,6 +73,8 @@ class BacktestExecutionService( minOrderDepth = null, // 回测无实时订单簿数据 maxSpread = null, // 回测无实时价差数据 maxPositionValue = task.maxPositionValue, + minPrice = task.minPrice, // 最低价格 + maxPrice = task.maxPrice, // 最高价格 keywordFilterMode = task.keywordFilterMode, keywords = task.keywords, configName = null, diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/backtest/BacktestService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/backtest/BacktestService.kt index 04cb43d..b0dcc02 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/backtest/BacktestService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/backtest/BacktestService.kt @@ -82,7 +82,9 @@ class BacktestService( } else { null }, - maxPositionValue = request.maxPositionValue?.toSafeBigDecimal() + maxPositionValue = request.maxPositionValue?.toSafeBigDecimal(), + minPrice = request.minPrice?.toSafeBigDecimal(), + maxPrice = request.maxPrice?.toSafeBigDecimal() ) backtestTaskRepository.save(task) @@ -192,7 +194,9 @@ class BacktestService( } else { emptyList() }, - maxPositionValue = task.maxPositionValue?.toPlainString() + maxPositionValue = task.maxPositionValue?.toPlainString(), + minPrice = task.minPrice?.toPlainString(), + maxPrice = task.maxPrice?.toPlainString() ) val statistics = BacktestStatisticsDto( @@ -379,7 +383,9 @@ class BacktestService( supportSell = source.supportSell, keywordFilterMode = source.keywordFilterMode, keywords = source.keywords, - maxPositionValue = source.maxPositionValue + maxPositionValue = source.maxPositionValue, + minPrice = source.minPrice, + maxPrice = source.maxPrice ) backtestTaskRepository.save(newTask) diff --git a/backend/src/main/resources/db/migration/V32__add_backtest_price_range_filter.sql b/backend/src/main/resources/db/migration/V32__add_backtest_price_range_filter.sql new file mode 100644 index 0000000..4c38139 --- /dev/null +++ b/backend/src/main/resources/db/migration/V32__add_backtest_price_range_filter.sql @@ -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表示不限制最高价'; diff --git a/frontend/src/locales/en/common.json b/frontend/src/locales/en/common.json index 5d01b72..b968220 100644 --- a/frontend/src/locales/en/common.json +++ b/frontend/src/locales/en/common.json @@ -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", diff --git a/frontend/src/locales/zh-CN/common.json b/frontend/src/locales/zh-CN/common.json index 4fb7ce9..88276ab 100644 --- a/frontend/src/locales/zh-CN/common.json +++ b/frontend/src/locales/zh-CN/common.json @@ -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": "延迟执行模拟真实跟单延迟", diff --git a/frontend/src/locales/zh-TW/common.json b/frontend/src/locales/zh-TW/common.json index 7768195..e4747ab 100644 --- a/frontend/src/locales/zh-TW/common.json +++ b/frontend/src/locales/zh-TW/common.json @@ -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": "延遲執行模擬真實跟單延遲", diff --git a/frontend/src/pages/BacktestList.tsx b/frontend/src/pages/BacktestList.tsx index 1d772e7..db7fc17 100644 --- a/frontend/src/pages/BacktestList.tsx +++ b/frontend/src/pages/BacktestList.tsx @@ -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 = () => { /> + + + + + { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} + /> + + + + + { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} + /> + + + + + { 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 )} + {(detailConfig.minPrice || detailConfig.maxPrice) && ( + + {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)}` + : ''} + + )} )}