回测系统设计
生产级量化回测系统:事件驱动引擎、高保真撮合仿真、滑点与手续费建模、Walk-Forward 滚动优化、过拟合检测与实盘一致性验证。
1. 回测系统总览
什么是回测?一句话解释
一个具体例子
假设你有一个交易想法:「BTC 的 5 日均线上穿 20 日均线时买入,下穿时卖出」。你怎么知道这个想法靠不靠谱?
❌ 不靠谱的做法
直接拿真钱交易。如果亏了才发现策略有问题,已经来不及了。而且你不知道亏是因为策略差,还是因为运气不好。
✅ 靠谱的做法
用过去 2 年的 BTC 价格数据,模拟这个策略的表现。如果在过去 2 年里这个策略能赚钱,那它在未来大概率也有一定效果。
回测的核心流程
让我们用一个简化的例子走一遍完整流程。假设初始资金 10,000 USDT,策略是 5/20 日均线交叉:
| 日期 | BTC 价格 | 5日均线 | 20日均线 | 信号 | 操作 | 资金 |
|---|---|---|---|---|---|---|
| Day 20 | $100 | $98 | $95 | 金叉 | 买入 99 BTC | $10,000 |
| Day 45 | $120 | $118 | $110 | — | 持有中 | $11,880 |
| Day 60 | $105 | $107 | $112 | 死叉 | 卖出 99 BTC | $10,395 |
| 结果 | 盈利 | +$395 (+3.95%) | ||||
这只是一笔交易。真正的回测会用几百甚至几千笔历史交易来检验策略。交易次数越多,结果越可靠。
回测系统的整体架构
2. 事件驱动引擎架构
回测引擎的核心思路是「事件驱动」—— 就像一条流水线:数据进来 → 策略判断 → 下单 → 成交。每一步都是一个独立的事件,按顺序处理。
用一笔交易走完整个流程
为什么要用事件驱动?
松耦合
策略只管发信号,不管怎么成交。撮合只管成交,不管策略逻辑。各模块独立工作,互不干扰。想换一个策略?只需替换策略模块。
回测/实盘统一
回测时数据源是历史文件,实盘时数据源是 WebSocket。但策略代码完全不变!只需要切换数据源和执行层。
严格的时间顺序
事件按时间顺序逐个处理。策略只能看到「当前时间之前」的数据,绝对不能偷看未来的价格。这是回测可信的基础。
100% 可复现
相同的数据 + 相同的参数 = 完全相同的结果。每次运行结果一致,方便调试和验证。
引擎核心伪代码(极简版)
// 回测引擎核心循环 — 极简版
while 还有历史数据:
K线 = 获取下一根K线() // ① 数据推送
信号 = 策略.判断(K线) // ② 策略看到K线,决定买/卖/观望
if 信号 == "买入":
订单 = 组合.生成买单(信号) // ③ 风控检查后生成订单
成交 = 撮合.模拟成交(订单) // ④ 模拟真实成交(含滑点、手续费)
组合.更新(成交) // ⑤ 更新仓位和资金
输出 绩效报告() // 统计收益、回撤、胜率等3. 历史数据管理
回测的质量取决于数据的质量。垃圾数据进去,垃圾结果出来。让我们看看回测需要什么样的数据。
数据长什么样?
最常用的是 K 线数据(也叫蜡烛图数据),每根 K 线记录了一段时间内的开盘价、最高价、最低价、收盘价和成交量:
// 一根 BTC/USDT 1小时 K线的数据
{
时间: "2024-01-15 14:00",
开盘价(Open): 42,850.00, // 这1小时的第一笔成交价
最高价(High): 43,120.50, // 这1小时内的最高成交价
最低价(Low): 42,780.00, // 这1小时内的最低成交价
收盘价(Close): 43,050.25, // 这1小时的最后一笔成交价
成交量(Volume): 1,234.56 // 这1小时总共成交了多少 BTC
}三个层级的数据
数据质量检查——别被脏数据坑了
历史数据经常有问题:缺失几根K线、价格异常跳动、成交量为零等。如果不检查,回测结果会不准确。常见的检查项:
| 检查项 | 什么意思 | 举例 |
|---|---|---|
| 时间连续性 | 相邻K线之间不能有缺失 | 1小时K线之间应该间隔恰好1小时 |
| OHLC 一致性 | 最高价必须≥开盘和收盘 | 如果 High < Close,这根K线有问题 |
| 价格合理性 | 单根K线涨跌不应超过 50% | BTC 从 40000 突然变成 100?数据错误 |
| 成交量异常 | 价格变了但成交量为 0 | 不可能的——有成交才有价格变化 |
4. 撮合仿真与滑点模型
理想 vs 现实:为什么回测收益会虚高?
具体数字对比
假设你想买入价值 10,000 USDT 的 BTC,当前价格 $100:
| 项目 | ❌ 理想化回测 | ✅ 现实回测 | 🔥 实盘 |
|---|---|---|---|
| 成交价 | $100.00 | $100.05 | $100.03 ~ $100.08 |
| 滑点 | 0 | 5 bps ($0.05) | 3~8 bps |
| 手续费 | 0 | $10 (0.1%) | $10 (0.1%) |
| 成交延迟 | 立即 | 下一根K线开盘 | 50ms ~ 500ms |
| 实际花费 | $10,000 | $10,015 | $10,011 ~ $10,018 |
一笔交易差 $15 好像不多?但如果一年交易 500 次,就是 $7,500!对 $10,000 本金来说就是 75% 的差距。所以撮合仿真的精度直接决定回测结果是否可信。
三种滑点模型对比
模型 1:固定滑点
每笔交易固定加 5 个基点(0.05%)。最简单,适合初步估算。缺点:大单和小单滑点一样,不够真实。
滑点 = 价格 × 5 / 10000 例: $100 × 5 / 10000 = $0.05
模型 2:成交量自适应
你的订单占当根K线成交量的比例越大,滑点越大。更真实——大单确实比小单滑点大。
参与率 = 你的量 / K线成交量 滑点 = 基础滑点 × (1 + 参与率 × 50) 例: 占成交量10% → 滑点放大6倍
5. 策略接口设计
策略代码应该尽可能简单——只关注「什么时候买、什么时候卖」,不需要关心数据怎么来的、订单怎么撮合的。下面是一个最简单的均线交叉策略:
// 均线交叉策略 — 最简实现
class 均线交叉策略:
初始化():
快线周期 = 5
慢线周期 = 20
每根K线(K线):
收盘价列表 = 获取最近(慢线周期 + 1)根收盘价
快线 = 最近5根的平均值
慢线 = 最近20根的平均值
上一根的快线 = ...
上一根的慢线 = ...
// 金叉:快线从下方穿越慢线 → 买入
if 上一根快线 ≤ 上一根慢线 and 快线 > 慢线:
买入(数量 = 95%资金 / 当前价格)
// 死叉:快线从上方穿越慢线 → 卖出
if 上一根快线 ≥ 上一根慢线 and 快线 < 慢线:
卖出(全部持仓)策略的生命周期
| 回调函数 | 什么时候调用 | 你要做什么 |
|---|---|---|
onInit() | 回测开始前 | 设置参数(均线周期等) |
onBar(candle) | 每根新K线到来时 | 核心逻辑——判断买/卖/持有 |
onFill(fill) | 订单成交后 | 记录日志、更新状态 |
onStop() | 回测结束时 | 平掉所有持仓 |
6. 回测引擎模拟器
下面是一个可以实际运行的回测模拟器。选择不同的策略和市场参数,观察资金曲线和核心指标的变化。试试调高波动率看看回撤会怎样!
7. 绩效评估体系
回测跑完了,怎么判断策略好不好?不能只看「赚了多少钱」——还要看风险、稳定性、胜率等多个维度。下面用大白话解释最重要的几个指标:
Sharpe Ratio (夏普比率)
每承担 1 份风险,能赚多少超额收益。就像考试成绩除以复习时间——不仅看分数,还看效率。
| < 0.5 | 差 |
| 0.5 ~ 1.5 | 可接受 |
| > 2.0 | 优秀 |
最大回撤 (Max Drawdown)
从最高点到最低点的最大跌幅。比收益率更重要!回测赚 100% 但中间回撤 60% —— 你能扛住看着账户腰斩吗?
| < 10% | 优秀 |
| 10% ~ 30% | 可接受 |
| > 30% | 危险 |
胜率 (Win Rate)
赚钱的交易占总交易的比例。但胜率高不一定赚钱!如果赢 9 次每次赚 $10,输 1 次亏 $100,胜率 90% 照样亏。要配合盈亏比看。
| 趋势策略 | 胜率 30-40%, 盈亏比 3:1 |
| 均值回归 | 胜率 55-65%, 盈亏比 1:1 |
| 网格交易 | 胜率 70-80%, 盈亏比 0.5:1 |
Profit Factor (利润因子)
总盈利 / 总亏损。大于 1 说明赚的比亏的多。最直观的指标之一。
| < 1.0 | 亏钱 |
| 1.0 ~ 1.5 | 勉强 |
| > 2.0 | 优秀 |
一张表看完所有指标的参考值
| 指标 | 差 | 可接受 | 优秀 |
|---|---|---|---|
| Sharpe Ratio | < 0.5 | 0.5 ~ 1.5 | > 2.0 |
| Sortino Ratio | < 0.5 | 0.5 ~ 2.0 | > 3.0 |
| 最大回撤 | > 30% | 10% ~ 30% | < 10% |
| 胜率 | < 40% | 40% ~ 55% | > 55% |
| 盈亏比 | < 1.0 | 1.0 ~ 2.0 | > 2.0 |
| Profit Factor | < 1.0 | 1.0 ~ 1.5 | > 2.0 |
| SQN | < 1.6 | 1.6 ~ 2.5 | > 3.0 |
8. Walk-Forward 优化
为什么需要 Walk-Forward?一个故事
Walk-Forward 怎么做?图解
把历史数据分成多个窗口。每个窗口分两段:训练段(找最优参数)和验证段(用训练段的最优参数测试)。如果验证段也赚钱,说明策略有真实的预测能力。
具体步骤
| 步骤 | 做什么 | 举例 |
|---|---|---|
| 1 | 把数据分成 N 个滚动窗口 | 12个月数据 → 5个窗口 |
| 2 | 每个窗口:70% 训练,30% 验证 | 窗口1: 1-7月训练, 8-9月验证 |
| 3 | 在训练段扫描所有参数组合 | 均线周期从 3 到 50 全部试一遍 |
| 4 | 选出训练段 Sharpe 最高的参数 | 快线=5, 慢线=20 最好 |
| 5 | 用这个参数在验证段上测试 | 8-9月用快5慢20跑一遍 |
| 6 | 拼接所有窗口的验证结果 | 5个窗口的OOS收益平均 = 6.3% |
如何判断结果好不好?
衰减率 < 50% ✅
训练段收益 20%,验证段收益 12%,衰减率 = (20-12)/20 = 40%。低于 50% 说明过拟合可控。如果衰减率 > 70%,说明大部分收益来自过拟合。
参数稳定性 ✅
如果 5 个窗口找到的最优参数分别是 5、6、5、7、5,说明策略稳健。如果是 3、20、8、50、12,说明策略对参数太敏感,不可靠。
Walk-Forward 模拟器
自己动手试试!调整信号强度和噪声水平,观察训练集和验证集收益的差距。如果差距很大,说明策略可能过拟合了:
9. 过拟合防范
什么是过拟合?
过拟合的四大元凶
参数太多
策略有 10 个参数?每增加一个参数,过拟合风险指数级增长。好的策略参数 ≤ 5 个。如果需要 > 10 个参数才能盈利,大概率是在拟合噪声。
数据窥探
你在同一份数据上反复调参、反复回测。每次回测都「偷看」了答案,所以最终结果必然很好。但你其实只是在拟合这份特定的数据。
选择偏差
你测试了 100 个策略变种,只展示最好的那个。按概率,100 个随机策略中总有几个恰好在这段时间表现不错——但这是运气,不是能力。
无法解释
好的策略应该有清晰的逻辑(趋势跟踪、均值回归、套利)。如果你无法解释为什么你的策略能赚钱,它大概率是数据挖掘的产物。
如何防范过拟合?检查清单
| 检查项 | 达标标准 | 你的策略? |
|---|---|---|
| 参数数量 | ≤ 5 个 | 越少越好 |
| 交易次数 | > 100 | 太少不可信 |
| Walk-Forward 衰减 | < 50% | > 70% = 过拟合 |
| 多市场验证 | ≥ 3 个市场盈利 | BTC/ETH/SOL 都测 |
| 逻辑可解释 | 能用一句话解释 | 「趋势跟踪」✅ 「周三满月」❌ |
| 衰减预算 | 预留 30-50% | 回测 Sharpe 3.0 → 实盘约 1.5-2.0 |
10. 回测 vs 实盘对比
策略通过了回测验证,接下来怎么上线?不是直接拿真钱冲,而是分阶段验证:
三个阶段详细对比
| 对比项 | 🔬 回测 | 📝 模拟盘 | 💰 实盘 |
|---|---|---|---|
| 数据 | 历史数据 | 实时行情 | 实时行情 |
| 成交 | 模拟撮合 | 虚拟撮合 | 真实成交 |
| 滑点 | 模型估算 | 更接近真实 | 真实滑点 |
| 心理因素 | 无 | 低 | 高(恐惧贪婪) |
| 可信度 | 参考 | 较高 | 最终标准 |
| 建议时长 | — | 2-4 周 | 先小仓 2 周 |
上线流程 — 四个阶段
Phase 1: 研究
回测达标:Sharpe > 2.0,最大回撤 < 15%,交易次数 > 100。Walk-Forward 衰减 < 50%。多市场验证通过。
Phase 2: 模拟盘 (2-4周)
用真实行情 + 虚拟撮合。对比回测:PnL 偏差 < 20%。验证信号一致性和系统稳定性。通过标准:模拟盘 Sharpe / 回测 Sharpe > 0.6。
Phase 3: 小资金 (2-4周)
用总资金的 5-10%。对比模拟盘:成交价差 < 5bps。实时监控 PnL 和滑点。通过标准:实盘与模拟盘方向一致。
Phase 4: 全量上线
逐步加仓到目标仓位。每日对比回测预期。自动熔断:日亏 > 2% 或回撤 > 10% 暂停。月度复盘:检查策略衰减。