- 新增产品需求文档 (BACKTEST_PRD.md) - 新增技术设计文档 (BACKTEST_TECHNICAL_DESIGN.md) - 新增设计审查清单 (BACKTEST_REVIEW_CHECKLIST.md) - 新增文档总览 (README.md) 关键设计: - 实时市场结算机制(按endDate检查) - 严格余额检查(不计入未实现持仓) - 停止条件优化(余额不足且无持仓时停止) - 不计算手续费(简化逻辑)
14 KiB
回测功能设计审查清单
一、设计审查要点
1.1 产品需求完整性 ✅
已覆盖的核心功能:
- ✅ 回测任务的创建、查询、删除
- ✅ 按Leader筛选和排序功能
- ✅ 回测配置参数复用现有跟单配置
- ✅ 回测详情展示 (交易记录、资金曲线图、统计数据)
- ✅ 资金不足时自动停止机制
- ✅ 回测天数限制 (1-30天)
潜在遗漏点:
Warning
需要确认的问题:
- 回测结果的可见性: 是否需要支持多用户? 当前设计未涉及权限控制
- 回测任务的生命周期管理: 是否需要自动清理过期的回测记录?
- 回测进度的实时展示: 前端如何获取运行中任务的进度? (考虑WebSocket或轮询)
1.2 技术设计合理性 ✅
优点:
- ✅ 数据库设计规范,索引合理
- ✅ API设计符合RESTful规范
- ✅ 复用现有的
CopyTradingFilterService,减少代码冗余 - ✅ 使用 BigDecimal 保证计算精度
- ✅ 异步执行回测任务,不阻塞主线程
可能的改进点:
Note
建议优化的地方:
- 历史数据获取: 当前设计依赖Polymarket API,需要考虑API限流和数据缺失的情况
- 缓存策略: 建议对Leader历史交易数据使用分层缓存 (内存 + Redis)
- 回测结果的序列化: 考虑将详细交易记录存储为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,立即结算该持仓- ✅ 资金可用: 结算后的资金立即计入余额,可以用于后续交易
- ✅ 兜底处理: 回测结束时,结算所有剩余未到期持仓
实现要点:
// 在交易循环中实时检查市场到期 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
修正: 基于用户反馈,修正了停止逻辑
错误设计:
if (currentBalance < $1) { break // ❌ 直接停止,忽略持仓 }正确设计:
// 只有"余额不足 且 无持仓"时才停止 if (currentBalance < $1 && positions.isEmpty()) { break // ✅ 确保无持仓时才停止 } // 有持仓时继续处理(等待卖出或结算) if (currentBalance < $1 && positions.isNotEmpty()) { // 继续处理,跳过买入,但执行卖出和结算 }理由:
- 持仓存在意味着可能有后续卖出或市场结算
- 这些操作会释放资金
- 过早停止会导致资金无法回收,回测不准确
1.4 性能和可扩展性 ✅
已考虑的优化:
- ✅ 异步执行,线程池限制并发
- ✅ 分页查询
- ✅ 数据库索引优化
- ✅ 前端虚拟滚动
需要进一步考虑:
Tip
性能优化建议:
- 批量插入交易记录: 使用
saveAll()而非逐条save()- 进度更新频率: 避免每笔交易都更新数据库,改为每100笔或每10秒更新一次
- 历史数据预加载: 在任务开始前一次性加载所有历史交易,避免多次API调用
二、数据库设计补充
2.1 缺失的字段建议
backtest_task 表
建议新增以下字段:
-- 用于计算平均持仓时间
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 索引优化
建议添加复合索引:
-- 用于按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:
{
"success": true,
"data": {
"progress": 65,
"currentBalance": "1150.00",
"totalTrades": 30,
"status": "RUNNING"
}
}
3.1.2 批量删除回测任务
DELETE /api/backtest/tasks
Request Body:
{
"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 状态轮询
对于运行中的回测任务,前端需要定时轮询进度:
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 图表数据压缩
当交易记录过多时,图表数据需要压缩:
// 将数据按时间聚合为最多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:
{
"backtest": {
"title": "回测管理",
"createTask": "新增回测",
"taskName": "回测名称",
"leader": "Leader",
"initialBalance": "初始金额",
"backtestDays": "回测天数",
"profitAmount": "收益额",
"profitRate": "收益率",
"status": {
"pending": "待执行",
"running": "运行中",
"completed": "已完成",
"stopped": "已停止",
"failed": "失败"
}
}
}
五、测试计划补充
5.1 单元测试
需要测试的核心方法:
BacktestExecutionService.executeBacktest()- 回测算法准确性BacktestExecutionService.calculateStatistics()- 统计数据计算BacktestDataService.getLeaderHistoricalTrades()- 历史数据获取
测试用例示例:
@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 集成测试
测试场景:
- 端到端测试: 创建任务 → 执行回测 → 查询结果
- 异常场景: 历史数据为空、API调用失败
- 边界条件: 余额刚好为0、单笔交易耗尽余额
5.3 性能测试
测试指标:
- 30天历史数据 (假设1000笔交易) 的回测执行时间 < 5分钟
- 并发5个回测任务时的系统资源占用
- 查询包含10000笔交易的回测详情页面加载时间 < 2秒
六、风险评估和缓解方案
6.1 数据准确性风险
风险: 历史数据不完整或API返回数据有误
缓解方案:
- 数据验证: 检查返回数据的完整性 (是否有时间断层)
- 数据对比: 使用多个数据源交叉验证
- 错误标记: 回测结果标注数据质量等级
6.2 计算精度风险
风险: BigDecimal计算中的舍入误差累积
缓解方案:
- 统一舍入模式: 使用
RoundingMode.HALF_UP - 精度测试: 编写专门的精度测试用例
- 误差补偿: 最终余额与理论值的误差 < 0.01 USDC
6.3 性能风险
风险: 大量回测任务导致系统负载过高
缓解方案:
- 任务队列: 使用异步任务队列 (可选: Redis Queue 或 RabbitMQ)
- 资源限流: 限制单用户最多创建10个待执行任务
- 自动清理: 定期清理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,避免数据库膨胀
八、文档总结
已完成的文档
- ✅ BACKTEST_PRD.md - 产品需求文档
- ✅ BACKTEST_TECHNICAL_DESIGN.md - 技术设计文档
- ✅ BACKTEST_REVIEW_CHECKLIST.md - 设计审查清单 (本文档)
建议补充的文档 (可选)
- BACKTEST_API_SPEC.md - API接口规范 (从技术设计文档提取)
- BACKTEST_DATABASE_MIGRATION.md - 数据库迁移脚本
- BACKTEST_TEST_PLAN.md - 详细测试计划
下一步行动
- 用户Review: 请用户审查以上文档,确认关键设计点
- 补充遗漏: 根据用户反馈补充缺失部分
- 进入执行: 用户确认后开始实施开发
审查日期: 2026-01-30
审查人: AI Assistant
状态: 待用户确认