From 20df47168387d8ac4a96decd4c37e2f1437c0ec8 Mon Sep 17 00:00:00 2001 From: WrBug Date: Mon, 9 Feb 2026 02:25:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9B=9E=E6=B5=8B=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=9C=80=E5=A4=A7=E4=BB=93=E4=BD=8D=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 参考跟单配置的实现,为回测任务添加最大仓位金额限制功能。 后端改动: - 添加数据库迁移文件 V31__add_backtest_max_position_value.sql - BacktestTask 实体添加 maxPositionValue 字段(BigDecimal?,NULL表示不启用) - BacktestCreateRequest 添加 maxPositionValue 字段 - BacktestConfigDto 添加 maxPositionValue 字段 - BacktestService 处理 maxPositionValue 的创建、详情和复制 - BacktestExecutionService.taskToCopyTrading 映射 maxPositionValue 参数 - BacktestExecutionService.executeBacktest 在买入逻辑中添加仓位检查 * 检查条件:当前仓位 + 买入金额 <= 最大仓位 * 仓位价值计算:quantity * avgPrice * 超过限制时跳过该笔买入并记录详细日志 * 按市场+方向(marketId + outcomeIndex)分别检查 前端改动: - BacktestList 表单添加最大仓位金额输入框(可选字段) - 创建回测任务时包含 maxPositionValue - 回测任务详情中显示 maxPositionValue(仅在配置了时显示) - 默认最大每日订单数从 50 改为 100 - 多语言翻译新增: * backtest.maxPositionValue: 最大仓位金额 / 最大倉位金額 / Max Position Value * maxPositionValuePlaceholder: 留空表示不启用最大仓位限制 功能特点: - 可选参数:留空或为 null 时表示不启用该限制,保持向后兼容 - 单市场单方向限制:按 marketId 和 outcomeIndex 分别计算和限制 - 精确计算:使用 BigDecimal 进行数值比较 - 详细日志:超过限制时记录当前仓位、买入金额、总计等详细信息 Co-authored-by: Cursor --- .../wrbug/polymarketbot/dto/BacktestDto.kt | 4 +++- .../polymarketbot/entity/BacktestTask.kt | 3 +++ .../backtest/BacktestExecutionService.kt | 22 +++++++++++++++++++ .../service/backtest/BacktestService.kt | 9 +++++--- .../V31__add_backtest_max_position_value.sql | 3 +++ frontend/src/locales/en/common.json | 1 + frontend/src/locales/zh-CN/common.json | 1 + frontend/src/locales/zh-TW/common.json | 1 + frontend/src/pages/BacktestList.tsx | 22 +++++++++++++++++-- 9 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V31__add_backtest_max_position_value.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 539c5ad..8876e85 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/BacktestDto.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/BacktestDto.kt @@ -21,6 +21,7 @@ data class BacktestCreateRequest( val supportSell: Boolean? = null, val keywordFilterMode: String? = null, // 关键字过滤模式:DISABLED(不启用)、WHITELIST(白名单)、BLACKLIST(黑名单) val keywords: List? = null, // 关键字列表 + val maxPositionValue: String? = null, // 最大仓位金额(USDC),NULL表示不启用 val pageForResume: Int? = null // 用于恢复中断任务,从指定页码开始获取历史数据(从1开始) ) @@ -164,7 +165,8 @@ data class BacktestConfigDto( val maxDailyOrders: Int, val supportSell: Boolean, val keywordFilterMode: String?, - val keywords: List? + val keywords: List?, + val maxPositionValue: String? ) /** 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 da2dfe2..b54bf2a 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/BacktestTask.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/BacktestTask.kt @@ -73,6 +73,9 @@ data class BacktestTask( @Column(name = "keywords", columnDefinition = "JSON") val keywords: String? = null, + @Column(name = "max_position_value", precision = 20, scale = 8) + val maxPositionValue: BigDecimal? = null, // 最大仓位金额(USDC),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 81db5b9..41c0ad6 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 @@ -10,6 +10,7 @@ import com.wrbug.polymarketbot.repository.BacktestTaskRepository import com.wrbug.polymarketbot.service.common.MarketPriceService import com.wrbug.polymarketbot.service.common.MarketService import com.wrbug.polymarketbot.service.copytrading.configs.CopyTradingFilterService +import com.wrbug.polymarketbot.util.gt import com.wrbug.polymarketbot.util.toSafeBigDecimal import org.slf4j.LoggerFactory import org.springframework.stereotype.Service @@ -71,6 +72,7 @@ class BacktestExecutionService( supportSell = task.supportSell, minOrderDepth = null, // 回测无实时订单簿数据 maxSpread = null, // 回测无实时价差数据 + maxPositionValue = task.maxPositionValue, keywordFilterMode = task.keywordFilterMode, keywords = task.keywords, configName = null, @@ -278,6 +280,26 @@ class BacktestExecutionService( } val totalCost = actualBuyAmount + // 5.6.3 检查最大仓位限制(如果配置了) + if (task.maxPositionValue != null) { + val positionKey = "${leaderTrade.marketId}:${leaderTrade.outcomeIndex ?: 0}" + val currentPosition = positions[positionKey] + val currentPositionValue = if (currentPosition != null) { + currentPosition.quantity.multiply(currentPosition.avgPrice) + } else { + BigDecimal.ZERO + } + val totalValueAfterOrder = currentPositionValue.add(actualBuyAmount) + + if (totalValueAfterOrder.gt(task.maxPositionValue)) { + val currentPositionValueStr = currentPositionValue.stripTrailingZeros().toPlainString() + val totalValueStr = totalValueAfterOrder.stripTrailingZeros().toPlainString() + val maxValueStr = task.maxPositionValue.stripTrailingZeros().toPlainString() + logger.info("超过最大仓位金额限制: 市场=${leaderTrade.marketId}, 方向=${leaderTrade.outcomeIndex}, 当前仓位=${currentPositionValueStr} USDC, 买入金额=${actualBuyAmount} USDC, 总计=${totalValueStr} USDC > 最大限制=${maxValueStr} USDC") + continue + } + } + // 更新余额和持仓(同市场同 outcome 多次买入合并:数量相加、加权均价、leaderBuyQuantity 相加) currentBalance -= totalCost val positionKey = "${leaderTrade.marketId}:${leaderTrade.outcomeIndex ?: 0}" 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 4ebc039..04cb43d 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 @@ -81,7 +81,8 @@ class BacktestService( request.keywords.toJson() } else { null - } + }, + maxPositionValue = request.maxPositionValue?.toSafeBigDecimal() ) backtestTaskRepository.save(task) @@ -190,7 +191,8 @@ class BacktestService( task.keywords.fromJson>() } else { emptyList() - } + }, + maxPositionValue = task.maxPositionValue?.toPlainString() ) val statistics = BacktestStatisticsDto( @@ -376,7 +378,8 @@ class BacktestService( maxDailyOrders = source.maxDailyOrders, supportSell = source.supportSell, keywordFilterMode = source.keywordFilterMode, - keywords = source.keywords + keywords = source.keywords, + maxPositionValue = source.maxPositionValue ) backtestTaskRepository.save(newTask) diff --git a/backend/src/main/resources/db/migration/V31__add_backtest_max_position_value.sql b/backend/src/main/resources/db/migration/V31__add_backtest_max_position_value.sql new file mode 100644 index 0000000..b2e3cae --- /dev/null +++ b/backend/src/main/resources/db/migration/V31__add_backtest_max_position_value.sql @@ -0,0 +1,3 @@ +-- 添加最大仓位金额配置到回测任务表 +ALTER TABLE backtest_task +ADD COLUMN max_position_value DECIMAL(20, 8) COMMENT '最大仓位金额(USDC),NULL表示不启用'; diff --git a/frontend/src/locales/en/common.json b/frontend/src/locales/en/common.json index 6ac9d05..5d01b72 100644 --- a/frontend/src/locales/en/common.json +++ b/frontend/src/locales/en/common.json @@ -1364,6 +1364,7 @@ "fixedAmountInvalid": "Fixed amount must be greater than 0", "priceFilters": "Price Filters", "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", "supportSellHint": "Whether to follow Leader sell orders", "sortBy": "Sort By", diff --git a/frontend/src/locales/zh-CN/common.json b/frontend/src/locales/zh-CN/common.json index 52d8932..4fb7ce9 100644 --- a/frontend/src/locales/zh-CN/common.json +++ b/frontend/src/locales/zh-CN/common.json @@ -1364,6 +1364,7 @@ "fixedAmountInvalid": "固定金额必须大于 0", "priceFilters": "价格过滤", "keywordsPlaceholder": "请输入关键字,按回车添加", + "maxPositionValuePlaceholder": "留空表示不启用最大仓位限制", "delaySecondsHint": "延迟执行模拟真实跟单延迟", "supportSellHint": "是否跟随 Leader 卖出", "sortBy": "排序字段", diff --git a/frontend/src/locales/zh-TW/common.json b/frontend/src/locales/zh-TW/common.json index d72ac67..7768195 100644 --- a/frontend/src/locales/zh-TW/common.json +++ b/frontend/src/locales/zh-TW/common.json @@ -1364,6 +1364,7 @@ "fixedAmountInvalid": "固定金額必須大於 0", "priceFilters": "價格過濾", "keywordsPlaceholder": "請輸入關鍵字,按回車添加", + "maxPositionValuePlaceholder": "留空表示不啟用最大倉位限制", "delaySecondsHint": "延遲執行模擬真實跟單延遲", "supportSellHint": "是否跟隨 Leader 賣出", "sortBy": "排序欄位", diff --git a/frontend/src/pages/BacktestList.tsx b/frontend/src/pages/BacktestList.tsx index 01cf516..da0ee2a 100644 --- a/frontend/src/pages/BacktestList.tsx +++ b/frontend/src/pages/BacktestList.tsx @@ -271,7 +271,8 @@ const BacktestList: React.FC = () => { maxDailyOrders: values.maxDailyOrders, supportSell: values.supportSell, keywordFilterMode: values.keywordFilterMode, - keywords: values.keywords + keywords: values.keywords, + maxPositionValue: values.maxPositionValue } const response = await backtestService.create(request) @@ -802,7 +803,7 @@ const BacktestList: React.FC = () => { layout="vertical" initialValues={{ maxDailyLoss: 500, - maxDailyOrders: 50, + maxDailyOrders: 100, supportSell: true, keywordFilterMode: 'DISABLED', backtestDays: 7 @@ -978,6 +979,18 @@ const BacktestList: React.FC = () => { + + + + { {detailConfig.keywords.join(', ')} )} + {detailConfig.maxPositionValue && ( + + {formatUSDC(detailConfig.maxPositionValue)} USDC + + )} )}