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:
@@ -21,6 +21,7 @@ data class BacktestCreateRequest(
|
|||||||
val supportSell: Boolean? = null,
|
val supportSell: Boolean? = null,
|
||||||
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 pageForResume: Int? = null // 用于恢复中断任务,从指定页码开始获取历史数据(从1开始)
|
val pageForResume: Int? = null // 用于恢复中断任务,从指定页码开始获取历史数据(从1开始)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -164,7 +165,8 @@ data class BacktestConfigDto(
|
|||||||
val maxDailyOrders: Int,
|
val maxDailyOrders: Int,
|
||||||
val supportSell: Boolean,
|
val supportSell: Boolean,
|
||||||
val keywordFilterMode: String?,
|
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")
|
@Column(name = "keywords", columnDefinition = "JSON")
|
||||||
val keywords: String? = null,
|
val keywords: String? = null,
|
||||||
|
|
||||||
|
@Column(name = "max_position_value", precision = 20, scale = 8)
|
||||||
|
val maxPositionValue: BigDecimal? = null, // 最大仓位金额(USDC),NULL表示不启用
|
||||||
|
|
||||||
// 统计字段
|
// 统计字段
|
||||||
@Column(name = "avg_holding_time")
|
@Column(name = "avg_holding_time")
|
||||||
var avgHoldingTime: Long? = null, // 平均持仓时间(毫秒)
|
var avgHoldingTime: Long? = null, // 平均持仓时间(毫秒)
|
||||||
|
|||||||
+22
@@ -10,6 +10,7 @@ import com.wrbug.polymarketbot.repository.BacktestTaskRepository
|
|||||||
import com.wrbug.polymarketbot.service.common.MarketPriceService
|
import com.wrbug.polymarketbot.service.common.MarketPriceService
|
||||||
import com.wrbug.polymarketbot.service.common.MarketService
|
import com.wrbug.polymarketbot.service.common.MarketService
|
||||||
import com.wrbug.polymarketbot.service.copytrading.configs.CopyTradingFilterService
|
import com.wrbug.polymarketbot.service.copytrading.configs.CopyTradingFilterService
|
||||||
|
import com.wrbug.polymarketbot.util.gt
|
||||||
import com.wrbug.polymarketbot.util.toSafeBigDecimal
|
import com.wrbug.polymarketbot.util.toSafeBigDecimal
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
@@ -71,6 +72,7 @@ class BacktestExecutionService(
|
|||||||
supportSell = task.supportSell,
|
supportSell = task.supportSell,
|
||||||
minOrderDepth = null, // 回测无实时订单簿数据
|
minOrderDepth = null, // 回测无实时订单簿数据
|
||||||
maxSpread = null, // 回测无实时价差数据
|
maxSpread = null, // 回测无实时价差数据
|
||||||
|
maxPositionValue = task.maxPositionValue,
|
||||||
keywordFilterMode = task.keywordFilterMode,
|
keywordFilterMode = task.keywordFilterMode,
|
||||||
keywords = task.keywords,
|
keywords = task.keywords,
|
||||||
configName = null,
|
configName = null,
|
||||||
@@ -278,6 +280,26 @@ class BacktestExecutionService(
|
|||||||
}
|
}
|
||||||
val totalCost = actualBuyAmount
|
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 相加)
|
// 更新余额和持仓(同市场同 outcome 多次买入合并:数量相加、加权均价、leaderBuyQuantity 相加)
|
||||||
currentBalance -= totalCost
|
currentBalance -= totalCost
|
||||||
val positionKey = "${leaderTrade.marketId}:${leaderTrade.outcomeIndex ?: 0}"
|
val positionKey = "${leaderTrade.marketId}:${leaderTrade.outcomeIndex ?: 0}"
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ class BacktestService(
|
|||||||
request.keywords.toJson()
|
request.keywords.toJson()
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
},
|
||||||
|
maxPositionValue = request.maxPositionValue?.toSafeBigDecimal()
|
||||||
)
|
)
|
||||||
|
|
||||||
backtestTaskRepository.save(task)
|
backtestTaskRepository.save(task)
|
||||||
@@ -190,7 +191,8 @@ class BacktestService(
|
|||||||
task.keywords.fromJson<List<String>>()
|
task.keywords.fromJson<List<String>>()
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
},
|
||||||
|
maxPositionValue = task.maxPositionValue?.toPlainString()
|
||||||
)
|
)
|
||||||
|
|
||||||
val statistics = BacktestStatisticsDto(
|
val statistics = BacktestStatisticsDto(
|
||||||
@@ -376,7 +378,8 @@ class BacktestService(
|
|||||||
maxDailyOrders = source.maxDailyOrders,
|
maxDailyOrders = source.maxDailyOrders,
|
||||||
supportSell = source.supportSell,
|
supportSell = source.supportSell,
|
||||||
keywordFilterMode = source.keywordFilterMode,
|
keywordFilterMode = source.keywordFilterMode,
|
||||||
keywords = source.keywords
|
keywords = source.keywords,
|
||||||
|
maxPositionValue = source.maxPositionValue
|
||||||
)
|
)
|
||||||
|
|
||||||
backtestTaskRepository.save(newTask)
|
backtestTaskRepository.save(newTask)
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- 添加最大仓位金额配置到回测任务表
|
||||||
|
ALTER TABLE backtest_task
|
||||||
|
ADD COLUMN max_position_value DECIMAL(20, 8) COMMENT '最大仓位金额(USDC),NULL表示不启用';
|
||||||
@@ -1364,6 +1364,7 @@
|
|||||||
"fixedAmountInvalid": "Fixed amount must be greater than 0",
|
"fixedAmountInvalid": "Fixed amount must be greater than 0",
|
||||||
"priceFilters": "Price Filters",
|
"priceFilters": "Price Filters",
|
||||||
"keywordsPlaceholder": "Please enter keywords, press Enter to add",
|
"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",
|
"delaySecondsHint": "Delay execution to simulate real copy trading delay",
|
||||||
"supportSellHint": "Whether to follow Leader sell orders",
|
"supportSellHint": "Whether to follow Leader sell orders",
|
||||||
"sortBy": "Sort By",
|
"sortBy": "Sort By",
|
||||||
|
|||||||
@@ -1364,6 +1364,7 @@
|
|||||||
"fixedAmountInvalid": "固定金额必须大于 0",
|
"fixedAmountInvalid": "固定金额必须大于 0",
|
||||||
"priceFilters": "价格过滤",
|
"priceFilters": "价格过滤",
|
||||||
"keywordsPlaceholder": "请输入关键字,按回车添加",
|
"keywordsPlaceholder": "请输入关键字,按回车添加",
|
||||||
|
"maxPositionValuePlaceholder": "留空表示不启用最大仓位限制",
|
||||||
"delaySecondsHint": "延迟执行模拟真实跟单延迟",
|
"delaySecondsHint": "延迟执行模拟真实跟单延迟",
|
||||||
"supportSellHint": "是否跟随 Leader 卖出",
|
"supportSellHint": "是否跟随 Leader 卖出",
|
||||||
"sortBy": "排序字段",
|
"sortBy": "排序字段",
|
||||||
|
|||||||
@@ -1364,6 +1364,7 @@
|
|||||||
"fixedAmountInvalid": "固定金額必須大於 0",
|
"fixedAmountInvalid": "固定金額必須大於 0",
|
||||||
"priceFilters": "價格過濾",
|
"priceFilters": "價格過濾",
|
||||||
"keywordsPlaceholder": "請輸入關鍵字,按回車添加",
|
"keywordsPlaceholder": "請輸入關鍵字,按回車添加",
|
||||||
|
"maxPositionValuePlaceholder": "留空表示不啟用最大倉位限制",
|
||||||
"delaySecondsHint": "延遲執行模擬真實跟單延遲",
|
"delaySecondsHint": "延遲執行模擬真實跟單延遲",
|
||||||
"supportSellHint": "是否跟隨 Leader 賣出",
|
"supportSellHint": "是否跟隨 Leader 賣出",
|
||||||
"sortBy": "排序欄位",
|
"sortBy": "排序欄位",
|
||||||
|
|||||||
@@ -271,7 +271,8 @@ const BacktestList: React.FC = () => {
|
|||||||
maxDailyOrders: values.maxDailyOrders,
|
maxDailyOrders: values.maxDailyOrders,
|
||||||
supportSell: values.supportSell,
|
supportSell: values.supportSell,
|
||||||
keywordFilterMode: values.keywordFilterMode,
|
keywordFilterMode: values.keywordFilterMode,
|
||||||
keywords: values.keywords
|
keywords: values.keywords,
|
||||||
|
maxPositionValue: values.maxPositionValue
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await backtestService.create(request)
|
const response = await backtestService.create(request)
|
||||||
@@ -802,7 +803,7 @@ const BacktestList: React.FC = () => {
|
|||||||
layout="vertical"
|
layout="vertical"
|
||||||
initialValues={{
|
initialValues={{
|
||||||
maxDailyLoss: 500,
|
maxDailyLoss: 500,
|
||||||
maxDailyOrders: 50,
|
maxDailyOrders: 100,
|
||||||
supportSell: true,
|
supportSell: true,
|
||||||
keywordFilterMode: 'DISABLED',
|
keywordFilterMode: 'DISABLED',
|
||||||
backtestDays: 7
|
backtestDays: 7
|
||||||
@@ -978,6 +979,18 @@ const BacktestList: React.FC = () => {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t('backtest.maxPositionValue') + ' (USDC)'}
|
||||||
|
name="maxPositionValue"
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder={t('backtest.maxPositionValuePlaceholder') || '留空表示不启用最大仓位限制'}
|
||||||
|
precision={2}
|
||||||
|
min={0}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('backtest.supportSell')}
|
label={t('backtest.supportSell')}
|
||||||
name="supportSell"
|
name="supportSell"
|
||||||
@@ -1248,6 +1261,11 @@ const BacktestList: React.FC = () => {
|
|||||||
{detailConfig.keywords.join(', ')}
|
{detailConfig.keywords.join(', ')}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
)}
|
)}
|
||||||
|
{detailConfig.maxPositionValue && (
|
||||||
|
<Descriptions.Item label={t('backtest.maxPositionValue')}>
|
||||||
|
{formatUSDC(detailConfig.maxPositionValue)} USDC
|
||||||
|
</Descriptions.Item>
|
||||||
|
)}
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user