docs: 新增跟单回测功能设计文档

- 新增产品需求文档 (BACKTEST_PRD.md)
- 新增技术设计文档 (BACKTEST_TECHNICAL_DESIGN.md)
- 新增设计审查清单 (BACKTEST_REVIEW_CHECKLIST.md)
- 新增文档总览 (README.md)

关键设计:
- 实时市场结算机制(按endDate检查)
- 严格余额检查(不计入未实现持仓)
- 停止条件优化(余额不足且无持仓时停止)
- 不计算手续费(简化逻辑)
This commit is contained in:
WrBug
2026-01-31 00:26:00 +08:00
parent e8fd1b503b
commit f1f809f54b
4 changed files with 1848 additions and 0 deletions
+324
View File
@@ -0,0 +1,324 @@
# 跟单回测功能产品需求文档 (PRD)
## 一、功能概述
跟单回测功能允许用户对历史数据进行模拟跟单交易,评估不同跟单策略的收益表现,帮助用户在实际投入资金前验证策略的有效性。
## 二、用户故事
### 主要用户场景
1. **作为用户**,我希望能够创建回测任务,选择特定的 Leader 和跟单配置,以便评估在过去一段时间内使用该策略的收益情况
2. **作为用户**,我希望能够查看所有历史回测结果,并按收益额或收益率排序,以便找到最优策略
3. **作为用户**,我希望能够按 Leader 筛选回测记录,以便比较不同 Leader 的表现
4. **作为用户**,我希望回测结果能够展示详细的交易记录和收益变化,以便深入分析策略表现
5. **作为用户**,我希望回测在资金不足时能够自动停止,模拟真实交易场景
## 三、功能需求
### 3.1 回测管理页面
#### 3.1.1 页面布局
- **页面位置**: 在跟单管理模块下新增"回测管理"菜单项
- **页面标题**: "回测管理" / "Backtest Management"
#### 3.1.2 列表功能
**筛选功能**:
- Leader 筛选下拉框,支持按 Leader 过滤回测记录
- 状态筛选: 全部 / 运行中 / 已完成 / 已停止
**排序功能**:
- 按收益额排序 (升序/降序)
- 按收益率排序 (升序/降序)
- 按创建时间排序 (默认降序)
**列表字段**:
| 字段名 | 说明 | 示例 |
|-------|------|------|
| 回测ID | 唯一标识 | #12345 |
| 配置名称 | 回测任务名称 | "激进策略-Leader A" |
| Leader名称 | 跟单的Leader | "Smart Trader" |
| 初始金额 | 回测起始资金 | $1000 |
| 最终金额 | 回测结束时资金 | $1250 |
| 收益额 | 最终金额 - 初始金额 | +$250 |
| 收益率 | (收益额/初始金额) × 100% | +25% |
| 回测天数 | 回测的时间跨度 | 30天 |
| 交易笔数 | 回测期间执行的交易数量 | 45笔 |
| 状态 | 运行中/已完成/已停止 | 已完成 |
| 开始时间 | 回测开始时间 | 2026-01-01 10:00 |
| 结束时间 | 回测结束时间 | 2026-01-31 15:30 |
| 操作 | 查看详情/删除 | - |
#### 3.1.3 列表操作
- **查看详情**: 点击后展开详细信息,包括:
- 回测配置参数
- 详细交易记录
- 资金变化曲线图
- 收益统计
- **删除**: 删除回测记录(确认后不可恢复)
### 3.2 新增回测任务
#### 3.2.1 创建入口
- 列表页面右上角"新增回测"按钮
- 点击后弹出创建对话框或跳转到创建页面
#### 3.2.2 配置表单
**基本配置**:
- **回测名称** (必填): 用户自定义名称,方便识别
- **选择Leader** (必填): 下拉框选择已添加的Leader
- **初始投入金额** (必填): 模拟起始资金,单位USDC,范围: 1 - 1,000,000
- **回测天数** (必填): 选择回测的历史天数,范围: 1 - 30天
**跟单配置** (参照现有跟单配置参数):
| 配置项 | 字段名 | 说明 | 默认值 |
|-------|-------|------|-------|
| 跟单模式 | copyMode | RATIO(比例模式) / FIXED(固定金额) | RATIO |
| 跟单比例 | copyRatio | 比例模式下生效,相对Leader订单金额的比例 | 1.0 |
| 固定金额 | fixedAmount | 固定金额模式下生效,每笔固定投入金额 | null |
| 最大单笔订单 | maxOrderSize | 单笔订单最大金额限制 | 1000 |
| 最小单笔订单 | minOrderSize | 单笔订单最小金额限制 | 1 |
| 最大每日亏损 | maxDailyLoss | 每日最大亏损限制 | 10000 |
| 最大每日订单数 | maxDailyOrders | 每日最大订单数量限制 | 100 |
| 价格容忍度 | priceTolerance | 价格偏差容忍百分比 | 5% |
| 延迟秒数 | delaySeconds | 跟单延迟时间 | 0 |
| 支持卖出 | supportSell | 是否跟随卖出 | true |
| 最小订单深度 | minOrderDepth | 订单簿深度要求 | null |
| 最大价差 | maxSpread | 买卖价差限制 | null |
| 最低价格 | minPrice | 最低价格限制 | null |
| 最高价格 | maxPrice | 最高价格限制 | null |
| 最大仓位金额 | maxPositionValue | 最大持仓总金额 | null |
| 最大仓位数量 | maxPositionCount | 最大持仓数量 | null |
| 关键字过滤模式 | keywordFilterMode | DISABLED/WHITELIST/BLACKLIST | DISABLED |
| 关键字列表 | keywords | 关键字数组 | [] |
| 市场截止时间限制 | maxMarketEndDate | 市场结束时间限制 | null |
> [!IMPORTANT]
> 跟单配置表单应完全复用现有的跟单配置组件,保持参数一致性
#### 3.2.3 表单验证
- 回测名称: 不能为空,长度1-100字符
- Leader: 必须选择有效的Leader
- 初始金额: 必须大于0
- 回测天数: 必须在1-30之间
- 其他配置参数: 遵循现有跟单配置的验证规则
#### 3.2.4 提交逻辑
1. 表单验证通过后,提交到后端API
2. 后端保存回测任务到数据库,状态设置为"待执行"
3. 前端显示创建成功提示,自动跳转到列表页面
4. 回测任务由后端轮询服务自动获取并执行
### 3.3 回测详情页面
#### 3.3.1 页面布局
**顶部概览卡片**:
- 回测名称
- Leader信息
- 初始金额 / 最终金额
- 收益额 / 收益率
- 回测时间范围
- 总交易笔数
- 状态
**资金变化图表**:
- X轴: 时间
- Y轴: 账户余额
- 折线图展示资金随时间的变化
**交易记录表格**:
| 时间 | 市场 | 方向 | 数量 | 价格 | 金额 | 盈亏 | 余额 |
|-----|------|-----|------|------|------|------|------|
| 2026-01-01 10:05 | BTC > $100k | 买入 YES | 100 | 0.65 | 65 | - | 935.00 |
| 2026-01-01 14:20 | BTC > $100k | 卖出 YES | 100 | 0.72 | 72 | +7.00 | 942.00 |
| 2026-01-05 16:00 | ETH > $5k | 买入 YES | 50 | 0.80 | 40 | - | 902.00 |
| 2026-01-10 00:00 | ETH > $5k | 市场结算 YES | 50 | 1.00 | 50 | +10.00 | 952.00 |
> [!NOTE]
> **交易类型说明**:
> - **买入**: 跟随Leader买入
> - **卖出**: 跟随Leader卖出
> - **市场结算**: 市场到期自动结算(赎回)
>
> **手续费**: 回测不计算手续费,简化计算逻辑
**统计数据**:
- 总交易笔数
- 买入笔数 / 卖出笔数
- 胜率 (盈利交易 / 总交易)
- 平均收益
- 最大单笔盈利
- 最大单笔亏损
- 最大回撤
## 四、业务规则
### 4.1 回测执行规则
#### 4.1.1 资金检查
- **停止条件**: 当账户余额 < $1 **且无任何持仓**时,回测自动停止
- **继续条件**: 如果余额 < $1 但仍有持仓,继续处理后续交易
- 原因: Leader 可能卖出,或市场到期结算,释放资金
- 处理: 跳过无法执行的买入订单,继续处理卖出和结算
- **订单检查**: 每次下单前检查余额是否充足
- **不足处理**: 余额不足时跳过该买入订单,记录日志,继续监听后续交易
> [!IMPORTANT]
> 只要有持仓存在,就不应停止回测,因为后续可能通过卖出或市场结算回收资金
#### 4.1.2 历史数据获取
- 从 Polymarket API 获取 Leader 的历史交易记录
- 根据回测天数计算起始时间: `startTime = now - (backtestDays × 24 × 3600 × 1000)`
- 按时间顺序回放交易
#### 4.1.3 交易执行模拟
- 按照配置的跟单规则计算跟单金额
- 应用所有过滤条件 (价格、深度、关键字等)
- 模拟订单成交(不计算手续费)
- 更新账户余额
> [!NOTE]
> 回测不计算手续费,简化计算逻辑,避免过于复杂的精度问题
#### 4.1.3.1 余额不足的处理 ⚠️
**场景说明**:
- 当前余额不足以执行买入订单
- 但有持仓未卖出(相当于"待赎回资产")
**处理策略**:
**方案A: 严格模式**(推荐)
- ✅ 仅使用当前可用余额(`currentBalance`
- ✅ 余额不足时跳过该订单,不考虑持仓价值
- ✅ 理由: 更保守,模拟真实场景(持仓未卖出前资金不可用)
**方案B: 宽松模式**(可选)
- 计算"潜在可用资金" = `currentBalance + 持仓市值`
- 允许"透支"买入,后续通过卖出或结算平衡
- 风险: 可能产生不切实际的回测结果
**推荐实现**:
```kotlin
// 严格检查余额
if (totalCost > currentBalance) {
logger.info("余额不足以执行买入订单: 需要 $totalCost, 可用 $currentBalance")
logger.debug("当前持仓价值: ${calculatePositionValue(positions)}, 但不计入可用余额")
continue // 跳过该订单
}
```
**特殊情况: 市场即将结算**
- 如果持仓市场在接下来很短时间内会结算,可以提前释放资金
- 实现: 在每次交易前先执行市场结算检查(已在4.1.5实现)
> [!IMPORTANT]
> 采用**严格模式**更符合真实跟单场景,避免回测结果过于乐观
#### 4.1.4 卖出跟随
- 如果 `supportSell = true`,跟随 Leader 的卖出操作
- 按照买入时的比例卖出持仓
- 计算盈亏并更新余额
#### 4.1.5 市场结算处理 ⭐
- **触发时机**:
- **实时检查**: 在处理每笔Leader交易前,检查所有持仓市场的`endDate`
- **到期即结算**: 如果市场结束时间 ≤ 当前交易时间,立即结算该持仓
- **兜底处理**: 回测结束时,结算所有剩余持仓
- **结算规则**:
- 获取市场最终结果 (通过Polymarket API)
- 持仓方向为胜出方: 按 **1.0** 价格结算
- 持仓方向为失败方: 按 **0.0** 价格结算
- 市场未结算或无法获取结果: 按**成本价**结算 (保守估计)
- **资金流转**: 结算后的资金立即计入余额,可用于后续交易
- **交易记录**: 生成"市场结算"类型的交易记录,用于详情展示
> [!IMPORTANT]
> **实时结算的优势**:
> - ✅ 模拟真实场景: 市场结束时资金会自动返还
> - ✅ 提高资金利用率: 结算后的资金可以参与后续交易
> - ✅ 更准确的收益计算: 反映实际的资金周转情况
### 4.2 数据持久化
#### 4.2.1 回测任务表
- 保存回测基本信息和配置
- 记录执行状态和结果
#### 4.2.2 回测交易记录表
- 保存每笔模拟交易的详细信息
- 用于详情页面展示和分析
### 4.3 并发控制
- 同一时间最多支持 5 个回测任务并发执行
- 新任务排队等待,FIFO策略
- 前端显示任务队列位置
## 五、UI/UX 要求
### 5.1 响应式设计
- 支持桌面和移动端浏览
- 表格在小屏幕上支持横向滚动
### 5.2 国际化
- 支持中文和英文
- 所有文案提供双语版本
### 5.3 交互体验
- 创建回测: 表单提交时显示Loading状态
- 回测执行中: 显示进度条或百分比
- 数据加载: Skeleton占位符
- 操作反馈: Toast提示 (成功/失败/警告)
### 5.4 数据可视化
- 资金变化图表使用 ECharts 或 Recharts
- 支持图表缩放和数据点Tooltip
- 图表颜色: 盈利绿色,亏损红色
## 六、非功能需求
### 6.1 性能要求
- 回测列表页面加载时间 < 2秒
- 单个回测任务执行时间 < 5分钟 (30天数据)
- 详情页图表渲染时间 < 1秒
### 6.2 数据准确性
- 回测结果误差 < 0.1%
- 余额计算使用 BigDecimal 避免精度丢失
- 价格和数量精确到小数点后8位
### 6.3 安全性
- 回测数据仅用户本人可见
- API接口需要身份认证
- 防止SQL注入和XSS攻击
## 七、后续迭代规划
### Phase 2 (可选)
- 支持批量创建回测任务
- 回测结果对比功能
- 导出回测报告 (PDF/Excel)
- AI策略推荐
### Phase 3 (可选)
- 实时回测 (边交易边回测)
- 社区策略分享
- 策略市场
---
## 附录: 页面路由规划
- 回测列表: `/copy-trading/backtest`
- 新增回测: `/copy-trading/backtest/create`
- 回测详情: `/copy-trading/backtest/:id`
@@ -0,0 +1,481 @@
# 回测功能设计审查清单
## 一、设计审查要点
### 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,减少表的大小
### 1.3 业务逻辑准确性 ⚠️
**需要验证的关键逻辑**:
#### 1.3.1 卖出匹配逻辑
> [!CAUTION]
> **潜在问题**: 当前设计中,卖出时如何匹配对应的买入持仓?
>
> **当前方案**: 使用 `positionKey = marketId + outcome` 匹配
>
> **问题场景**:
> - 同一市场同一方向多次买入,价格不同 (需要先进先出FIFO吗?)
> - Leader部分卖出时,如何计算跟单的卖出比例?
>
> **建议**:
> - 明确卖出匹配策略: FIFO (先进先出) 或 加权平均价
> - 在技术设计文档中补充详细说明
#### 1.3.2 价格滑点模拟
> [!NOTE]
> **关键问题**: 是否需要模拟价格滑点?
>
> **当前设计**: 不模拟价格滑点,直接使用Leader的成交价
> - 优点: 简化逻辑,回测速度快
> - 缺点: 可能高估收益(实际跟单可能有滑点)
>
> **可选方案**: 增加可配置的滑点参数
> - 买入时: 价格 × (1 + 滑点%)
> - 卖出时: 价格 × (1 - 滑点%)
> - 增加可选的滑点模拟参数 (例如: ±0.5%)
> - 在PRD中补充此配置项
#### 1.3.3 手续费计算 ✅ (已移除)
> [!NOTE]
> **用户决策**: 回测不计算手续费
>
> **理由**:
> - 简化计算逻辑
> - 避免精度问题
> - 降低复杂度
>
> **实现**:
> - 所有交易的 `fee` 字段均为 `0`
> - 买入成本 = 数量 × 价格
> - 卖出收入 = 数量 × 价格
> - 结算收入 = 数量 × 结算价
#### 1.3.4 市场结算处理 ⭐ (已优化)
> [!NOTE]
> **关键问题**: 市场结束时,未平仓位如何自动结算?
>
> **优化方案** (采纳用户建议):
> - ✅ **实时检查**: 在回测循环中,每处理一笔Leader交易前,检查所有持仓的市场`endDate`
> - ✅ **到期即结算**: 如果 `marketEndDate <= currentTradeTime`,立即结算该持仓
> - ✅ **资金可用**: 结算后的资金立即计入余额,可以用于后续交易
> - ✅ **兜底处理**: 回测结束时,结算所有剩余未到期持仓
>
> **实现要点**:
> ```kotlin
> // 在交易循环中实时检查市场到期
> for (leaderTrade in leaderTrades.sortedBy { it.timestamp }) {
>
> // 1. 检查并结算已到期的市场
> val expiredPositions = positions.filter { (_, position) ->
> val marketInfo = getMarketInfo(position.marketId)
> marketInfo.endDate <= leaderTrade.timestamp
> }
>
> for ((positionKey, position) in expiredPositions) {
> // 结算逻辑...
> currentBalance += settlementValue
> positions.remove(positionKey)
> }
>
> // 2. 处理当前Leader交易
> // ...
> }
> ```
>
> **优势**:
> - 更符合真实场景 (市场结束时自动返还资金)
> - 提高资金利用率 (结算资金可参与后续交易)
> - 更准确的收益计算
#### 1.3.5 余额不足与持仓处理 ⚠️ (边缘场景) - 已修正
> [!WARNING]
> **关键问题**: 当余额不足但有未卖出持仓时,如何处理?
>
> **场景示例**:
> - 初始余额: $1000
> - 已买入持仓市值: $800(未卖出)
> - 当前余额: $200
> - 新买入订单需要: $300
> - **问题**: 是否允许买入?虽然持仓市值足够,但资金被占用
>
> **推荐方案: 严格模式**
> - ❌ 不允许买入(余额不足)
> - ✅ 仅使用 `currentBalance` 判断
> - ✅ 不计入持仓市值(因为持仓未实现)
> - ✅ 理由: 更真实,避免过于乐观的回测结果
>
> **替代方案: 宽松模式**
> - ✅ 计算"虚拟可用资金" = `currentBalance + 持仓估值`
> - ⚠️ 允许"透支"买入
> - ❌ 风险: 可能产生不切实际的收益
>
> **实际影响**:
> - 严格模式下,资金周转率是限制因素
> - 鼓励快进快出的策略
> - 长期持仓策略会因资金占用而错过后续机会
>
> **已在文档中采用**: 严格模式
#### 1.3.6 回测停止条件 ✅ (已修正)
> [!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调用
## 二、数据库设计补充
### 2.1 缺失的字段建议
#### `backtest_task` 表
建议新增以下字段:
```sql
-- 用于计算平均持仓时间
avg_holding_time BIGINT DEFAULT NULL COMMENT '平均持仓时间(毫秒)',
-- 用于记录回测使用的数据源
data_source VARCHAR(50) DEFAULT 'API' COMMENT '数据源: INTERNAL/API/MIXED',
-- 用于记录回测执行的详细日志
execution_log TEXT DEFAULT NULL COMMENT '执行日志(JSON格式)'
```
### 2.2 索引优化
建议添加复合索引:
```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设计补充
### 3.1 缺失的API
建议新增以下API:
#### 3.1.1 查询回测进度 (实时更新)
```
GET /api/backtest/tasks/{id}/progress
```
**Response**:
```json
{
"success": true,
"data": {
"progress": 65,
"currentBalance": "1150.00",
"totalTrades": 30,
"status": "RUNNING"
}
}
```
#### 3.1.2 批量删除回测任务
```
DELETE /api/backtest/tasks
```
**Request Body**:
```json
{
"taskIds": [12345, 12346, 12347]
}
```
#### 3.1.3 导出回测报告
```
GET /api/backtest/tasks/{id}/export?format=csv|pdf
```
### 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
**状态**: 待用户确认
@@ -0,0 +1,967 @@
# 跟单回测功能技术设计文档
## 一、技术架构概览
```mermaid
graph TB
subgraph "Frontend Layer"
A[回测管理页面] --> B[回测创建页面]
A --> C[回测详情页面]
end
subgraph "Backend API Layer"
D[BacktestController]
end
subgraph "Service Layer"
E[BacktestService] --> F[BacktestExecutionService]
E --> G[BacktestDataService]
F --> H[CopyTradingFilterService]
end
subgraph "Scheduled Tasks"
I[BacktestPollingService]
end
subgraph "Data Layer"
J[(Backtest Task Table)]
K[(Backtest Trade Table)]
L[(Leader Historical Data)]
end
A --> D
B --> D
C --> D
D --> E
I --> F
E --> J
F --> K
F --> L
G --> L
F --> H
```
## 二、数据库设计
### 2.1 回测任务表 (backtest_task)
```sql
CREATE TABLE backtest_task (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '回测任务ID',
task_name VARCHAR(100) NOT NULL COMMENT '回测任务名称',
leader_id BIGINT NOT NULL COMMENT 'Leader ID',
initial_balance DECIMAL(20, 8) NOT NULL COMMENT '初始资金',
final_balance DECIMAL(20, 8) DEFAULT NULL COMMENT '最终资金',
profit_amount DECIMAL(20, 8) DEFAULT NULL COMMENT '收益金额',
profit_rate DECIMAL(10, 4) DEFAULT NULL COMMENT '收益率(%)',
backtest_days INT NOT NULL COMMENT '回测天数',
start_time BIGINT NOT NULL COMMENT '回测开始时间(历史时间)',
end_time BIGINT DEFAULT NULL COMMENT '回测结束时间(历史时间)',
-- 跟单配置 (复制CopyTrading表结构)
copy_mode VARCHAR(10) NOT NULL COMMENT '跟单模式: RATIO/FIXED',
copy_ratio DECIMAL(20, 8) DEFAULT 1.0 COMMENT '跟单比例',
fixed_amount DECIMAL(20, 8) DEFAULT NULL COMMENT '固定金额',
max_order_size DECIMAL(20, 8) NOT NULL COMMENT '最大单笔订单',
min_order_size DECIMAL(20, 8) NOT NULL COMMENT '最小单笔订单',
max_daily_loss DECIMAL(20, 8) NOT NULL COMMENT '最大每日亏损',
max_daily_orders INT NOT NULL COMMENT '最大每日订单数',
price_tolerance DECIMAL(5, 2) NOT NULL COMMENT '价格容忍度(%)',
delay_seconds INT DEFAULT 0 COMMENT '延迟秒数',
support_sell BOOLEAN DEFAULT TRUE COMMENT '是否支持卖出',
min_order_depth DECIMAL(20, 8) DEFAULT NULL COMMENT '最小订单深度',
max_spread DECIMAL(20, 8) DEFAULT NULL COMMENT '最大价差',
min_price DECIMAL(20, 8) DEFAULT NULL COMMENT '最低价格',
max_price DECIMAL(20, 8) DEFAULT NULL COMMENT '最高价格',
max_position_value DECIMAL(20, 8) DEFAULT NULL COMMENT '最大仓位金额',
max_position_count INT DEFAULT NULL COMMENT '最大仓位数量',
keyword_filter_mode VARCHAR(20) DEFAULT 'DISABLED' COMMENT '关键字过滤模式',
keywords JSON DEFAULT NULL COMMENT '关键字列表',
max_market_end_date BIGINT DEFAULT NULL COMMENT '市场截止时间限制',
-- 执行状态
status VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '状态: PENDING/RUNNING/COMPLETED/STOPPED/FAILED',
progress INT DEFAULT 0 COMMENT '执行进度(0-100)',
total_trades INT DEFAULT 0 COMMENT '总交易笔数',
buy_trades INT DEFAULT 0 COMMENT '买入笔数',
sell_trades INT DEFAULT 0 COMMENT '卖出笔数',
win_trades INT DEFAULT 0 COMMENT '盈利交易笔数',
loss_trades INT DEFAULT 0 COMMENT '亏损交易笔数',
win_rate DECIMAL(5, 2) DEFAULT NULL COMMENT '胜率(%)',
max_profit DECIMAL(20, 8) DEFAULT NULL COMMENT '最大单笔盈利',
max_loss DECIMAL(20, 8) DEFAULT NULL COMMENT '最大单笔亏损',
max_drawdown DECIMAL(20, 8) DEFAULT NULL COMMENT '最大回撤',
error_message TEXT DEFAULT NULL COMMENT '错误信息',
created_at BIGINT NOT NULL COMMENT '创建时间',
execution_started_at BIGINT DEFAULT NULL COMMENT '执行开始时间(系统时间)',
execution_finished_at BIGINT DEFAULT NULL COMMENT '执行完成时间(系统时间)',
updated_at BIGINT NOT NULL COMMENT '更新时间',
INDEX idx_leader_id (leader_id),
INDEX idx_status (status),
INDEX idx_created_at (created_at)
) COMMENT='回测任务表';
```
### 2.2 回测交易记录表 (backtest_trade)
```sql
CREATE TABLE backtest_trade (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '交易记录ID',
backtest_task_id BIGINT NOT NULL COMMENT '回测任务ID',
trade_time BIGINT NOT NULL COMMENT '交易时间',
market_id VARCHAR(100) NOT NULL COMMENT '市场ID',
market_title VARCHAR(500) DEFAULT NULL COMMENT '市场标题',
side VARCHAR(20) NOT NULL COMMENT '方向: BUY/SELL/SETTLEMENT',
outcome VARCHAR(50) NOT NULL COMMENT '结果: YES/NO或outcomeIndex',
quantity DECIMAL(20, 8) NOT NULL COMMENT '数量',
price DECIMAL(20, 8) NOT NULL COMMENT '价格',
amount DECIMAL(20, 8) NOT NULL COMMENT '金额',
fee DECIMAL(20, 8) NOT NULL COMMENT '手续费',
profit_loss DECIMAL(20, 8) DEFAULT NULL COMMENT '盈亏(仅卖出时)',
balance_after DECIMAL(20, 8) NOT NULL COMMENT '交易后余额',
leader_trade_id VARCHAR(100) DEFAULT NULL COMMENT 'Leader原始交易ID',
created_at BIGINT NOT NULL COMMENT '创建时间',
INDEX idx_backtest_task_id (backtest_task_id),
INDEX idx_trade_time (trade_time),
FOREIGN KEY (backtest_task_id) REFERENCES backtest_task(id) ON DELETE CASCADE
) COMMENT='回测交易记录表';
```
### 2.3 索引优化建议
- `backtest_task`:
- 主查询索引: `idx_leader_id`, `idx_status`
- 排序索引: `idx_created_at`
- `backtest_trade`:
- 关联查询索引: `idx_backtest_task_id`
- 时间序列索引: `idx_trade_time`
## 三、API设计
### 3.1 RESTful API规范
#### 3.1.1 创建回测任务
```
POST /api/backtest/tasks
```
**Request Body**:
```json
{
"taskName": "激进策略-Leader A",
"leaderId": 123,
"initialBalance": "1000.00",
"backtestDays": 30,
"copyMode": "RATIO",
"copyRatio": "1.0",
"fixedAmount": null,
"maxOrderSize": "1000.00",
"minOrderSize": "1.00",
"maxDailyLoss": "10000.00",
"maxDailyOrders": 100,
"priceTolerance": "5.00",
"delaySeconds": 0,
"supportSell": true,
"minOrderDepth": null,
"maxSpread": null,
"minPrice": null,
"maxPrice": null,
"maxPositionValue": null,
"maxPositionCount": null,
"keywordFilterMode": "DISABLED",
"keywords": [],
"maxMarketEndDate": null
}
```
**Response**:
```json
{
"success": true,
"data": {
"id": 12345,
"taskName": "激进策略-Leader A",
"status": "PENDING",
"createdAt": 1738238400000
},
"message": "回测任务创建成功"
}
```
#### 3.1.2 查询回测任务列表
```
GET /api/backtest/tasks?leaderId={leaderId}&status={status}&sortBy={field}&sortOrder={asc|desc}&page={page}&size={size}
```
**Query Parameters**:
- `leaderId` (可选): Leader ID
- `status` (可选): PENDING/RUNNING/COMPLETED/STOPPED/FAILED
- `sortBy` (可选): profitAmount / profitRate / createdAt (默认: createdAt)
- `sortOrder` (可选): asc / desc (默认: desc)
- `page` (可选): 页码,从1开始 (默认: 1)
- `size` (可选): 每页数量 (默认: 20)
**Response**:
```json
{
"success": true,
"data": {
"list": [
{
"id": 12345,
"taskName": "激进策略-Leader A",
"leaderId": 123,
"leaderName": "Smart Trader",
"leaderAddress": "0x123...",
"initialBalance": "1000.00",
"finalBalance": "1250.00",
"profitAmount": "250.00",
"profitRate": "25.00",
"backtestDays": 30,
"totalTrades": 45,
"status": "COMPLETED",
"startTime": 1735646400000,
"endTime": 1738238400000,
"createdAt": 1738238400000
}
],
"total": 100,
"page": 1,
"size": 20
}
}
```
#### 3.1.3 查询回测任务详情
```
GET /api/backtest/tasks/{id}
```
**Response**:
```json
{
"success": true,
"data": {
"id": 12345,
"taskName": "激进策略-Leader A",
"leaderId": 123,
"leaderName": "Smart Trader",
"initialBalance": "1000.00",
"finalBalance": "1250.00",
"profitAmount": "250.00",
"profitRate": "25.00",
"backtestDays": 30,
"startTime": 1735646400000,
"endTime": 1738238400000,
"config": {
"copyMode": "RATIO",
"copyRatio": "1.0",
// ... 其他配置
},
"statistics": {
"totalTrades": 45,
"buyTrades": 23,
"sellTrades": 22,
"winTrades": 30,
"lossTrades": 15,
"winRate": "66.67",
"maxProfit": "50.00",
"maxLoss": "-20.00",
"maxDrawdown": "100.00"
},
"status": "COMPLETED",
"progress": 100,
"createdAt": 1738238400000
}
}
```
#### 3.1.4 查询回测交易记录
```
GET /api/backtest/tasks/{id}/trades?page={page}&size={size}
```
**Response**:
```json
{
"success": true,
"data": {
"list": [
{
"id": 1,
"tradeTime": 1735646400000,
"marketTitle": "BTC > $100k",
"side": "BUY",
"outcome": "YES",
"quantity": "100.00",
"price": "0.65",
"amount": "65.00",
"fee": "0.13",
"profitLoss": null,
"balanceAfter": "934.87"
}
],
"total": 45,
"page": 1,
"size": 20
}
}
```
#### 3.1.5 删除回测任务
```
DELETE /api/backtest/tasks/{id}
```
**Response**:
```json
{
"success": true,
"message": "回测任务删除成功"
}
```
#### 3.1.6 停止运行中的回测
```
POST /api/backtest/tasks/{id}/stop
```
**Response**:
```json
{
"success": true,
"message": "回测任务已停止"
}
```
## 四、后端服务设计
### 4.1 Service层架构
#### 4.1.1 BacktestService
**职责**: 回测任务的CRUD操作
**核心方法**:
```kotlin
interface BacktestService {
// 创建回测任务
fun createBacktestTask(request: BacktestCreateRequest): Result<BacktestTaskDto>
// 查询回测任务列表
fun getBacktestTaskList(request: BacktestListRequest): Result<BacktestListResponse>
// 查询回测任务详情
fun getBacktestTaskDetail(taskId: Long): Result<BacktestTaskDetailDto>
// 删除回测任务
fun deleteBacktestTask(taskId: Long): Result<Unit>
// 停止回测任务
fun stopBacktestTask(taskId: Long): Result<Unit>
// 查询回测交易记录
fun getBacktestTrades(taskId: Long, page: Int, size: Int): Result<BacktestTradeListResponse>
}
```
**实现要点**:
- 复用 `CopyTradingFilterService` 的参数验证逻辑
- 使用 `@Transactional` 保证数据一致性
- 返回值使用 `Result<T>` 统一错误处理
#### 4.1.2 BacktestExecutionService
**职责**: 执行回测任务
**核心方法**:
```kotlin
interface BacktestExecutionService {
// 执行回测任务
suspend fun executeBacktest(task: BacktestTask)
// 获取Leader历史交易
suspend fun getLeaderHistoricalTrades(
leaderId: Long,
startTime: Long,
endTime: Long
): List<HistoricalTrade>
// 模拟交易执行
suspend fun simulateTrade(
task: BacktestTask,
trade: HistoricalTrade,
currentBalance: BigDecimal,
positions: MutableMap<String, Position>
): TradeResult
// 计算收益统计
fun calculateStatistics(trades: List<BacktestTrade>): BacktestStatistics
}
```
**执行流程**:
```mermaid
sequenceDiagram
participant P as BacktestPollingService
participant E as BacktestExecutionService
participant D as BacktestDataService
participant F as CopyTradingFilterService
participant DB as Database
P->>DB: 查询PENDING状态任务
DB-->>P: 返回待执行任务
P->>E: executeBacktest(task)
E->>DB: 更新状态为RUNNING
E->>D: getLeaderHistoricalTrades()
D-->>E: 返回历史交易列表
loop 遍历每笔交易
E->>E: 检查余额是否充足
alt 余额 >= $1
E->>F: 应用过滤规则
F-->>E: 返回是否通过
alt 通过过滤
E->>E: 计算跟单金额
E->>E: 模拟成交,扣除手续费
E->>DB: 保存交易记录
E->>E: 更新余额和持仓
end
else 余额 < $1
E->>E: 停止回测
end
end
E->>E: calculateStatistics()
E->>DB: 更新任务状态和统计数据
E-->>P: 执行完成
```
#### 4.1.3 BacktestDataService
**职责**: 获取Leader历史数据
**数据源**:
1. **优先使用**: `ProcessedTrade` 表 (系统已记录的交易)
2. **补充数据**: Polymarket API (获取更早的历史数据)
**API调用**:
```kotlin
// Polymarket Trade History API
// GET https://data-api.polymarket.com/trades?maker={address}&start_ts={startTime}&end_ts={endTime}
```
**缓存策略**:
- 使用 Redis 缓存历史交易数据,TTL = 1小时
- Key格式: `backtest:leader:{leaderId}:trades:{startTime}:{endTime}`
#### 4.1.4 BacktestPollingService
**职责**: 轮询待执行的回测任务
**实现方式**:
```kotlin
@Service
class BacktestPollingService(
private val backtestTaskRepository: BacktestTaskRepository,
private val executionService: BacktestExecutionService
) {
private val logger = LoggerFactory.getLogger(BacktestPollingService::class.java)
private val executor = Executors.newFixedThreadPool(5) // 最多5个并发任务
@Scheduled(fixedDelay = 10000) // 每10秒轮询一次
fun pollPendingTasks() {
val pendingTasks = backtestTaskRepository.findByStatus("PENDING")
pendingTasks.forEach { task ->
executor.submit {
try {
runBlocking {
executionService.executeBacktest(task)
}
} catch (e: Exception) {
logger.error("回测任务执行失败: ${task.id}", e)
backtestTaskRepository.updateStatus(task.id!!, "FAILED", e.message)
}
}
}
}
}
```
**并发控制**:
- 线程池大小: 5
- 超出并发数的任务保持 `PENDING` 状态,下次轮询继续执行
### 4.2 核心算法
#### 4.2.1 回测算法伪代码
```kotlin
fun executeBacktest(task: BacktestTask) {
// 1. 初始化
var currentBalance = task.initialBalance
val positions = mutableMapOf<String, Position>() // marketId + outcome -> Position
val trades = mutableListOf<BacktestTrade>()
val marketInfoCache = mutableMapOf<String, MarketInfo>() // 缓存市场信息
// 2. 计算回测时间范围
val endTime = System.currentTimeMillis()
val startTime = endTime - (task.backtestDays * 24 * 3600 * 1000)
// 3. 获取Leader历史交易
val leaderTrades = getLeaderHistoricalTrades(task.leaderId, startTime, endTime)
// 4. 按时间顺序回放交易
for (leaderTrade in leaderTrades.sortedBy { it.timestamp }) {
// ✨ 4.1 实时检查并结算已到期的市场
val expiredPositions = positions.filter { (positionKey, position) ->
val marketInfo = marketInfoCache.getOrPut(position.marketId) {
try {
marketService.getMarketInfo(position.marketId)
} catch (e: Exception) {
logger.warn("无法获取市场${position.marketId}信息", e)
null
}
}
// 检查市场是否已到结束时间
marketInfo?.endDate != null && marketInfo.endDate <= leaderTrade.timestamp
}
// 结算已到期的市场
for ((positionKey, position) in expiredPositions) {
val marketInfo = marketInfoCache[position.marketId]!!
val settlementPrice = when {
marketInfo.winner == position.outcome -> BigDecimal.ONE // 胜出方
marketInfo.winner != null -> BigDecimal.ZERO // 失败方
else -> position.avgPrice // 未结算,按成本价保守估计
}
val settlementValue = position.quantity * settlementPrice
val profitLoss = settlementValue - (position.quantity * position.avgPrice)
currentBalance += settlementValue
// 记录结算交易
trades.add(BacktestTrade(
backtestTaskId = task.id!!,
tradeTime = marketInfo.endDate,
marketId = position.marketId,
side = "SETTLEMENT",
outcome = position.outcome,
quantity = position.quantity,
price = settlementPrice,
amount = settlementValue,
fee = BigDecimal.ZERO,
profitLoss = profitLoss,
balanceAfter = currentBalance
))
// 移除已结算的持仓
positions.remove(positionKey)
logger.info("市场到期结算: ${position.marketId}, 时间: ${marketInfo.endDate}, 结算价: $settlementPrice, 盈亏: $profitLoss")
}
// 4.2 检查余额和持仓状态
// ✨ 修正: 只有当"余额不足 且 无持仓"时才停止回测
// 如果有持仓,继续处理(可能有卖出或市场结算释放资金)
if (currentBalance < BigDecimal.ONE && positions.isEmpty()) {
logger.info("余额不足且无持仓,停止回测: $currentBalance")
break
}
// 如果余额不足但有持仓,记录日志但继续处理
if (currentBalance < BigDecimal.ONE && positions.isNotEmpty()) {
logger.info("余额不足 $currentBalance,但还有 ${positions.size} 个持仓,继续处理后续交易(等待卖出或结算)")
}
// 4.3 应用过滤规则
if (!passFilters(task, leaderTrade)) {
continue
}
// 4.4 计算跟单金额
val followAmount = calculateFollowAmount(task, leaderTrade)
if (leaderTrade.side == "BUY") {
// 买入逻辑
val quantity = followAmount / leaderTrade.price
val totalCost = followAmount // 不计算手续费
// ✨ 严格模式: 仅检查当前可用余额,不考虑持仓市值
// 理由: 持仓未卖出前资金不可用,这更符合真实场景
// 注意: 市场到期结算会在步骤4.1中提前释放资金
if (totalCost > currentBalance) {
logger.info("余额不足以执行买入订单: 需要 ${totalCost.toPlainString()}, 可用 ${currentBalance.toPlainString()}")
// 记录持仓价值用于分析(但不计入可用余额)
if (logger.isDebugEnabled) {
val positionValue = positions.values.sumOf { it.quantity * it.avgPrice }
logger.debug("当前持仓市值: ${positionValue.toPlainString()}, 但资金被占用")
}
continue
}
// 更新余额和持仓
currentBalance -= totalCost
val positionKey = "${leaderTrade.marketId}:${leaderTrade.outcome}"
positions[positionKey] = Position(
marketId = leaderTrade.marketId,
outcome = leaderTrade.outcome,
quantity = quantity,
avgPrice = leaderTrade.price,
leaderBuyQuantity = leaderTrade.quantity
)
// 记录交易
trades.add(BacktestTrade(
backtestTaskId = task.id!!,
tradeTime = leaderTrade.timestamp,
marketId = leaderTrade.marketId,
side = "BUY",
outcome = leaderTrade.outcome,
quantity = quantity,
price = leaderTrade.price,
amount = followAmount,
fee = BigDecimal.ZERO, // 不计算手续费
balanceAfter = currentBalance
))
} else { // SELL
if (!task.supportSell) continue
val positionKey = "${leaderTrade.marketId}:${leaderTrade.outcome}"
val position = positions[positionKey] ?: continue
// 计算卖出数量 (按比例)
val sellQuantity = if (task.copyMode == "RATIO") {
// 比例模式: 按Leader卖出比例
position.quantity * (leaderTrade.quantity / position.leaderBuyQuantity)
} else {
// 固定金额模式: 全部卖出
position.quantity
}
val sellAmount = sellQuantity * leaderTrade.price
val netAmount = sellAmount // 不扣除手续费
// 计算盈亏
val cost = sellQuantity * position.avgPrice
val profitLoss = netAmount - cost
// 更新余额和持仓
currentBalance += netAmount
position.quantity -= sellQuantity
if (position.quantity <= BigDecimal.ZERO) {
positions.remove(positionKey)
}
// 记录交易
trades.add(BacktestTrade(
backtestTaskId = task.id!!,
tradeTime = leaderTrade.timestamp,
marketId = leaderTrade.marketId,
side = "SELL",
outcome = leaderTrade.outcome,
quantity = sellQuantity,
price = leaderTrade.price,
amount = sellAmount,
fee = BigDecimal.ZERO, // 不计算手续费
profitLoss = profitLoss,
balanceAfter = currentBalance
))
}
}
// 5. 处理回测结束时仍未到期的持仓 (兜底处理)
for ((positionKey, position) in positions) {
try {
val marketInfo = marketInfoCache.getOrPut(position.marketId) {
marketService.getMarketInfo(position.marketId)
}
// 如果市场已结算但endDate晚于回测结束时间,或市场信息获取失败
val settlementPrice = when {
marketInfo?.winner == position.outcome -> BigDecimal.ONE
marketInfo?.winner != null -> BigDecimal.ZERO
else -> position.avgPrice // 未结算或无法获取,按成本价
}
val settlementValue = position.quantity * settlementPrice
val profitLoss = settlementValue - (position.quantity * position.avgPrice)
currentBalance += settlementValue
trades.add(BacktestTrade(
backtestTaskId = task.id!!,
tradeTime = marketInfo?.endDate ?: endTime,
marketId = position.marketId,
side = "SETTLEMENT",
outcome = position.outcome,
quantity = position.quantity,
price = settlementPrice,
amount = settlementValue,
fee = BigDecimal.ZERO,
profitLoss = profitLoss,
balanceAfter = currentBalance
))
logger.info("回测结束时结算剩余持仓: ${position.marketId}, 结算价: $settlementPrice")
} catch (e: Exception) {
logger.warn("无法获取市场${position.marketId}结算信息,按成本价计算", e)
currentBalance += position.quantity * position.avgPrice
}
}
// 6. 计算最终统计数据
val statistics = calculateStatistics(trades)
// 7. 更新任务状态
task.finalBalance = currentBalance
task.profitAmount = currentBalance - task.initialBalance
task.profitRate = (task.profitAmount!! / task.initialBalance) * BigDecimal(100)
task.status = "COMPLETED"
task.totalTrades = trades.size
// ... 更新其他统计字段
// 8. 保存数据
backtestTaskRepository.save(task)
backtestTradeRepository.saveAll(trades)
}
```
#### 4.2.2 过滤规则复用
直接调用 `CopyTradingFilterService` 的方法:
- `checkPriceFilter()`
- `checkDepthFilter()`
- `checkSpreadFilter()`
- `checkPositionLimits()`
- `checkKeywordFilter()`
- `checkMarketEndDate()`
**适配要点**:
- 回测模式下,持仓数据来自内存 `positions` Map,而非实时API
- 订单簿数据可能不可用 (历史数据),需要容错处理
## 五、前端实现方案
### 5.1 页面组件结构
```
src/pages/
├── BacktestList.tsx # 回测列表页
├── BacktestCreate.tsx # 创建回测页
└── BacktestDetail.tsx # 回测详情页
src/components/Backtest/
├── BacktestTable.tsx # 回测列表表格
├── BacktestForm.tsx # 回测创建表单
├── BacktestChart.tsx # 资金变化图表
├── BacktestTradeTable.tsx # 交易记录表格
└── BacktestStatistics.tsx # 统计数据卡片
```
### 5.2 状态管理
使用 React Context 或 Redux:
```typescript
interface BacktestState {
tasks: BacktestTask[]
currentTask: BacktestTaskDetail | null
trades: BacktestTrade[]
loading: boolean
error: string | null
}
```
### 5.3 API Service
```typescript
// src/services/backtestService.ts
export const backtestService = {
createTask: (data: BacktestCreateRequest) =>
api.post('/api/backtest/tasks', data),
getTaskList: (params: BacktestListParams) =>
api.get('/api/backtest/tasks', { params }),
getTaskDetail: (id: number) =>
api.get(`/api/backtest/tasks/${id}`),
getTrades: (id: number, page: number, size: number) =>
api.get(`/api/backtest/tasks/${id}/trades`, { params: { page, size } }),
deleteTask: (id: number) =>
api.delete(`/api/backtest/tasks/${id}`),
stopTask: (id: number) =>
api.post(`/api/backtest/tasks/${id}/stop`)
}
```
### 5.4 复用现有组件
**跟单配置表单**:
- 直接复用 `CopyTradingForm.tsx` 或相关组件
- 提取配置参数部分为独立组件 `CopyTradingConfigFields.tsx`
- 回测创建页面引入该组件
**好处**:
- 减少重复代码
- 保证参数一致性
- 降低维护成本
### 5.5 图表实现
使用 **ECharts** (项目可能已使用) 或 **Recharts**:
```tsx
import ReactECharts from 'echarts-for-react';
const BacktestChart: React.FC<{ trades: BacktestTrade[] }> = ({ trades }) => {
const option = {
title: { text: '资金变化曲线' },
xAxis: { type: 'time' },
yAxis: { type: 'value', name: '余额 (USDC)' },
series: [{
type: 'line',
data: trades.map(t => [t.tradeTime, t.balanceAfter]),
smooth: true,
itemStyle: { color: '#00b96b' }
}],
tooltip: { trigger: 'axis' }
};
return <ReactECharts option={option} />;
};
```
## 六、技术实现要点
### 6.1 代码复用策略
| 模块 | 复用内容 | 新增内容 |
|-----|---------|---------|
| 后端Entity | `CopyTrading` 参数字段 | `BacktestTask`, `BacktestTrade` |
| 后端Service | `CopyTradingFilterService` 全部方法 | `BacktestService`, `BacktestExecutionService` |
| 后端Repository | JPA通用方法 | 自定义查询方法 |
| 前端组件 | 跟单配置表单组件 | 回测列表、详情、图表组件 |
| 前端Service | API请求封装 | 回测相关API |
### 6.2 性能优化
#### 6.2.1 数据库优化
- 合理使用索引 (见2.3节)
- 分页查询,避免一次加载大量数据
- `BacktestTrade` 使用级联删除 (`ON DELETE CASCADE`)
#### 6.2.2 API优化
- Leader历史交易数据缓存 (Redis, TTL=1h)
- 回测详情页使用懒加载: 先加载任务信息,再加载交易记录
#### 6.2.3 前端优化
- 虚拟滚动: 交易记录表格使用 `react-window``react-virtualized`
- 图表按需渲染: 首次加载最近100条数据,支持分页加载更多
- 防抖: 搜索和筛选操作使用 `debounce`
### 6.3 错误处理
#### 6.3.1 后端异常处理
```kotlin
try {
executeBacktest(task)
} catch (e: Exception) {
logger.error("回测执行失败", e)
task.status = "FAILED"
task.errorMessage = e.message
backtestTaskRepository.save(task)
}
```
#### 6.3.2 前端错误处理
- API调用失败: Toast提示错误信息
- 数据加载失败: 显示错误状态,提供重试按钮
- 表单验证: 实时验证,显示错误提示
### 6.4 数据精度处理
**使用 BigDecimal**:
- 后端: 所有金额计算使用 `BigDecimal`
- 数据库: `DECIMAL(20, 8)` 精度
- 前端: 显示时格式化为2位小数,计算时保持原始精度
## 七、开发流程建议
### Phase 1: 数据库和API (2天)
1. 创建数据表 `backtest_task`, `backtest_trade`
2. 创建 Entity, Repository
3. 实现 `BacktestService` (CRUD操作)
4. 实现 `BacktestController` (API接口)
5. 使用 Postman 测试API
### Phase 2: 回测执行引擎 (3天)
1. 实现 `BacktestDataService` (获取历史数据)
2. 实现 `BacktestExecutionService` (核心算法)
3. 实现 `BacktestPollingService` (定时轮询)
4. 单元测试: 回测算法准确性测试
### Phase 3: 前端页面 (3天)
1. 创建回测列表页 (`BacktestList.tsx`)
2. 创建回测创建页 (`BacktestCreate.tsx`)
3. 创建回测详情页 (`BacktestDetail.tsx`)
4. 集成API,调试交互
### Phase 4: 测试和优化 (2天)
1. 端到端测试
2. 性能测试和优化
3. Bug修复
4. 文档完善
## 八、文档目录建议
根据用户要求,可以将文档拆分为:
1. **BACKTEST_PRD.md** - 产品需求文档 (已完成)
2. **BACKTEST_TECHNICAL_DESIGN.md** - 技术设计文档 (本文档)
3. **BACKTEST_API_SPEC.md** - API接口规范 (可选,从本文档第三节提取)
4. **BACKTEST_DATABASE_SCHEMA.md** - 数据库设计 (可选,从本文档第二节提取)
## 九、风险和注意事项
### 9.1 数据准确性
- **风险**: 历史数据可能不完整或不准确
- **缓解**: 从多个数据源验证,添加数据完整性检查
### 9.2 性能瓶颈
- **风险**: 30天历史数据可能有数千笔交易,执行时间过长
- **缓解**: 异步执行,显示进度条,优化算法
### 9.3 兼容性
- **风险**: 跟单配置参数未来可能变更
- **缓解**: 使用版本化配置,`backtest_task` 表独立存储配置快照
### 9.4 并发控制
- **风险**: 大量回测任务同时执行导致资源耗尽
- **缓解**: 线程池限制并发数,任务队列管理
---
**文档版本**: v1.0
**最后更新**: 2026-01-30
+76
View File
@@ -0,0 +1,76 @@
# 跟单回测功能文档
## 📚 文档清单
本目录包含跟单回测功能的完整设计文档:
### 核心文档
1. **[BACKTEST_PRD.md](./BACKTEST_PRD.md)** - 产品需求文档
- 功能概述与用户故事
- UI/UX设计详细说明
- 业务规则与数据要求
2. **[BACKTEST_TECHNICAL_DESIGN.md](./BACKTEST_TECHNICAL_DESIGN.md)** - 技术设计文档
- 数据库表结构设计
- RESTful API接口规范
- 后端服务架构
- 前端组件设计
- 回测算法实现
3. **[BACKTEST_REVIEW_CHECKLIST.md](./BACKTEST_REVIEW_CHECKLIST.md)** - 设计审查清单
- 设计完整性检查
- 边缘场景处理
- 风险评估与缓解
## 🎯 核心特性
- ✅ 完全复用现有跟单配置参数
- ✅ 实时市场结算(按 `endDate` 检查)
- ✅ 严格余额检查(避免过于乐观的回测)
- ✅ 支持按Leader筛选、按收益排序
- ✅ 详细的交易记录和资金曲线图
## 📖 阅读建议
**产品经理**: 先阅读 PRD,再查看审查清单中的关键决策点
**技术负责人**: 先阅读技术设计文档,再查看审查清单评估风险
**开发工程师**: 按顺序阅读所有文档,重点关注技术设计的实现细节
## 🔄 文档版本
- **创建日期**: 2026-01-30
- **最后更新**: 2026-01-30
- **当前版本**: v1.0
## 📝 关键设计决策
### 1. 市场结算机制
- 采用**实时检查**方式:每笔交易前检查市场 `endDate`
- 到期即结算,资金立即释放可用于后续交易
### 2. 余额检查策略
- 采用**严格模式**:仅使用 `currentBalance`,不计入持仓市值
- 停止条件:余额 < $1 **且** 无任何持仓
### 3. 数据源选择
- 优先使用系统记录的 `ProcessedTrade`
- 不足时调用 Polymarket API 补充历史数据
### 4. 代码复用策略
- 后端:完全复用 `CopyTradingFilterService` 的所有过滤逻辑
- 前端:复用跟单配置表单组件
- 数据库:配置字段与 `CopyTrading` 表保持一致
## 🚀 下一步
完成文档审查后,可以:
1. 创建 `implementation_plan.md` 详细规划实施步骤
2. 开始开发(数据库表 → API → 前端页面)
3. 单元测试和集成测试
---
**文档位置**: `/Users/wrbug/polyhermes/docs/zh/backtest/`