From f1f809f54bfb3735fcc635698b3a4ee3b8d90f83 Mon Sep 17 00:00:00 2001 From: WrBug Date: Sat, 31 Jan 2026 00:26:00 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9E=E8=B7=9F=E5=8D=95?= =?UTF-8?q?=E5=9B=9E=E6=B5=8B=E5=8A=9F=E8=83=BD=E8=AE=BE=E8=AE=A1=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增产品需求文档 (BACKTEST_PRD.md) - 新增技术设计文档 (BACKTEST_TECHNICAL_DESIGN.md) - 新增设计审查清单 (BACKTEST_REVIEW_CHECKLIST.md) - 新增文档总览 (README.md) 关键设计: - 实时市场结算机制(按endDate检查) - 严格余额检查(不计入未实现持仓) - 停止条件优化(余额不足且无持仓时停止) - 不计算手续费(简化逻辑) --- docs/zh/backtest/BACKTEST_PRD.md | 324 ++++++ docs/zh/backtest/BACKTEST_REVIEW_CHECKLIST.md | 481 +++++++++ docs/zh/backtest/BACKTEST_TECHNICAL_DESIGN.md | 967 ++++++++++++++++++ docs/zh/backtest/README.md | 76 ++ 4 files changed, 1848 insertions(+) create mode 100644 docs/zh/backtest/BACKTEST_PRD.md create mode 100644 docs/zh/backtest/BACKTEST_REVIEW_CHECKLIST.md create mode 100644 docs/zh/backtest/BACKTEST_TECHNICAL_DESIGN.md create mode 100644 docs/zh/backtest/README.md diff --git a/docs/zh/backtest/BACKTEST_PRD.md b/docs/zh/backtest/BACKTEST_PRD.md new file mode 100644 index 0000000..905afff --- /dev/null +++ b/docs/zh/backtest/BACKTEST_PRD.md @@ -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` diff --git a/docs/zh/backtest/BACKTEST_REVIEW_CHECKLIST.md b/docs/zh/backtest/BACKTEST_REVIEW_CHECKLIST.md new file mode 100644 index 0000000..aeedf0d --- /dev/null +++ b/docs/zh/backtest/BACKTEST_REVIEW_CHECKLIST.md @@ -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 +**状态**: 待用户确认 diff --git a/docs/zh/backtest/BACKTEST_TECHNICAL_DESIGN.md b/docs/zh/backtest/BACKTEST_TECHNICAL_DESIGN.md new file mode 100644 index 0000000..25abe8a --- /dev/null +++ b/docs/zh/backtest/BACKTEST_TECHNICAL_DESIGN.md @@ -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 + + // 查询回测任务列表 + fun getBacktestTaskList(request: BacktestListRequest): Result + + // 查询回测任务详情 + fun getBacktestTaskDetail(taskId: Long): Result + + // 删除回测任务 + fun deleteBacktestTask(taskId: Long): Result + + // 停止回测任务 + fun stopBacktestTask(taskId: Long): Result + + // 查询回测交易记录 + fun getBacktestTrades(taskId: Long, page: Int, size: Int): Result +} +``` + +**实现要点**: +- 复用 `CopyTradingFilterService` 的参数验证逻辑 +- 使用 `@Transactional` 保证数据一致性 +- 返回值使用 `Result` 统一错误处理 + +#### 4.1.2 BacktestExecutionService + +**职责**: 执行回测任务 + +**核心方法**: +```kotlin +interface BacktestExecutionService { + // 执行回测任务 + suspend fun executeBacktest(task: BacktestTask) + + // 获取Leader历史交易 + suspend fun getLeaderHistoricalTrades( + leaderId: Long, + startTime: Long, + endTime: Long + ): List + + // 模拟交易执行 + suspend fun simulateTrade( + task: BacktestTask, + trade: HistoricalTrade, + currentBalance: BigDecimal, + positions: MutableMap + ): TradeResult + + // 计算收益统计 + fun calculateStatistics(trades: List): 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() // marketId + outcome -> Position + val trades = mutableListOf() + val marketInfoCache = mutableMapOf() // 缓存市场信息 + + // 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 ; +}; +``` + +## 六、技术实现要点 + +### 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 diff --git a/docs/zh/backtest/README.md b/docs/zh/backtest/README.md new file mode 100644 index 0000000..030184f --- /dev/null +++ b/docs/zh/backtest/README.md @@ -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/`