12、回测系统搭建:回测框架设计、历史数据回放、策略参数优化、绩效评估指标
做市交易这行,有个残酷的现实:你的策略在实盘里能活多久,取决于它在历史数据里死过多少次。
我见过太多人,策略写出来就急着上实盘。结果呢?三天亏掉一个月的利润。说白了,就是没做回测,或者回测做得太糙。
这一章,我们就来聊聊怎么搭一个靠谱的回测系统。不是那种花里胡哨的,而是能真正帮你发现问题的。
核心观点:回测不是为了证明策略赚钱,而是为了证明策略在哪些情况下会亏钱。搞清楚这个,你才算入门了。
回测框架设计:别想着一步到位
我个人习惯,回测框架分三层:
- 数据层:负责加载、清洗、对齐历史数据
- 引擎层:负责模拟撮合、订单管理、资金计算
- 策略层:你的做市逻辑,比如报价更新、库存管理
为什么要分层?因为你在调试策略时,最怕的是数据出问题,你却以为是策略写错了。分层之后,每一层可以独立测试。
举个例子,我早期做的一个回测框架,数据层和引擎层耦合在一起。结果有一次,数据里有个时间戳跳变,引擎直接崩溃了。我花了三天才定位到问题——原来是数据源的一个bug。从那以后,我坚持数据层必须独立,并且要有数据校验模块。
小技巧:回测框架的引擎层,不要追求完全模拟交易所。你只需要模拟「订单簿变化」和「成交逻辑」就够了。太复杂的引擎,反而容易引入bug。
历史数据回放:快进还是慢放?
历史数据回放,说白了就是让策略「穿越」回过去,重新跑一遍。
但这里有个坑:数据的时间粒度。
做市策略对时间敏感,通常需要 tick 级数据。但 tick 数据量巨大,一天可能几百万条。全量回放,速度慢得让人崩溃。
我的做法是:
- 快速回放:用于参数优化,只保留关键事件(如大单成交、价格跳变)
- 精细回放:用于策略验证,逐 tick 模拟,但只跑最近一周的数据
你想想看,如果你每次调参数都要跑三个月的数据,那优化效率得多低?
注意:快速回放会丢失一些细节,比如滑点、订单簿深度变化。所以,参数优化用快速回放,最终验证必须用精细回放。这个顺序不能乱。
策略参数优化:别掉进过拟合的坑
参数优化,是回测里最容易让人兴奋,也最容易让人翻车的地方。
我记得有一次,我优化一个做市策略的参数,在历史数据上跑出了年化 80% 的收益。当时那个激动啊,觉得要发财了。结果实盘一周,亏了 15%。
为什么?因为参数过拟合了。策略完美适配了历史数据里的噪声,但一到实盘,噪声变了,策略就失效了。
怎么避免?我总结了三条经验:
- 参数数量要少:一个策略超过 5 个可调参数,就要警惕了。参数越多,过拟合风险越大。
- 使用交叉验证:把历史数据分成三段,用前两段优化,第三段验证。如果三段结果差异很大,说明参数不稳定。
- 设置参数边界:比如报价频率,不能低于 100ms,否则交易所会拒绝。这些边界条件,必须在优化前就定好。
一句话总结:参数优化不是找「最优解」,而是找「稳健解」。一个在多种市场环境下都能赚钱的参数,才是好参数。
绩效评估指标:别只看收益率
做市策略的评估,和普通交易策略不一样。你不能只看收益率,还要看:
| 指标 | 说明 | 我的经验值 |
|---|---|---|
| 夏普比率 | 衡量风险调整后收益 | 做市策略通常 > 2 才算合格 |
| 最大回撤 | 账户从峰值跌落的幅度 | 做市策略应 < 5%,否则风险太高 |
| 胜率 | 盈利交易占比 | 做市策略胜率通常 > 60% |
| 盈亏比 | 平均盈利 / 平均亏损 | 做市策略盈亏比 > 1.5 比较健康 |
| 库存周转率 | 库存被清空的频率 | 越高越好,说明流动性好 |
嗯,这里要注意:夏普比率高不代表策略好。我见过一个策略,夏普比率 3.5,但最大回撤 20%。这种策略,你根本拿不住。
我个人习惯,先看最大回撤,再看夏普比率。回撤控制不住,收益再高也没用。
回测系统的核心逻辑
说了这么多,我画了一张图,帮你理清回测系统的整体结构:
这张图展示了回测系统的完整流程。数据层提供原料,引擎层负责烹饪,策略层决定口味。而回放模式切换,决定了你是「快餐」还是「米其林」。
一个简单的回测代码示例
说了这么多理论,来点实际的。下面是一个极简的回测引擎核心代码,只展示最关键的逻辑:
class BacktestEngine:
def __init__(self, data, initial_capital=100000):
self.data = data # 历史数据
self.capital = initial_capital
self.position = 0 # 当前持仓
self.trades = [] # 成交记录
def run(self, strategy):
"""运行回测"""
for tick in self.data:
# 1. 策略生成报价
bid, ask = strategy.generate_quotes(tick)
# 2. 模拟撮合
if tick.price <= bid: # 买单成交
self._execute_buy(bid, tick.volume)
elif tick.price >= ask: # 卖单成交
self._execute_sell(ask, tick.volume)
# 3. 更新策略状态
strategy.update(tick, self.position)
# 4. 计算绩效
return self._calculate_performance()
def _execute_buy(self, price, volume):
cost = price * volume
if cost <= self.capital:
self.capital -= cost
self.position += volume
self.trades.append(('buy', price, volume))
def _execute_sell(self, price, volume):
if self.position >= volume:
self.capital += price * volume
self.position -= volume
self.trades.append(('sell', price, volume))
def _calculate_performance(self):
# 这里计算夏普比率、最大回撤等指标
pass
这段代码虽然简单,但包含了回测引擎的核心:数据循环、撮合逻辑、状态更新。你可以在此基础上,加入滑点、手续费、订单簿深度等细节。
我的建议:刚开始做回测,不要追求完美。先跑通一个最简单的版本,然后逐步加功能。我见过太多人,一开始就想做「通用回测框架」,结果半年过去了,连一个策略都没跑通。
避坑指南
最后,分享几个我踩过的坑:
- 前视偏差:回测时不小心用了未来的数据。比如,用当天的收盘价来生成当天的交易信号。这绝对是回测里最常见的错误。我曾经因为这个,白高兴了一个月。
- 忽略交易成本:做市策略对交易成本极其敏感。手续费、滑点、甚至网络延迟,都会吃掉你的利润。回测时一定要把这些算进去。
- 过度优化:前面说过了,参数优化要适可而止。记住,历史数据不会重演,但人性会。
最后一句忠告:回测通过,只是开始。实盘验证,才是真正的考验。永远给你的回测结果打个折扣——比如 50%。这样,当实盘表现不如预期时,你至少不会太失望。
交易系统化学习资料 微信Strategy888888