## 功能概述 实现完整的回测功能,支持基于历史数据模拟跟单策略的执行效果。 ## 后端实现 - 数据库:新增 backtest_task 和 backtest_trade 表(V27迁移脚本) - 实体类:BacktestTask、BacktestTrade - Repository:BacktestTaskRepository、BacktestTradeRepository - Service: - BacktestService:回测任务管理(CRUD) - BacktestDataService:从 Polymarket Data API 获取历史交易数据 - BacktestExecutionService:回测算法核心实现 - BacktestPollingService:定时轮询执行回测任务 - Controller:BacktestController(6个API接口) - DTO:BacktestDto、TradeData - 错误码:新增回测相关错误码和国际化消息 ## 前端实现 - 页面组件: - BacktestList:回测任务列表 - BacktestCreate:创建回测任务 - BacktestDetail:回测详情(含图表) - BacktestChart:资金曲线图表(使用 ECharts) - 类型定义:backtest.ts - API 服务:集成所有回测接口 - 国际化:支持中英文 ## 核心特性 - 回测天数限制:1-15 天 - 数据获取:直接从 Polymarket Data API 获取历史交易(不使用缓存表) - 任务执行:同一时刻只执行一个任务,按创建时间顺序执行最早创建的任务 - 回测算法:完整实现市场结算、卖出匹配、价格容忍度、每日订单限制等规则 - 实时进度:支持任务进度更新和实时轮询 ## 文档更新 - BACKTEST_PRD.md:产品需求文档 - BACKTEST_TECHNICAL_DESIGN.md:技术设计文档 - BACKTEST_REVIEW_CHECKLIST.md:设计评审检查清单 ## 其他修改 - 移除 max_position_count 配置(V26迁移脚本) - 移除 BacktestSyncService(不再需要实时同步) - 修复前后端编译错误
20 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 历史数据获取 ⭐ (已修正)
Note
问题: 现有
ProcessedTrade表字段有限,无法满足回测需求。解决方案: 创建独立的
backtest_historical_trades表
- ✅ 存储完整的交易信息(marketId, price, quantity, outcomeIndex 等)
- ✅ 支持实时数据同步(跟单时同时写入)
- ✅ 支持通过 API 补充历史数据
- ✅ 不影响现有跟单功能
1.3.2 卖出匹配逻辑 ⭐ (已修正)
Note
改进: 使用
outcomeIndex支持多元市场实现方案:
- 持仓键:
marketId + outcomeIndex(支持多元市场)- 比例模式: 按 Leader 卖出比例计算
- 固定金额模式: 全部卖出
- 参考
CopyOrderTracking的逻辑
伪代码:
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 的成交价,不进行滑点调整
1.3.2 价格滑点模拟
Note
关键问题: 是否需要模拟价格滑点?
当前设计: 不模拟价格滑点,直接使用Leader的成交价
- 优点: 简化逻辑,回测速度快
- 缺点: 可能高估收益(实际跟单可能有滑点)
可选方案: 增加可配置的滑点参数
- 买入时: 价格 × (1 + 滑点%)
- 卖出时: 价格 × (1 - 滑点%)
- 增加可选的滑点模拟参数 (例如: ±0.5%)
- 在PRD中补充此配置项
1.3.3 手续费计算 ✅ (已移除)
Note
用户决策: 回测不计算手续费
理由:
- 简化计算逻辑
- 避免精度问题
- 降低复杂度
实现:
- 所有交易的
fee字段均为0- 买入成本 = 数量 × 价格
- 卖出收入 = 数量 × 价格
- 结算收入 = 数量 × 结算价
1.3.4 市场结算处理 ⭐ (已优化)
Note
问题: 市场结束时,未平仓位如何自动结算?
优化方案 (采纳用户建议):
- ✅ 实时检查: 在回测循环中,每处理一笔Leader交易前,检查所有持仓的市场
endDate- ✅ 到期即结算: 如果
marketEndDate <= currentTradeTime,立即结算该持仓- ✅ 资金可用: 结算后的资金立即计入余额,可以用于后续交易
- ✅ 兜底处理: 回测结束时,结算所有剩余未到期持仓
结算价格判断 (通过市场价格):
- 价格 >= 0.95: 胜出 (按 1.0 结算)
- 价格 <= 0.05: 失败 (按 0.0 结算)
- 其他情况: 按成本价保守估计
实现要点:
// 在交易循环中实时检查市场到期 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) { 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 currentBalance += settlementValue positions.remove(positionKey) } // 2. 处理当前Leader交易 // ... }优势:
- 更符合真实场景 (市场结束时自动返还资金)
- 提高资金利用率 (结算资金可参与后续交易)
- 更准确的收益计算
- 通过市场价格判断,无需依赖可能不存在的
winner字段
1.3.5 余额不足与持仓处理 ⚠️ (边缘场景) - 已修正
Warning
关键问题: 当余额不足但有未卖出持仓时,如何处理?
场景示例:
- 初始余额: $1000
- 已买入持仓市值: $800(未卖出)
- 当前余额: $200
- 新买入订单需要: $300
- 问题: 是否允许买入?虽然持仓市值足够,但资金被占用
推荐方案: 严格模式
- ❌ 不允许买入(余额不足)
- ✅ 仅使用
currentBalance判断- ✅ 不计入持仓市值(因为持仓未实现)
- ✅ 理由: 更真实,避免过于乐观的回测结果
替代方案: 宽松模式
- ✅ 计算"虚拟可用资金" =
currentBalance + 持仓估值- ⚠️ 允许"透支"买入
- ❌ 风险: 可能产生不切实际的收益
实际影响:
- 严格模式下,资金周转率是限制因素
- 鼓励快进快出的策略
- 长期持仓策略会因资金占用而错过后续机会
已在文档中采用: 严格模式
1.3.6 每日订单数限制 ✅ (已补充)
Note
问题: 文档提到了
maxDailyOrders参数,但未在算法中实现解决方案: 在回测循环中添加每日订单数统计
实现:
// 统计当前交易时间当天已有的订单数 val dailyOrderCount = trades.count { isSameDay(it.tradeTime, leaderTrade.timestamp) } if (dailyOrderCount >= task.maxDailyOrders) { logger.info("已达到每日最大订单数限制: $dailyOrderCount / ${task.maxDailyOrders}") continue }优势:
- 符合实际跟单的风险控制逻辑
- 避免回测结果过于激进
1.3.7 价格容忍度检查 ✅ (已补充)
Note
问题: 文档提到了
priceTolerance参数,但未在算法中实现解决方案: 在执行交易前检查当前市场价格是否在容忍范围内
实现:
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 回测停止条件 ✅ (已修正)
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 新增回测历史交易表
问题: 现有 ProcessedTrade 表字段有限,无法满足回测需求。
解决方案: 创建独立的 backtest_historical_trades 表,存储完整的 Leader 历史交易数据。
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 其他字段建议
backtest_task 表
建议新增以下字段:
-- 用于计算平均持仓时间
avg_holding_time BIGINT DEFAULT NULL COMMENT '平均持仓时间(毫秒)',
-- 用于记录回测使用的数据源
data_source VARCHAR(50) DEFAULT 'MIXED' COMMENT '数据源: INTERNAL/API/MIXED',
-- 用于记录回测执行的详细日志
execution_log TEXT DEFAULT NULL COMMENT '执行日志(JSON格式)'
2.4 索引优化
建议添加复合索引:
-- 用于按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 规范修正
问题: 文档中使用 GET/DELETE 方法,违反项目统一使用 POST 的规范。
修正方案:
# ❌ 错误(使用 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
建议新增以下API:
3.2.1 查询回测进度 (实时更新)
POST /api/backtest/tasks/progress
Request Body:
{
"id": 12345
}
Response:
{
"success": true,
"data": {
"progress": 65,
"currentBalance": "1150.00",
"totalTrades": 30,
"status": "RUNNING"
}
}
3.2.2 批量删除回测任务
POST /api/backtest/tasks/batch-delete
Request Body:
{
"taskIds": [12345, 12346, 12347]
}
3.2.3 导出回测报告
POST /api/backtest/tasks/export
Request Body:
{
"id": 12345,
"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
状态: 待用户确认