4、回测引擎设计:事件驱动架构、Bar驱动架构、回测引擎核心类设计、交易模拟器实现
回测引擎是整个量化系统的「心脏」。
说白了,它就是一台时间机器。你给它历史数据,它模拟交易,最后告诉你策略到底行不行。我见过太多人把精力全花在策略上,结果回测引擎写得一塌糊涂,最后得出的结论全是错的。
嗯,今天我们就来拆解这个核心模块。
4.1 两种主流架构:Bar驱动 vs 事件驱动
先聊架构选型。我个人习惯把回测引擎分成两大类:Bar驱动和事件驱动。它们各有各的适用场景。
4.1.1 Bar驱动架构
Bar驱动是最直观的方式。你想想看,K线数据是一根一根来的,每来一根,引擎就推进一步。
核心逻辑:
- 按时间顺序遍历每一根Bar
- 在每根Bar上执行策略逻辑
- 记录持仓和交易结果
我在项目中遇到过一个小团队,他们用Bar驱动做日线级别的策略回测,代码量很少,跑得飞快。但换到分钟级别时,问题就来了——滑点模拟不准确,成交细节丢失。
适用场景:低频策略、日线及以上周期、快速验证想法
缺点:无法处理盘中高频事件,成交模拟粗糙
4.1.2 事件驱动架构
事件驱动就复杂多了。它不依赖固定的时间步长,而是响应各种「事件」——比如行情来了、订单成交了、定时器到了。
为什么会这样?因为真实市场里,交易不是按Bar发生的。订单簿在毫秒级变化,你的策略需要对这些事件做出反应。
核心组件:
- 事件队列——所有事件排队等待处理
- 事件循环——不断从队列取事件并分发
- 事件处理器——策略逻辑挂载在这里
我曾经帮一个做高频的朋友调试回测,他的事件驱动引擎里塞了十几个事件类型。调试起来确实痛苦,但回测结果和实盘非常接近。
我的建议:如果你刚开始做回测系统,先别碰事件驱动。从Bar驱动入手,跑通流程再说。等需要精细模拟时再升级。
4.2 回测引擎核心类设计
不管选哪种架构,核心类的设计思路是相通的。我一般会拆成这几个模块:
4.2.1 数据管理器(DataManager)
负责加载、对齐、切片历史数据。别小看这个模块,数据对齐出问题,回测结果直接废掉。
class DataManager:
def __init__(self, data_sources):
self.data = {}
self.current_index = 0
def load_data(self, symbol, start, end):
# 加载并对齐多品种数据
pass
def get_current_bar(self, symbol):
# 返回当前时间点的Bar数据
pass
def next(self):
# 推进到下一根Bar
self.current_index += 1
4.2.2 策略基类(Strategy)
策略是用户写的,但框架得提供基类。我习惯把「信号生成」和「下单逻辑」分开。
class Strategy:
def __init__(self):
self.positions = {}
self.orders = []
def on_bar(self, data):
# 用户在这里写策略逻辑
raise NotImplementedError
def buy(self, symbol, quantity):
# 生成买入订单
pass
def sell(self, symbol, quantity):
# 生成卖出订单
pass
4.2.3 投资组合管理器(PortfolioManager)
这个模块管钱。它跟踪账户余额、持仓市值、保证金占用。嗯,这里要注意:杠杆、手续费、保证金规则都得模拟到位。
| 属性 | 说明 |
|---|---|
| cash | 可用现金 |
| holdings | 持仓字典 {symbol: quantity} |
| total_value | 总资产 = cash + 持仓市值 |
| commission | 累计手续费 |
4.2.4 回测引擎主类(BacktestEngine)
这是总调度。它把上面几个模块串起来,驱动整个回测流程。
class BacktestEngine:
def __init__(self, data_manager, strategy, portfolio):
self.data = data_manager
self.strategy = strategy
self.portfolio = portfolio
self.results = []
def run(self):
while self.data.has_next():
bar = self.data.next()
self.strategy.on_bar(bar)
self._process_orders()
self._update_portfolio()
self._record_snapshot()
def _process_orders(self):
# 撮合订单,更新持仓
pass
def _record_snapshot(self):
# 记录当前时间点的账户状态
pass
4.3 交易模拟器实现
交易模拟器是回测引擎里最容易出bug的地方。说白了,它要回答一个问题:「这个订单到底能不能成交?以什么价格成交?」
4.3.1 撮合逻辑
最简单的撮合方式:用Bar的收盘价成交。但这样太粗糙了。
我建议至少做到这几点:
- 限价单:检查当前Bar的最高最低价,判断是否触达
- 市价单:用下一根Bar的开盘价成交(模拟滑点)
- 部分成交:大单要考虑流动性,不能一次性全成交
避坑指南:我曾经在回测里用了收盘价成交,结果策略表现特别好。实盘一跑,滑点直接吃掉一半利润。后来我强制加了一个「滑点模型」,用随机数模拟成交价格的偏移,回测结果才靠谱起来。
4.3.2 订单状态机
每个订单都有自己的生命周期。我习惯用状态机来管理:
订单状态流转:
CREATED → SUBMITTED → PARTIALLY_FILLED → FILLED
→ REJECTED
→ CANCELLED
4.3.3 费用模型
别小看手续费。高频策略里,手续费能决定生死。
我一般会模拟:
- 固定费率(如万分之三)
- 阶梯费率(交易量越大费率越低)
- 印花税、过户费(A股特有)
- 滑点成本(固定点数或百分比)
4.4 整体架构图
下面这张图是我自己画的核心架构。你看一眼就能明白各个模块怎么配合:
个人经验:我建议你把回测引擎做成「可插拔」的。数据源可以换、撮合逻辑可以换、费用模型可以换。这样同一个引擎,既能跑股票,也能跑期货,甚至能跑加密货币。
好了,回测引擎的核心设计就聊到这儿。记住一句话:回测引擎的精度,决定了你策略的可信度。别图快,把细节做扎实了,后面省心得多。