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)}` + : ''} + + )} )}