4、回测引擎设计:事件驱动架构、Bar驱动架构、回测引擎核心类设计、交易模拟器实现

回测引擎是整个量化系统的「心脏」。

说白了,它就是一台时间机器。你给它历史数据,它模拟交易,最后告诉你策略到底行不行。我见过太多人把精力全花在策略上,结果回测引擎写得一塌糊涂,最后得出的结论全是错的。

嗯,今天我们就来拆解这个核心模块。

4.1 两种主流架构:Bar驱动 vs 事件驱动

先聊架构选型。我个人习惯把回测引擎分成两大类:Bar驱动和事件驱动。它们各有各的适用场景。

4.1.1 Bar驱动架构

Bar驱动是最直观的方式。你想想看,K线数据是一根一根来的,每来一根,引擎就推进一步。

核心逻辑:

  • 按时间顺序遍历每一根Bar
  • 在每根Bar上执行策略逻辑
  • 记录持仓和交易结果

我在项目中遇到过一个小团队,他们用Bar驱动做日线级别的策略回测,代码量很少,跑得飞快。但换到分钟级别时,问题就来了——滑点模拟不准确,成交细节丢失。

适用场景:低频策略、日线及以上周期、快速验证想法

缺点:无法处理盘中高频事件,成交模拟粗糙

4.1.2 事件驱动架构

事件驱动就复杂多了。它不依赖固定的时间步长,而是响应各种「事件」——比如行情来了、订单成交了、定时器到了。

为什么会这样?因为真实市场里,交易不是按Bar发生的。订单簿在毫秒级变化,你的策略需要对这些事件做出反应。

核心组件:

  1. 事件队列——所有事件排队等待处理
  2. 事件循环——不断从队列取事件并分发
  3. 事件处理器——策略逻辑挂载在这里

我曾经帮一个做高频的朋友调试回测,他的事件驱动引擎里塞了十几个事件类型。调试起来确实痛苦,但回测结果和实盘非常接近。

我的建议:如果你刚开始做回测系统,先别碰事件驱动。从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 整体架构图

下面这张图是我自己画的核心架构。你看一眼就能明白各个模块怎么配合:

回测引擎核心架构图 数据管理器 DataManager 策略模块 Strategy 投资组合管理器 PortfolioManager 回测引擎主类 BacktestEngine 交易模拟器 TradeSimulator 推送数据 生成信号 查询状态 发送订单 成交回报 实线:数据/指令流 | 虚线:反馈流

个人经验:我建议你把回测引擎做成「可插拔」的。数据源可以换、撮合逻辑可以换、费用模型可以换。这样同一个引擎,既能跑股票,也能跑期货,甚至能跑加密货币。

好了,回测引擎的核心设计就聊到这儿。记住一句话:回测引擎的精度,决定了你策略的可信度。别图快,把细节做扎实了,后面省心得多。