2015 字
10 分钟

回测框架进阶:向量化与事件驱动

本课目标:理解向量化与事件驱动两种回测范式的取舍,能画出一个最小事件驱动回测器的模块图,并知道如何验证回测器本身没写错。

两种范式:一次算完 vs 逐日推演#

向量化回测把整段历史当作矩阵运算一次算完:信号矩阵乘以收益矩阵,几行 pandas 就能得到净值曲线。它快、代码短、适合大规模参数扫描和因子研究。

事件驱动回测则模拟真实交易过程:时间一天一天(或一笔一笔)往前推,每个时点上策略只能看到当时已有的信息,产生订单,订单经过撮合规则变成成交,成交更新持仓和现金。它慢、代码多,但更接近实盘。

维度向量化事件驱动
速度极快慢一到两个数量级
前视偏差风险高(一不小心就用了未来数据)低(结构上隔离了未来信息)
路径依赖逻辑难表达(止损、资金约束)自然表达
适用阶段因子筛选、参数扫描策略验证、接近实盘的模拟

实际工作流通常是两段式:先用向量化快速筛掉明显不行的想法,再把幸存者放进事件驱动框架做精细验证。两边结果的差异本身就是重要信息——差异大往往说明策略依赖了向量化框架无法表达的交易细节,比如止损触发的先后顺序、资金不足时的舍弃逻辑,这些恰恰是实盘中真实存在的约束。

最小事件驱动回测器的五个模块#

不必一上来就写通用框架,通用是重构出来的,不是设计出来的。一个能用的最小实现只需要五个模块,各司其职、只通过事件通信:

  • 数据模块(DataHandler):按时间顺序吐出行情事件,保证任意时刻只暴露该时刻之前的数据。
  • 信号模块(Strategy):消费行情事件,产出信号事件(想买什么、想卖什么)。
  • 组合模块(Portfolio):把信号翻译成目标仓位,结合当前持仓与现金约束生成订单事件。
  • 执行模块(ExecutionHandler):模拟撮合,考虑成交价、滑点、涨跌停与停牌,产出成交事件。
  • 记账模块(Ledger):消费成交事件,更新持仓、现金和每日净值。

模块之间通过一个事件队列串联,主循环大致是这样:

from collections import deque
events = deque()
for bar in data_handler.stream_bars(): # 逐日推进
events.append(bar)
while events:
event = events.popleft()
if event.type == 'BAR':
strategy.on_bar(event, events) # 可能追加 SIGNAL
portfolio.on_bar(event) # 更新市值
elif event.type == 'SIGNAL':
portfolio.on_signal(event, events) # 可能追加 ORDER
elif event.type == 'ORDER':
broker.on_order(event, events) # 可能追加 FILL
elif event.type == 'FILL':
portfolio.on_fill(event)
ledger.record(event)

这个结构的价值在于隔离:策略只看得到行情和自己的信号,成交细节全部封装在执行模块里。想换一种滑点假设,只改执行模块,策略代码一行不动。

撮合假设:魔鬼在执行模块里#

事件驱动框架”贴近实盘”的程度,完全取决于执行模块里的假设。至少要明确回答这几个问题:

  • 用什么价格成交:次日开盘价、次日均价还是次日收盘价?用当日收盘价成交当日信号是最常见的前视错误。对 A 股日线回测,“T 日收盘出信号、T+1 以开盘价或 VWAP 近似成交”是比较诚实的假设。
  • T+1 规则:A 股股票当日买入不能当日卖出。执行模块要区分”可卖持仓”和”总持仓”,否则日内往返的策略会在回测里合法、在实盘里违规。
  • 部分成交:目标金额超出流动性约束时是顺延、缩量还是放弃?三种选择对高换手策略的结果差异不小。
  • 资金检查顺序:同一天既有买单又有卖单时,是先卖后买(腾出资金)还是并行处理?处理顺序不同,可用资金就不同。

这些假设没有唯一正确答案,但必须显式写下来,而且宁可保守。回测的职责不是把策略算得好看,而是给实盘表现一个不至于失望的下界。

开源框架:泛泛的选型建议#

社区里有不少现成框架,风格各异:有的是老牌事件驱动框架,功能全但代码风格偏老;有的主打向量化,扫参数极快,适合研究阶段;有的国内框架对 A 股的交易规则(涨跌停、T+1、股票代码规范)支持更好;还有的框架同时提供回测和实盘接口,方便平滑过渡。

选型建议只有三条:一是研究阶段优先选快的,验证阶段优先选贴近实盘的;二是看它对 A 股规则的支持程度,缺什么自己是否补得动;三是无论用什么框架,你都应该亲手写过一遍最小事件驱动回测器——不是为了用它,而是为了看懂框架在替你做什么假设。

如何验证回测器本身的正确性#

策略可以错,回测器不能错。验证回测器有几个实用手段:

  1. 恒等检验:构造一个”永远满仓持有单一资产”的策略,回测净值应当严格等于该资产的复权净值曲线(扣除设定成本后)。对不上就是记账有 bug。
  2. 零成本对照:把佣金、滑点全部设为零,事件驱动结果应当收敛到同一策略的向量化结果。两边差异应该能被逐笔解释。
  3. 守恒检查:任意时刻,现金 + 持仓市值 = 总资产;每笔成交前后资产变化应恰好等于交易成本。把这写成断言,跑回测时全程开启。
  4. 极端用例:只有一个交易日的数据、全程停牌的股票、开盘即涨停的委托——这些边界用例最容易暴露撮合逻辑的漏洞。

把这些检验写成单元测试,回测器每次改动都自动跑一遍。另外强烈建议给回测器加上逐笔成交日志:每笔成交记录时间、方向、数量、价格、成本和成交后的现金与持仓快照。当回测结果可疑时,抽几笔手工核对,是定位问题最快的路径——比盯着最终净值曲线猜测有效得多。回测器是你所有研究结论的地基,在它上面花的每一分验证功夫,都会在后续省下十倍的排查时间。本文讨论的是工程方法,不构成投资建议。

本课小结#

  • 向量化适合大规模筛选,事件驱动适合精细验证,两者是流水线上的先后两站,不是二选一。
  • 最小事件驱动回测器 = 数据、信号、组合、执行、记账五个模块 + 一个事件队列。
  • 模块化的核心收益是隔离假设:改滑点模型不碰策略代码。
  • 框架选型看研究/验证阶段的需求和对 A 股规则的支持,但最好亲手写过一个最小实现。
  • 用恒等检验、零成本对照、守恒断言和极端用例来验证回测器本身,并固化为单元测试。

动手练习:亲手实现上面的五模块最小回测器(不超过 300 行),跑一个”月初买入、月末卖出某 ETF”的玩具策略。然后做恒等检验:写一个永远满仓的策略,验证净值与标的复权曲线一致;再把同一策略用向量化方式实现一遍,对账两者的每日净值差异。