回测引擎设计:事件驱动架构、Bar数据循环、订单管理模块、持仓管理模块、资金管理模块
做量化交易这些年,我见过太多人把精力全花在策略因子上,结果回测引擎一跑就崩。说白了,回测引擎才是整个系统的地基。地基不稳,策略再漂亮也是空中楼阁。
今天咱们就聊聊回测引擎的核心设计。我个人习惯把回测引擎拆成五个模块:事件驱动架构、Bar数据循环、订单管理、持仓管理、资金管理。这五个模块配合好了,你的回测才能真实反映策略水平。
核心观点:回测引擎不是简单的“数据进来-信号出去”的循环。它需要模拟真实交易环境中的每一个环节,包括订单排队、滑点、手续费、资金占用等。忽略这些细节,回测结果就是自欺欺人。
一、事件驱动架构
事件驱动,说白了就是“发生了什么事,就触发什么处理逻辑”。我刚开始做回测时用的是简单的循环遍历,后来发现根本处理不了复杂场景——比如同时收到多个订单回报、行情数据延迟到达等。
事件驱动架构的核心是事件队列。所有外部输入(行情、订单回报、定时器)都变成事件,塞进队列里。引擎从队列头部取事件,分发给对应的处理器。
class EventEngine:
def __init__(self):
self.queue = Queue()
self.handlers = {}
def register_handler(self, event_type, handler):
if event_type not in self.handlers:
self.handlers[event_type] = []
self.handlers[event_type].append(handler)
def put_event(self, event):
self.queue.put(event)
def run(self):
while True:
event = self.queue.get()
if event.type == 'STOP':
break
for handler in self.handlers.get(event.type, []):
handler(event)
事件类型我一般分这么几类:
| 事件类型 | 说明 | 触发时机 |
|---|---|---|
| TICK | 逐笔行情 | 每收到一笔成交数据 |
| BAR | K线数据 | 每个时间周期结束 |
| ORDER | 订单状态变化 | 订单成交、撤单、拒绝 |
| TRADE | 成交回报 | 订单部分或全部成交 |
| TIMER | 定时器 | 按固定间隔触发 |
我的经验:事件队列一定要用线程安全的实现。我在项目中遇到过因为队列并发访问导致事件丢失的bug,查了两天才定位到问题。Python里用queue.Queue就挺好,别自己造轮子。
二、Bar数据循环
Bar数据循环是回测引擎的主循环。每个Bar进来,引擎要做三件事:更新行情、检查信号、执行交易。
嗯,这里要注意一个细节:Bar的闭合时间点。比如5分钟K线,是在第5分钟结束时生成Bar,还是第6分钟开始时?这个差异会影响信号触发时机。我个人习惯用“Bar闭合后立即触发”的方式,更贴近实盘。
class BarLoop:
def __init__(self, strategy, data_feed):
self.strategy = strategy
self.data_feed = data_feed
self.current_bar = None
def run(self):
for bar in self.data_feed:
self.current_bar = bar
# 1. 更新策略的行情数据
self.strategy.on_bar(bar)
# 2. 检查是否有新信号
signals = self.strategy.check_signals()
# 3. 执行交易
for signal in signals:
self.execute_signal(signal)
# 4. 更新持仓和资金
self.update_portfolio()
Bar数据循环中,我最常踩的坑是数据对齐问题。不同时间周期的Bar,比如日线和小时线,它们的闭合时间不一致。你想想看,如果日线Bar还没闭合,你就用它的数据去计算指标,结果肯定不对。
避坑指南:我曾经因为Bar数据的时间戳对齐问题,导致回测结果比实盘好了一大截。后来发现是用了未来数据——用当天的收盘价去计算当天的买入信号。这种错误在回测中非常隐蔽,一定要在数据循环中加入时间戳校验。
三、订单管理模块
订单管理模块负责处理订单的整个生命周期:创建、发送、成交、撤单。说白了,它就是连接策略和市场的桥梁。
订单类型我一般支持这几种:
- 市价单:立即以当前最优价成交,不保证价格只保证成交
- 限价单:指定价格,不保证成交时间
- 止损单:价格触及止损位时触发市价单
- 止盈单:价格触及止盈位时触发限价单
class OrderManager:
def __init__(self):
self.orders = {} # order_id -> Order
self.pending_orders = []
def send_order(self, order):
order_id = self.generate_order_id()
order.order_id = order_id
self.orders[order_id] = order
self.pending_orders.append(order)
# 发送到交易所(模拟)
self.exchange.send(order)
def on_order_report(self, report):
order = self.orders[report.order_id]
order.status = report.status
if report.status == 'FILLED':
self.on_trade(report)
elif report.status == 'REJECTED':
self.on_reject(report)
订单管理模块里,我最看重的是订单状态机。一个订单从创建到最终完成,中间可能经历多次状态变化。如果状态机设计得不够严谨,很容易出现订单状态不一致的问题。
我的建议:订单状态机一定要用有限状态机来实现,不要用if-else硬编码。我在项目中用过一个叫transitions的Python库,专门管理状态转换,代码清晰很多。
四、持仓管理模块
持仓管理模块记录你当前持有多少股票、成本价是多少、浮动盈亏是多少。听起来简单,但实际做起来有不少细节。
举个例子:你分三次买入同一只股票,每次价格不同。那么你的持仓成本怎么算?加权平均?先进先出?还是后进先出?不同的算法会影响盈亏计算。
class PositionManager:
def __init__(self):
self.positions = {} # symbol -> Position
def update_position(self, trade):
symbol = trade.symbol
if symbol not in self.positions:
self.positions[symbol] = Position(symbol)
position = self.positions[symbol]
if trade.direction == 'BUY':
position.quantity += trade.quantity
position.cost = (position.cost * (position.quantity - trade.quantity) +
trade.price * trade.quantity) / position.quantity
elif trade.direction == 'SELL':
position.quantity -= trade.quantity
# 实现盈亏 = (卖出价 - 成本价) * 卖出数量
position.realized_pnl += (trade.price - position.cost) * trade.quantity
持仓管理还有一个容易忽略的点:分红、送股、配股等事件。这些事件会影响持仓数量和成本价。如果你在回测中不考虑这些,长期回测的结果会偏差很大。
避坑指南:我曾经在回测一只分红率很高的股票时,发现回测收益比实际收益高了很多。排查了半天,原来是忘了在除权除息日调整持仓成本。从那以后,我在持仓管理模块里专门加了一个“公司事件处理”的子模块。
五、资金管理模块
资金管理模块负责计算账户的总资产、可用资金、保证金占用等。它是整个回测引擎的“财务部”,所有交易最终都要反映到资金账户上。
资金管理的核心公式其实很简单:
总资产 = 现金 + 持仓市值
可用资金 = 现金 - 冻结资金
持仓市值 = Σ(持仓数量 * 当前价格)
浮动盈亏 = Σ(持仓数量 * (当前价格 - 成本价))
但实际应用中,有几个细节需要特别注意:
- 手续费计算:不同券商、不同品种的手续费标准不同。我一般把手续费做成可配置的,方便切换。
- 滑点模拟:市价单成交时,实际成交价可能比看到的价格差一些。我习惯在回测中加一个滑点参数,比如0.1%。
- 资金利用率:有些策略会满仓操作,有些会留一部分现金。资金管理模块要能控制仓位比例。
class CapitalManager:
def __init__(self, initial_capital=1000000):
self.cash = initial_capital
self.frozen_cash = 0
self.commission_rate = 0.0003 # 万分之三
self.slippage = 0.001 # 千分之一滑点
def can_afford(self, order):
cost = order.quantity * order.price * (1 + self.slippage)
commission = cost * self.commission_rate
return self.cash - self.frozen_cash >= cost + commission
def freeze_cash(self, amount):
self.frozen_cash += amount
def unfreeze_cash(self, amount):
self.frozen_cash -= amount
def settle_trade(self, trade):
cost = trade.quantity * trade.price
commission = cost * self.commission_rate
if trade.direction == 'BUY':
self.cash -= (cost + commission)
elif trade.direction == 'SELL':
self.cash += (cost - commission)
self.unfreeze_cash(cost)
我的经验:资金管理模块最好单独做一个回测后的分析功能。比如计算最大回撤、夏普比率、胜率等指标。这些指标能帮你判断策略的风险收益特征是否合理。
整体架构图
下面这张图展示了回测引擎五个模块之间的关系。你可以看到,事件驱动架构是核心,其他四个模块都通过事件队列进行通信。
这张图里,事件驱动引擎是中心枢纽。Bar数据循环产生行情事件,订单管理模块处理交易事件,持仓和资金模块根据成交回报更新状态。五个模块各司其职,又紧密配合。
好了,回测引擎的五个核心模块就聊到这儿。每个模块单独拿出来都能写一整章,但今天咱们先把框架搭起来。后面几章会深入每个模块的细节,包括性能优化、异常处理、日志记录等实战内容。
总结一下:回测引擎设计的关键是模块化、事件驱动、状态机管理。把复杂问题拆成小模块,每个模块只做一件事,做好一件事。这样后期维护和扩展都会轻松很多。