feat: 回测任务添加最大仓位参数支持

参考跟单配置的实现,为回测任务添加最大仓位金额限制功能。

后端改动:
- 添加数据库迁移文件 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 <cursoragent@cursor.com>
This commit is contained in:
WrBug
2026-02-09 02:25:24 +08:00
co-authored by Cursor
parent 5487c7c862
commit 20df471683
9 changed files with 60 additions and 6 deletions
@@ -21,6 +21,7 @@ data class BacktestCreateRequest(
val supportSell: Boolean? = null,
val keywordFilterMode: String? = null, // 关键字过滤模式:DISABLED(不启用)、WHITELIST(白名单)、BLACKLIST(黑名单)
val keywords: List<String>? = 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<String>?
val keywords: List<String>?,
val maxPositionValue: String?
)
/**
@@ -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, // 平均持仓时间(毫秒)
@@ -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}"
@@ -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<List<String>>()
} 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)
@@ -0,0 +1,3 @@
-- 添加最大仓位金额配置到回测任务表
ALTER TABLE backtest_task
ADD COLUMN max_position_value DECIMAL(20, 8) COMMENT '最大仓位金额(USDC),NULL表示不启用';