2026-01-31 00:26:00 +08:00
|
|
|
|
# 回测功能设计审查清单
|
|
|
|
|
|
|
|
|
|
|
|
## 一、设计审查要点
|
|
|
|
|
|
|
|
|
|
|
|
### 1.1 产品需求完整性 ✅
|
|
|
|
|
|
|
|
|
|
|
|
**已覆盖的核心功能**:
|
|
|
|
|
|
- ✅ 回测任务的创建、查询、删除
|
|
|
|
|
|
- ✅ 按Leader筛选和排序功能
|
|
|
|
|
|
- ✅ 回测配置参数复用现有跟单配置
|
|
|
|
|
|
- ✅ 回测详情展示 (交易记录、资金曲线图、统计数据)
|
|
|
|
|
|
- ✅ 资金不足时自动停止机制
|
|
|
|
|
|
- ✅ 回测天数限制 (1-30天)
|
|
|
|
|
|
|
|
|
|
|
|
**潜在遗漏点**:
|
|
|
|
|
|
> [!WARNING]
|
|
|
|
|
|
> **需要确认的问题**:
|
|
|
|
|
|
> 1. **回测结果的可见性**: 是否需要支持多用户? 当前设计未涉及权限控制
|
|
|
|
|
|
> 2. **回测任务的生命周期管理**: 是否需要自动清理过期的回测记录?
|
|
|
|
|
|
> 3. **回测进度的实时展示**: 前端如何获取运行中任务的进度? (考虑WebSocket或轮询)
|
|
|
|
|
|
|
|
|
|
|
|
### 1.2 技术设计合理性 ✅
|
|
|
|
|
|
|
|
|
|
|
|
**优点**:
|
|
|
|
|
|
- ✅ 数据库设计规范,索引合理
|
|
|
|
|
|
- ✅ API设计符合RESTful规范
|
|
|
|
|
|
- ✅ 复用现有的 `CopyTradingFilterService`,减少代码冗余
|
|
|
|
|
|
- ✅ 使用 BigDecimal 保证计算精度
|
|
|
|
|
|
- ✅ 异步执行回测任务,不阻塞主线程
|
|
|
|
|
|
|
|
|
|
|
|
**可能的改进点**:
|
|
|
|
|
|
> [!NOTE]
|
|
|
|
|
|
> **建议优化的地方**:
|
|
|
|
|
|
> 1. **历史数据获取**: 当前设计依赖Polymarket API,需要考虑API限流和数据缺失的情况
|
|
|
|
|
|
> 2. **缓存策略**: 建议对Leader历史交易数据使用分层缓存 (内存 + Redis)
|
|
|
|
|
|
> 3. **回测结果的序列化**: 考虑将详细交易记录存储为JSON,减少表的大小
|
|
|
|
|
|
|
2026-01-31 07:27:36 +08:00
|
|
|
|
### 1.3 业务逻辑准确性 ✅
|
2026-01-31 00:26:00 +08:00
|
|
|
|
|
2026-01-31 07:27:36 +08:00
|
|
|
|
**已完善的关键逻辑**:
|
2026-01-31 00:26:00 +08:00
|
|
|
|
|
2026-01-31 07:27:36 +08:00
|
|
|
|
#### 1.3.1 历史数据获取 ⭐ (已修正)
|
|
|
|
|
|
> [!NOTE]
|
|
|
|
|
|
> **问题**: 现有 `ProcessedTrade` 表字段有限,无法满足回测需求。
|
|
|
|
|
|
>
|
|
|
|
|
|
> **解决方案**: 创建独立的 `backtest_historical_trades` 表
|
|
|
|
|
|
> - ✅ 存储完整的交易信息(marketId, price, quantity, outcomeIndex 等)
|
|
|
|
|
|
> - ✅ 支持实时数据同步(跟单时同时写入)
|
|
|
|
|
|
> - ✅ 支持通过 API 补充历史数据
|
|
|
|
|
|
> - ✅ 不影响现有跟单功能
|
|
|
|
|
|
|
|
|
|
|
|
#### 1.3.2 卖出匹配逻辑 ⭐ (已修正)
|
|
|
|
|
|
> [!NOTE]
|
|
|
|
|
|
> **改进**: 使用 `outcomeIndex` 支持多元市场
|
|
|
|
|
|
>
|
|
|
|
|
|
> **实现方案**:
|
|
|
|
|
|
> - 持仓键: `marketId + outcomeIndex`(支持多元市场)
|
|
|
|
|
|
> - 比例模式: 按 Leader 卖出比例计算
|
|
|
|
|
|
> - 固定金额模式: 全部卖出
|
|
|
|
|
|
> - 参考 `CopyOrderTracking` 的逻辑
|
|
|
|
|
|
|
|
|
|
|
|
**伪代码**:
|
|
|
|
|
|
```kotlin
|
|
|
|
|
|
val positionKey = "${leaderTrade.marketId}:${leaderTrade.outcomeIndex ?: 0}"
|
|
|
|
|
|
val position = positions[positionKey] ?: continue
|
|
|
|
|
|
|
|
|
|
|
|
val sellQuantity = if (task.copyMode == "RATIO") {
|
|
|
|
|
|
if (position.leaderBuyQuantity != null && position.leaderBuyQuantity > BigDecimal.ZERO) {
|
|
|
|
|
|
position.quantity * (leaderTrade.quantity / position.leaderBuyQuantity)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
position.quantity // 全部卖出
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
position.quantity // 固定金额模式全部卖出
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 1.3.3 价格滑点模拟 ✅ (已决策)
|
|
|
|
|
|
> [!NOTE]
|
|
|
|
|
|
> **用户决策**: 暂不模拟价格滑点
|
|
|
|
|
|
>
|
|
|
|
|
|
> **理由**:
|
|
|
|
|
|
> - 简化回测逻辑
|
|
|
|
|
|
> - 减少复杂度
|
|
|
|
|
|
> - 后续可以作为可选项添加
|
|
|
|
|
|
>
|
|
|
|
|
|
> **实现**: 使用 Leader 的成交价,不进行滑点调整
|
2026-01-31 00:26:00 +08:00
|
|
|
|
|
|
|
|
|
|
#### 1.3.2 价格滑点模拟
|
|
|
|
|
|
> [!NOTE]
|
|
|
|
|
|
> **关键问题**: 是否需要模拟价格滑点?
|
|
|
|
|
|
>
|
|
|
|
|
|
> **当前设计**: 不模拟价格滑点,直接使用Leader的成交价
|
|
|
|
|
|
> - 优点: 简化逻辑,回测速度快
|
|
|
|
|
|
> - 缺点: 可能高估收益(实际跟单可能有滑点)
|
|
|
|
|
|
>
|
|
|
|
|
|
> **可选方案**: 增加可配置的滑点参数
|
|
|
|
|
|
> - 买入时: 价格 × (1 + 滑点%)
|
|
|
|
|
|
> - 卖出时: 价格 × (1 - 滑点%)
|
|
|
|
|
|
> - 增加可选的滑点模拟参数 (例如: ±0.5%)
|
|
|
|
|
|
> - 在PRD中补充此配置项
|
|
|
|
|
|
|
|
|
|
|
|
#### 1.3.3 手续费计算 ✅ (已移除)
|
|
|
|
|
|
> [!NOTE]
|
|
|
|
|
|
> **用户决策**: 回测不计算手续费
|
|
|
|
|
|
>
|
|
|
|
|
|
> **理由**:
|
|
|
|
|
|
> - 简化计算逻辑
|
|
|
|
|
|
> - 避免精度问题
|
|
|
|
|
|
> - 降低复杂度
|
|
|
|
|
|
>
|
|
|
|
|
|
> **实现**:
|
|
|
|
|
|
> - 所有交易的 `fee` 字段均为 `0`
|
|
|
|
|
|
> - 买入成本 = 数量 × 价格
|
|
|
|
|
|
> - 卖出收入 = 数量 × 价格
|
|
|
|
|
|
> - 结算收入 = 数量 × 结算价
|
|
|
|
|
|
|
|
|
|
|
|
#### 1.3.4 市场结算处理 ⭐ (已优化)
|
|
|
|
|
|
> [!NOTE]
|
2026-01-31 07:27:36 +08:00
|
|
|
|
> **问题**: 市场结束时,未平仓位如何自动结算?
|
|
|
|
|
|
>
|
2026-01-31 00:26:00 +08:00
|
|
|
|
> **优化方案** (采纳用户建议):
|
|
|
|
|
|
> - ✅ **实时检查**: 在回测循环中,每处理一笔Leader交易前,检查所有持仓的市场`endDate`
|
|
|
|
|
|
> - ✅ **到期即结算**: 如果 `marketEndDate <= currentTradeTime`,立即结算该持仓
|
|
|
|
|
|
> - ✅ **资金可用**: 结算后的资金立即计入余额,可以用于后续交易
|
|
|
|
|
|
> - ✅ **兜底处理**: 回测结束时,结算所有剩余未到期持仓
|
2026-01-31 07:27:36 +08:00
|
|
|
|
>
|
|
|
|
|
|
> **结算价格判断** (通过市场价格):
|
|
|
|
|
|
> - 价格 >= 0.95: 胜出 (按 1.0 结算)
|
|
|
|
|
|
> - 价格 <= 0.05: 失败 (按 0.0 结算)
|
|
|
|
|
|
> - 其他情况: 按成本价保守估计
|
|
|
|
|
|
>
|
2026-01-31 00:26:00 +08:00
|
|
|
|
> **实现要点**:
|
|
|
|
|
|
> ```kotlin
|
|
|
|
|
|
> // 在交易循环中实时检查市场到期
|
|
|
|
|
|
> for (leaderTrade in leaderTrades.sortedBy { it.timestamp }) {
|
2026-01-31 07:27:36 +08:00
|
|
|
|
>
|
2026-01-31 00:26:00 +08:00
|
|
|
|
> // 1. 检查并结算已到期的市场
|
|
|
|
|
|
> val expiredPositions = positions.filter { (_, position) ->
|
|
|
|
|
|
> val marketInfo = getMarketInfo(position.marketId)
|
|
|
|
|
|
> marketInfo.endDate <= leaderTrade.timestamp
|
|
|
|
|
|
> }
|
2026-01-31 07:27:36 +08:00
|
|
|
|
>
|
2026-01-31 00:26:00 +08:00
|
|
|
|
> for ((positionKey, position) in expiredPositions) {
|
2026-01-31 07:27:36 +08:00
|
|
|
|
> val marketPrice = marketPriceService.getCurrentMarketPrice(
|
|
|
|
|
|
> marketId = position.marketId,
|
|
|
|
|
|
> outcomeIndex = position.outcomeIndex ?: 0
|
|
|
|
|
|
> )
|
|
|
|
|
|
>
|
|
|
|
|
|
> val settlementPrice = when {
|
|
|
|
|
|
> marketPrice >= BigDecimal("0.95") -> BigDecimal.ONE // 胜出
|
|
|
|
|
|
> marketPrice <= BigDecimal("0.05") -> BigDecimal.ZERO // 失败
|
|
|
|
|
|
> else -> position.avgPrice // 未结算,按成本价
|
|
|
|
|
|
> }
|
|
|
|
|
|
>
|
|
|
|
|
|
> val settlementValue = position.quantity * settlementPrice
|
2026-01-31 00:26:00 +08:00
|
|
|
|
> currentBalance += settlementValue
|
|
|
|
|
|
> positions.remove(positionKey)
|
|
|
|
|
|
> }
|
2026-01-31 07:27:36 +08:00
|
|
|
|
>
|
2026-01-31 00:26:00 +08:00
|
|
|
|
> // 2. 处理当前Leader交易
|
|
|
|
|
|
> // ...
|
|
|
|
|
|
> }
|
|
|
|
|
|
> ```
|
2026-01-31 07:27:36 +08:00
|
|
|
|
>
|
2026-01-31 00:26:00 +08:00
|
|
|
|
> **优势**:
|
|
|
|
|
|
> - 更符合真实场景 (市场结束时自动返还资金)
|
|
|
|
|
|
> - 提高资金利用率 (结算资金可参与后续交易)
|
|
|
|
|
|
> - 更准确的收益计算
|
2026-01-31 07:27:36 +08:00
|
|
|
|
> - 通过市场价格判断,无需依赖可能不存在的 `winner` 字段
|
2026-01-31 00:26:00 +08:00
|
|
|
|
|
|
|
|
|
|
#### 1.3.5 余额不足与持仓处理 ⚠️ (边缘场景) - 已修正
|
|
|
|
|
|
> [!WARNING]
|
|
|
|
|
|
> **关键问题**: 当余额不足但有未卖出持仓时,如何处理?
|
|
|
|
|
|
>
|
|
|
|
|
|
> **场景示例**:
|
|
|
|
|
|
> - 初始余额: $1000
|
|
|
|
|
|
> - 已买入持仓市值: $800(未卖出)
|
|
|
|
|
|
> - 当前余额: $200
|
|
|
|
|
|
> - 新买入订单需要: $300
|
|
|
|
|
|
> - **问题**: 是否允许买入?虽然持仓市值足够,但资金被占用
|
|
|
|
|
|
>
|
|
|
|
|
|
> **推荐方案: 严格模式**
|
|
|
|
|
|
> - ❌ 不允许买入(余额不足)
|
|
|
|
|
|
> - ✅ 仅使用 `currentBalance` 判断
|
|
|
|
|
|
> - ✅ 不计入持仓市值(因为持仓未实现)
|
|
|
|
|
|
> - ✅ 理由: 更真实,避免过于乐观的回测结果
|
|
|
|
|
|
>
|
|
|
|
|
|
> **替代方案: 宽松模式**
|
|
|
|
|
|
> - ✅ 计算"虚拟可用资金" = `currentBalance + 持仓估值`
|
|
|
|
|
|
> - ⚠️ 允许"透支"买入
|
|
|
|
|
|
> - ❌ 风险: 可能产生不切实际的收益
|
|
|
|
|
|
>
|
|
|
|
|
|
> **实际影响**:
|
|
|
|
|
|
> - 严格模式下,资金周转率是限制因素
|
|
|
|
|
|
> - 鼓励快进快出的策略
|
|
|
|
|
|
> - 长期持仓策略会因资金占用而错过后续机会
|
|
|
|
|
|
>
|
|
|
|
|
|
> **已在文档中采用**: 严格模式
|
|
|
|
|
|
|
2026-01-31 07:27:36 +08:00
|
|
|
|
#### 1.3.6 每日订单数限制 ✅ (已补充)
|
|
|
|
|
|
> [!NOTE]
|
|
|
|
|
|
> **问题**: 文档提到了 `maxDailyOrders` 参数,但未在算法中实现
|
|
|
|
|
|
>
|
|
|
|
|
|
> **解决方案**: 在回测循环中添加每日订单数统计
|
|
|
|
|
|
>
|
|
|
|
|
|
> **实现**:
|
|
|
|
|
|
> ```kotlin
|
|
|
|
|
|
> // 统计当前交易时间当天已有的订单数
|
|
|
|
|
|
> val dailyOrderCount = trades.count { isSameDay(it.tradeTime, leaderTrade.timestamp) }
|
|
|
|
|
|
>
|
|
|
|
|
|
> if (dailyOrderCount >= task.maxDailyOrders) {
|
|
|
|
|
|
> logger.info("已达到每日最大订单数限制: $dailyOrderCount / ${task.maxDailyOrders}")
|
|
|
|
|
|
> continue
|
|
|
|
|
|
> }
|
|
|
|
|
|
> ```
|
|
|
|
|
|
>
|
|
|
|
|
|
> **优势**:
|
|
|
|
|
|
> - 符合实际跟单的风险控制逻辑
|
|
|
|
|
|
> - 避免回测结果过于激进
|
|
|
|
|
|
|
|
|
|
|
|
#### 1.3.7 价格容忍度检查 ✅ (已补充)
|
|
|
|
|
|
> [!NOTE]
|
|
|
|
|
|
> **问题**: 文档提到了 `priceTolerance` 参数,但未在算法中实现
|
|
|
|
|
|
>
|
|
|
|
|
|
> **解决方案**: 在执行交易前检查当前市场价格是否在容忍范围内
|
|
|
|
|
|
>
|
|
|
|
|
|
> **实现**:
|
|
|
|
|
|
> ```kotlin
|
|
|
|
|
|
> if (task.priceTolerance > BigDecimal.ZERO) {
|
|
|
|
|
|
> val tolerance = task.priceTolerance.divide(BigDecimal("100"))
|
|
|
|
|
|
> val minPrice = leaderTrade.price.multiply(BigDecimal.ONE.subtract(tolerance))
|
|
|
|
|
|
> val maxPrice = leaderTrade.price.multiply(BigDecimal.ONE.add(tolerance))
|
|
|
|
|
|
>
|
|
|
|
|
|
> val currentPrice = marketPriceService.getCurrentMarketPrice(
|
|
|
|
|
|
> marketId = leaderTrade.marketId,
|
|
|
|
|
|
> outcomeIndex = leaderTrade.outcomeIndex ?: 0
|
|
|
|
|
|
> )
|
|
|
|
|
|
>
|
|
|
|
|
|
> if (currentPrice < minPrice || currentPrice > maxPrice) {
|
|
|
|
|
|
> logger.info("价格超出容忍度范围: 当前=$currentPrice, 可用范围=[$minPrice, $maxPrice]")
|
|
|
|
|
|
> continue
|
|
|
|
|
|
> }
|
|
|
|
|
|
> }
|
|
|
|
|
|
> ```
|
|
|
|
|
|
|
|
|
|
|
|
#### 1.3.8 回测停止条件 ✅ (已修正)
|
2026-01-31 00:26:00 +08:00
|
|
|
|
> [!NOTE]
|
|
|
|
|
|
> **修正**: 基于用户反馈,修正了停止逻辑
|
|
|
|
|
|
>
|
|
|
|
|
|
> **错误设计**:
|
|
|
|
|
|
> ```kotlin
|
|
|
|
|
|
> if (currentBalance < $1) {
|
|
|
|
|
|
> break // ❌ 直接停止,忽略持仓
|
|
|
|
|
|
> }
|
|
|
|
|
|
> ```
|
|
|
|
|
|
>
|
|
|
|
|
|
> **正确设计**:
|
|
|
|
|
|
> ```kotlin
|
|
|
|
|
|
> // 只有"余额不足 且 无持仓"时才停止
|
|
|
|
|
|
> if (currentBalance < $1 && positions.isEmpty()) {
|
|
|
|
|
|
> break // ✅ 确保无持仓时才停止
|
|
|
|
|
|
> }
|
|
|
|
|
|
>
|
|
|
|
|
|
> // 有持仓时继续处理(等待卖出或结算)
|
|
|
|
|
|
> if (currentBalance < $1 && positions.isNotEmpty()) {
|
|
|
|
|
|
> // 继续处理,跳过买入,但执行卖出和结算
|
|
|
|
|
|
> }
|
|
|
|
|
|
> ```
|
|
|
|
|
|
>
|
|
|
|
|
|
> **理由**:
|
|
|
|
|
|
> - 持仓存在意味着可能有后续卖出或市场结算
|
|
|
|
|
|
> - 这些操作会释放资金
|
|
|
|
|
|
> - 过早停止会导致资金无法回收,回测不准确
|
|
|
|
|
|
|
|
|
|
|
|
### 1.4 性能和可扩展性 ✅
|
|
|
|
|
|
|
|
|
|
|
|
**已考虑的优化**:
|
|
|
|
|
|
- ✅ 异步执行,线程池限制并发
|
|
|
|
|
|
- ✅ 分页查询
|
|
|
|
|
|
- ✅ 数据库索引优化
|
|
|
|
|
|
- ✅ 前端虚拟滚动
|
|
|
|
|
|
|
|
|
|
|
|
**需要进一步考虑**:
|
|
|
|
|
|
> [!TIP]
|
|
|
|
|
|
> **性能优化建议**:
|
|
|
|
|
|
> 1. **批量插入交易记录**: 使用 `saveAll()` 而非逐条 `save()`
|
|
|
|
|
|
> 2. **进度更新频率**: 避免每笔交易都更新数据库,改为每100笔或每10秒更新一次
|
|
|
|
|
|
> 3. **历史数据预加载**: 在任务开始前一次性加载所有历史交易,避免多次API调用
|
|
|
|
|
|
|
|
|
|
|
|
## 二、数据库设计补充
|
|
|
|
|
|
|
2026-01-31 07:27:36 +08:00
|
|
|
|
### 2.1 新增回测历史交易表
|
|
|
|
|
|
|
|
|
|
|
|
**问题**: 现有 `ProcessedTrade` 表字段有限,无法满足回测需求。
|
|
|
|
|
|
|
|
|
|
|
|
**解决方案**: 创建独立的 `backtest_historical_trades` 表,存储完整的 Leader 历史交易数据。
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
CREATE TABLE backtest_historical_trades (
|
|
|
|
|
|
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '记录ID',
|
|
|
|
|
|
leader_id BIGINT NOT NULL COMMENT 'Leader ID',
|
|
|
|
|
|
trade_id VARCHAR(100) NOT NULL COMMENT 'Leader 交易ID(唯一标识)',
|
|
|
|
|
|
market_id VARCHAR(100) NOT NULL COMMENT '市场ID',
|
|
|
|
|
|
market_title VARCHAR(500) DEFAULT NULL COMMENT '市场标题',
|
|
|
|
|
|
market_slug VARCHAR(200) DEFAULT NULL COMMENT '市场 slug(用于生成链接)',
|
|
|
|
|
|
side VARCHAR(10) NOT NULL COMMENT '交易方向: BUY/SELL',
|
|
|
|
|
|
outcome VARCHAR(50) DEFAULT NULL COMMENT '市场方向(如 YES, NO 等)',
|
|
|
|
|
|
outcome_index INT DEFAULT NULL COMMENT '结果索引(0, 1, 2, ...),支持多元市场',
|
|
|
|
|
|
price DECIMAL(20, 8) NOT NULL COMMENT '交易价格',
|
|
|
|
|
|
size DECIMAL(20, 8) NOT NULL COMMENT '交易数量',
|
|
|
|
|
|
amount DECIMAL(20, 8) NOT NULL COMMENT '交易金额(price × size)',
|
|
|
|
|
|
trade_timestamp BIGINT NOT NULL COMMENT '交易时间戳(毫秒)',
|
|
|
|
|
|
|
|
|
|
|
|
-- 元数据
|
|
|
|
|
|
source VARCHAR(20) NOT NULL DEFAULT 'POLLING' COMMENT '数据来源: WEBSOCKET/POLLING/API',
|
|
|
|
|
|
fetched_at BIGINT NOT NULL COMMENT '数据获取时间(毫秒)',
|
|
|
|
|
|
created_at BIGINT NOT NULL COMMENT '创建时间(毫秒)',
|
|
|
|
|
|
|
|
|
|
|
|
UNIQUE INDEX uk_leader_trade (leader_id, trade_id),
|
|
|
|
|
|
INDEX idx_leader_id (leader_id),
|
|
|
|
|
|
INDEX idx_trade_timestamp (trade_timestamp),
|
|
|
|
|
|
INDEX idx_market_id (market_id)
|
|
|
|
|
|
) COMMENT='回测历史交易表';
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**优势**:
|
|
|
|
|
|
- ✅ 不影响现有跟单系统的 `ProcessedTrade` 表
|
|
|
|
|
|
- ✅ 存储完整的交易信息,满足回测需求
|
|
|
|
|
|
- ✅ 支持实时数据同步(跟单时同时写入)
|
|
|
|
|
|
- ✅ 支持通过 API 补充历史数据
|
|
|
|
|
|
- ✅ 唯一索引自动去重
|
|
|
|
|
|
|
|
|
|
|
|
### 2.2 移除 max_position_count 字段
|
|
|
|
|
|
|
|
|
|
|
|
**问题**: 文档中包含 `max_position_count` 字段,但 V26 迁移已删除该字段。
|
|
|
|
|
|
|
|
|
|
|
|
**解决方案**: 从 `backtest_task` 表和相关 API 中移除该字段。
|
|
|
|
|
|
|
|
|
|
|
|
### 2.3 其他字段建议
|
2026-01-31 00:26:00 +08:00
|
|
|
|
|
|
|
|
|
|
#### `backtest_task` 表
|
|
|
|
|
|
建议新增以下字段:
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
-- 用于计算平均持仓时间
|
|
|
|
|
|
avg_holding_time BIGINT DEFAULT NULL COMMENT '平均持仓时间(毫秒)',
|
|
|
|
|
|
|
|
|
|
|
|
-- 用于记录回测使用的数据源
|
2026-01-31 07:27:36 +08:00
|
|
|
|
data_source VARCHAR(50) DEFAULT 'MIXED' COMMENT '数据源: INTERNAL/API/MIXED',
|
2026-01-31 00:26:00 +08:00
|
|
|
|
|
|
|
|
|
|
-- 用于记录回测执行的详细日志
|
|
|
|
|
|
execution_log TEXT DEFAULT NULL COMMENT '执行日志(JSON格式)'
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-01-31 07:27:36 +08:00
|
|
|
|
### 2.4 索引优化
|
2026-01-31 00:26:00 +08:00
|
|
|
|
|
|
|
|
|
|
建议添加复合索引:
|
|
|
|
|
|
```sql
|
|
|
|
|
|
-- 用于按Leader和收益率查询
|
|
|
|
|
|
CREATE INDEX idx_leader_profit ON backtest_task(leader_id, profit_rate DESC);
|
|
|
|
|
|
|
|
|
|
|
|
-- 用于按状态和创建时间查询
|
|
|
|
|
|
CREATE INDEX idx_status_created ON backtest_task(status, created_at DESC);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 三、API设计补充
|
|
|
|
|
|
|
2026-01-31 07:27:36 +08:00
|
|
|
|
### 3.1 API 规范修正
|
|
|
|
|
|
|
|
|
|
|
|
**问题**: 文档中使用 GET/DELETE 方法,违反项目统一使用 POST 的规范。
|
|
|
|
|
|
|
|
|
|
|
|
**修正方案**:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# ❌ 错误(使用 GET/DELETE)
|
|
|
|
|
|
GET /api/backtest/tasks
|
|
|
|
|
|
GET /api/backtest/tasks/{id}
|
|
|
|
|
|
DELETE /api/backtest/tasks/{id}
|
|
|
|
|
|
|
|
|
|
|
|
# ✅ 正确(统一使用 POST)
|
|
|
|
|
|
POST /api/backtest/tasks/list
|
|
|
|
|
|
POST /api/backtest/tasks/detail
|
|
|
|
|
|
POST /api/backtest/tasks/delete
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**完整的 API 列表**:
|
|
|
|
|
|
|
|
|
|
|
|
| 功能 | 方法 | 路径 | 说明 |
|
|
|
|
|
|
|-----|------|------|------|
|
|
|
|
|
|
| 创建回测 | POST | /api/backtest/tasks | 创建新的回测任务 |
|
|
|
|
|
|
| 查询列表 | POST | /api/backtest/tasks/list | 分页查询回测任务列表 |
|
|
|
|
|
|
| 查询详情 | POST | /api/backtest/tasks/detail | 查询单个回测任务详情 |
|
|
|
|
|
|
| 查询交易 | POST | /api/backtest/tasks/trades | 查询回测的交易记录 |
|
|
|
|
|
|
| 删除任务 | POST | /api/backtest/tasks/delete | 删除回测任务 |
|
|
|
|
|
|
| 停止任务 | POST | /api/backtest/tasks/stop | 停止运行中的回测 |
|
|
|
|
|
|
| 查询进度 | POST | /api/backtest/tasks/progress | 查询回测执行进度 |
|
|
|
|
|
|
|
|
|
|
|
|
### 3.2 缺失的API
|
2026-01-31 00:26:00 +08:00
|
|
|
|
|
|
|
|
|
|
建议新增以下API:
|
|
|
|
|
|
|
2026-01-31 07:27:36 +08:00
|
|
|
|
#### 3.2.1 查询回测进度 (实时更新)
|
2026-01-31 00:26:00 +08:00
|
|
|
|
```
|
2026-01-31 07:27:36 +08:00
|
|
|
|
POST /api/backtest/tasks/progress
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**Request Body**:
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": 12345
|
|
|
|
|
|
}
|
2026-01-31 00:26:00 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**Response**:
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"data": {
|
|
|
|
|
|
"progress": 65,
|
|
|
|
|
|
"currentBalance": "1150.00",
|
|
|
|
|
|
"totalTrades": 30,
|
|
|
|
|
|
"status": "RUNNING"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-01-31 07:27:36 +08:00
|
|
|
|
#### 3.2.2 批量删除回测任务
|
2026-01-31 00:26:00 +08:00
|
|
|
|
```
|
2026-01-31 07:27:36 +08:00
|
|
|
|
POST /api/backtest/tasks/batch-delete
|
2026-01-31 00:26:00 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**Request Body**:
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"taskIds": [12345, 12346, 12347]
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-01-31 07:27:36 +08:00
|
|
|
|
#### 3.2.3 导出回测报告
|
|
|
|
|
|
```
|
|
|
|
|
|
POST /api/backtest/tasks/export
|
2026-01-31 00:26:00 +08:00
|
|
|
|
```
|
2026-01-31 07:27:36 +08:00
|
|
|
|
|
|
|
|
|
|
**Request Body**:
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": 12345,
|
|
|
|
|
|
"format": "csv" // 或 "pdf"
|
|
|
|
|
|
}
|
2026-01-31 00:26:00 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3.2 API错误码规范
|
|
|
|
|
|
|
|
|
|
|
|
建议统一错误码:
|
|
|
|
|
|
|
|
|
|
|
|
| 错误码 | 说明 |
|
|
|
|
|
|
|-------|------|
|
|
|
|
|
|
| 40001 | 回测任务不存在 |
|
|
|
|
|
|
| 40002 | Leader不存在 |
|
|
|
|
|
|
| 40003 | 回测天数超出限制 |
|
|
|
|
|
|
| 40004 | 初始金额无效 |
|
|
|
|
|
|
| 40005 | 回测任务正在运行,无法删除 |
|
|
|
|
|
|
| 50001 | 历史数据获取失败 |
|
|
|
|
|
|
| 50002 | 回测执行失败 |
|
|
|
|
|
|
|
|
|
|
|
|
## 四、前端实现补充
|
|
|
|
|
|
|
|
|
|
|
|
### 4.1 状态轮询
|
|
|
|
|
|
|
|
|
|
|
|
对于运行中的回测任务,前端需要定时轮询进度:
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (task.status === 'RUNNING') {
|
|
|
|
|
|
const interval = setInterval(async () => {
|
|
|
|
|
|
const progress = await backtestService.getProgress(task.id);
|
|
|
|
|
|
setTask({ ...task, ...progress });
|
|
|
|
|
|
}, 3000); // 每3秒轮询一次
|
|
|
|
|
|
|
|
|
|
|
|
return () => clearInterval(interval);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [task.status]);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4.2 图表数据压缩
|
|
|
|
|
|
|
|
|
|
|
|
当交易记录过多时,图表数据需要压缩:
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// 将数据按时间聚合为最多200个点
|
|
|
|
|
|
const compressChartData = (trades: BacktestTrade[], maxPoints: number = 200) => {
|
|
|
|
|
|
if (trades.length <= maxPoints) return trades;
|
|
|
|
|
|
|
|
|
|
|
|
const interval = Math.floor(trades.length / maxPoints);
|
|
|
|
|
|
return trades.filter((_, index) => index % interval === 0);
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4.3 国际化文案
|
|
|
|
|
|
|
|
|
|
|
|
需要在 `locales/` 目录下补充以下文案:
|
|
|
|
|
|
|
|
|
|
|
|
**zh-CN.json**:
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"backtest": {
|
|
|
|
|
|
"title": "回测管理",
|
|
|
|
|
|
"createTask": "新增回测",
|
|
|
|
|
|
"taskName": "回测名称",
|
|
|
|
|
|
"leader": "Leader",
|
|
|
|
|
|
"initialBalance": "初始金额",
|
|
|
|
|
|
"backtestDays": "回测天数",
|
|
|
|
|
|
"profitAmount": "收益额",
|
|
|
|
|
|
"profitRate": "收益率",
|
|
|
|
|
|
"status": {
|
|
|
|
|
|
"pending": "待执行",
|
|
|
|
|
|
"running": "运行中",
|
|
|
|
|
|
"completed": "已完成",
|
|
|
|
|
|
"stopped": "已停止",
|
|
|
|
|
|
"failed": "失败"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 五、测试计划补充
|
|
|
|
|
|
|
|
|
|
|
|
### 5.1 单元测试
|
|
|
|
|
|
|
|
|
|
|
|
**需要测试的核心方法**:
|
|
|
|
|
|
- `BacktestExecutionService.executeBacktest()` - 回测算法准确性
|
|
|
|
|
|
- `BacktestExecutionService.calculateStatistics()` - 统计数据计算
|
|
|
|
|
|
- `BacktestDataService.getLeaderHistoricalTrades()` - 历史数据获取
|
|
|
|
|
|
|
|
|
|
|
|
**测试用例示例**:
|
|
|
|
|
|
```kotlin
|
|
|
|
|
|
@Test
|
|
|
|
|
|
fun `test backtest with simple buy-sell scenario`() {
|
|
|
|
|
|
// Given: 初始余额1000, Leader买入100@0.5, 卖出100@0.6
|
|
|
|
|
|
val task = createTestTask(initialBalance = 1000.toBigDecimal())
|
|
|
|
|
|
val trades = listOf(
|
|
|
|
|
|
createBuyTrade(quantity = 100.toBigDecimal(), price = 0.5.toBigDecimal()),
|
|
|
|
|
|
createSellTrade(quantity = 100.toBigDecimal(), price = 0.6.toBigDecimal())
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// When: 执行回测
|
|
|
|
|
|
val result = executionService.executeBacktest(task)
|
|
|
|
|
|
|
|
|
|
|
|
// Then: 验证收益
|
|
|
|
|
|
// 买入: 100 * 0.5 = 50, 手续费0.1, 总成本50.1
|
|
|
|
|
|
// 卖出: 100 * 0.6 = 60, 手续费0.12, 净收入59.88
|
|
|
|
|
|
// 盈利: 59.88 - 50.1 = 9.78
|
|
|
|
|
|
// 最终余额: 1000 - 50.1 + 59.88 = 1009.78
|
|
|
|
|
|
assertEquals(1009.78.toBigDecimal(), result.finalBalance)
|
|
|
|
|
|
assertEquals(9.78.toBigDecimal(), result.profitAmount)
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 5.2 集成测试
|
|
|
|
|
|
|
|
|
|
|
|
**测试场景**:
|
|
|
|
|
|
1. 端到端测试: 创建任务 → 执行回测 → 查询结果
|
|
|
|
|
|
2. 异常场景: 历史数据为空、API调用失败
|
|
|
|
|
|
3. 边界条件: 余额刚好为0、单笔交易耗尽余额
|
|
|
|
|
|
|
|
|
|
|
|
### 5.3 性能测试
|
|
|
|
|
|
|
|
|
|
|
|
**测试指标**:
|
|
|
|
|
|
- 30天历史数据 (假设1000笔交易) 的回测执行时间 < 5分钟
|
|
|
|
|
|
- 并发5个回测任务时的系统资源占用
|
|
|
|
|
|
- 查询包含10000笔交易的回测详情页面加载时间 < 2秒
|
|
|
|
|
|
|
|
|
|
|
|
## 六、风险评估和缓解方案
|
|
|
|
|
|
|
|
|
|
|
|
### 6.1 数据准确性风险
|
|
|
|
|
|
|
|
|
|
|
|
**风险**: 历史数据不完整或API返回数据有误
|
|
|
|
|
|
|
|
|
|
|
|
**缓解方案**:
|
|
|
|
|
|
1. 数据验证: 检查返回数据的完整性 (是否有时间断层)
|
|
|
|
|
|
2. 数据对比: 使用多个数据源交叉验证
|
|
|
|
|
|
3. 错误标记: 回测结果标注数据质量等级
|
|
|
|
|
|
|
|
|
|
|
|
### 6.2 计算精度风险
|
|
|
|
|
|
|
|
|
|
|
|
**风险**: BigDecimal计算中的舍入误差累积
|
|
|
|
|
|
|
|
|
|
|
|
**缓解方案**:
|
|
|
|
|
|
1. 统一舍入模式: 使用 `RoundingMode.HALF_UP`
|
|
|
|
|
|
2. 精度测试: 编写专门的精度测试用例
|
|
|
|
|
|
3. 误差补偿: 最终余额与理论值的误差 < 0.01 USDC
|
|
|
|
|
|
|
|
|
|
|
|
### 6.3 性能风险
|
|
|
|
|
|
|
|
|
|
|
|
**风险**: 大量回测任务导致系统负载过高
|
|
|
|
|
|
|
|
|
|
|
|
**缓解方案**:
|
|
|
|
|
|
1. 任务队列: 使用异步任务队列 (可选: Redis Queue 或 RabbitMQ)
|
|
|
|
|
|
2. 资源限流: 限制单用户最多创建10个待执行任务
|
|
|
|
|
|
3. 自动清理: 定期清理30天前的回测记录
|
|
|
|
|
|
|
|
|
|
|
|
## 七、需要与用户确认的问题
|
|
|
|
|
|
|
|
|
|
|
|
> [!IMPORTANT]
|
|
|
|
|
|
> **关键决策点 - 需要用户反馈**:
|
|
|
|
|
|
|
|
|
|
|
|
### 7.1 卖出匹配策略
|
|
|
|
|
|
**问题**: 当用户多次买入同一市场时,卖出应该匹配哪笔买入?
|
|
|
|
|
|
|
|
|
|
|
|
**选项**:
|
|
|
|
|
|
- **选项A**: FIFO (先进先出) - 先卖出最早的买入
|
|
|
|
|
|
- **选项B**: 加权平均 - 按平均成本价计算盈亏
|
|
|
|
|
|
- **选项C**: 完全跟随Leader - Leader卖多少比例,我们也卖多少比例
|
|
|
|
|
|
|
|
|
|
|
|
**建议**: 选项C (完全跟随),与实际跟单逻辑保持一致
|
|
|
|
|
|
|
|
|
|
|
|
### 7.2 价格滑点模拟
|
|
|
|
|
|
**问题**: 是否需要在回测中模拟价格滑点?
|
|
|
|
|
|
|
|
|
|
|
|
**选项**:
|
|
|
|
|
|
- **选项A**: 不模拟,使用Leader成交价 (乐观估计)
|
|
|
|
|
|
- **选项B**: 固定滑点 (如买入+0.5%, 卖出-0.5%)
|
|
|
|
|
|
- **选项C**: 可配置滑点,用户自定义
|
|
|
|
|
|
|
|
|
|
|
|
**建议**: 选项C,增加灵活性
|
|
|
|
|
|
|
|
|
|
|
|
### 7.3 数据源选择
|
|
|
|
|
|
**问题**: 历史数据来源?
|
|
|
|
|
|
|
|
|
|
|
|
**选项**:
|
|
|
|
|
|
- **选项A**: 仅使用 Polymarket API
|
|
|
|
|
|
- **选项B**: 优先使用系统记录的 `ProcessedTrade` 表,不足时调用API
|
|
|
|
|
|
- **选项C**: 仅使用 `ProcessedTrade` 表 (限制回测范围为系统运行期间)
|
|
|
|
|
|
|
|
|
|
|
|
**建议**: 选项B,兼顾数据完整性和性能
|
|
|
|
|
|
|
|
|
|
|
|
### 7.4 回测结果保留时长
|
|
|
|
|
|
**问题**: 回测记录保留多久?
|
|
|
|
|
|
|
|
|
|
|
|
**选项**:
|
|
|
|
|
|
- **选项A**: 永久保留
|
|
|
|
|
|
- **选项B**: 保留30天,自动清理
|
|
|
|
|
|
- **选项C**: 用户手动删除,无自动清理
|
|
|
|
|
|
|
|
|
|
|
|
**建议**: 选项B,避免数据库膨胀
|
|
|
|
|
|
|
|
|
|
|
|
## 八、文档总结
|
|
|
|
|
|
|
|
|
|
|
|
### 已完成的文档
|
|
|
|
|
|
1. ✅ **BACKTEST_PRD.md** - 产品需求文档
|
|
|
|
|
|
2. ✅ **BACKTEST_TECHNICAL_DESIGN.md** - 技术设计文档
|
|
|
|
|
|
3. ✅ **BACKTEST_REVIEW_CHECKLIST.md** - 设计审查清单 (本文档)
|
|
|
|
|
|
|
|
|
|
|
|
### 建议补充的文档 (可选)
|
|
|
|
|
|
1. **BACKTEST_API_SPEC.md** - API接口规范 (从技术设计文档提取)
|
|
|
|
|
|
2. **BACKTEST_DATABASE_MIGRATION.md** - 数据库迁移脚本
|
|
|
|
|
|
3. **BACKTEST_TEST_PLAN.md** - 详细测试计划
|
|
|
|
|
|
|
|
|
|
|
|
### 下一步行动
|
|
|
|
|
|
1. **用户Review**: 请用户审查以上文档,确认关键设计点
|
|
|
|
|
|
2. **补充遗漏**: 根据用户反馈补充缺失部分
|
|
|
|
|
|
3. **进入执行**: 用户确认后开始实施开发
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
**审查日期**: 2026-01-30
|
|
|
|
|
|
**审查人**: AI Assistant
|
|
|
|
|
|
**状态**: 待用户确认
|