回测框架设计原则:回测与实盘差异、核心组件、事件驱动架构、数据与执行分离

做市商策略的回测,说白了就是一场「模拟考试」。但模拟考和真实高考,差距有多大?我见过太多人,回测曲线漂亮得像艺术品,一上实盘就崩。为什么?因为回测框架的设计,从一开始就埋下了坑。

今天我们就来聊聊,怎么搭一个靠谱的回测框架。我个人习惯是:先搞清楚「回测和实盘到底差在哪」,再动手设计组件。顺序不能乱。

回测与实盘的差异:那些看不见的坑

先泼一盆冷水:回测永远无法完美模拟实盘。但我们可以尽量缩小差距。我总结了几点核心差异:

  • 延迟与滑点:回测里订单瞬间成交,实盘里可能等几秒甚至被拒。我曾经在回测中忽略滑点,结果实盘时每笔交易多亏了0.5个tick,一个月下来利润少了30%。
  • 流动性幻觉:回测时你挂单10个BTC,市场纹丝不动。实盘时你挂10个BTC,价格直接被打穿。嗯,这就是所谓的「市场冲击成本」。
  • 数据质量:回测用的历史数据,往往是「事后清洗过的」。实盘数据有缺失、有异常、有延迟。你想想看,如果回测数据里没有「交易所宕机」的场景,那你的风控逻辑就是纸糊的。
  • 手续费与资金费率:很多新手回测时只算手续费,忘了资金费率。做市商策略最怕这个——持仓过夜被收几次资金费,利润全没了。
避坑指南:我曾经在回测中把手续费设为0,结果实盘时发现手续费吃掉了一半的利润。从那以后,我强制自己在回测框架里加入「手续费+滑点+资金费率」三件套,缺一不可。

回测框架核心组件:搭积木的思路

一个成熟的回测框架,应该像乐高积木一样,每个组件独立、可替换。我习惯把它拆成这几个模块:

组件 职责 个人经验
数据源 提供历史行情数据(tick、1s、1min等) 我建议至少准备两种数据源:本地CSV和数据库。本地CSV用于快速迭代,数据库用于大规模回测。
策略引擎 执行做市逻辑:报价、撤单、风控 策略引擎要支持「热插拔」,方便切换不同策略版本。
订单管理 模拟订单簿、撮合、记录成交 这里最容易出bug。我踩过坑:订单簿的撮合逻辑写错了,导致回测里成交价永远比实盘好。
风控模块 检查仓位、资金、最大亏损等 风控要放在「策略执行之前」,而不是之后。否则等你发现风险,已经亏完了。
绩效分析 计算夏普、最大回撤、胜率等 别只看收益率。我见过回测年化200%的策略,最大回撤80%,这种你敢上实盘?

每个组件之间通过「事件」通信。这就是我们接下来要讲的事件驱动架构。

事件驱动架构:让回测「活」起来

为什么用事件驱动?因为实盘就是事件驱动的。行情来了、订单成交了、风控触发了——这些都是事件。回测框架如果写成「顺序执行」,那和实盘就是两个世界。

我设计回测框架时,会定义一个事件总线。所有组件都往总线上发事件、收事件。举个例子:

class EventBus:
    def __init__(self):
        self.handlers = {}

    def register(self, event_type, handler):
        if event_type not in self.handlers:
            self.handlers[event_type] = []
        self.handlers[event_type].append(handler)

    def emit(self, event):
        for handler in self.handlers.get(event.type, []):
            handler(event)

# 使用示例
bus = EventBus()
bus.register('tick', strategy.on_tick)
bus.register('fill', order_manager.on_fill)
bus.emit(TickEvent(symbol='BTC/USDT', price=50000, volume=1.5))

你看,这样每个组件只关心自己感兴趣的事件。策略引擎不用管订单怎么撮合,订单管理器不用管行情怎么来。耦合度低,测试起来也方便。

小技巧:事件类型要定义得足够细。我习惯把「tick」「orderbook」「fill」「cancel」「error」都分开。太粗的事件会让处理逻辑变得臃肿。

为什么会这样?因为实盘里,不同事件的优先级不同。比如「风控事件」必须优先处理,而「行情事件」可以稍微延迟。事件驱动架构天然支持这种优先级调度。

数据与执行分离:回测的「单线程」陷阱

很多新手写回测框架,喜欢把数据读取和策略执行混在一起。比如这样:

# 错误示范:数据和执行耦合
for tick in data:
    strategy.on_tick(tick)
    order_manager.process_orders(tick)
    risk_manager.check(tick)

这有什么问题?问题大了。实盘里,数据是异步来的,订单执行也是异步的。你把它们写成同步循环,等于假设「所有事情都在一瞬间完成」。这会导致回测结果过于乐观。

我建议的做法是:数据层和执行层完全分离

  • 数据层:只负责提供数据,不关心策略怎么用。可以是一个迭代器,也可以是一个异步流。
  • 执行层:只负责处理订单,不关心数据从哪里来。它维护一个虚拟订单簿,模拟撮合逻辑。

两者通过「时间戳」同步。比如数据层每推送一个tick,执行层就检查当前时间戳下有没有待处理的订单。这样更接近实盘的行为。

核心原则:数据是「输入」,执行是「输出」。两者之间只通过事件通信,不共享任何状态。这样你换数据源、换执行引擎,都不影响策略逻辑。

我记得有一次,团队里有人把数据缓存和订单簿放在同一个对象里。结果回测时数据量一大,内存直接爆了。后来改成数据与执行分离,不仅内存降了60%,回测速度也快了3倍。

知识体系结构图

下面这张图,是我对本章核心逻辑的总结。你可以把它当作回测框架设计的「路线图」:

回测框架设计核心逻辑 回测与实盘差异 延迟/滑点/流动性 数据质量/手续费 核心组件 数据源/策略引擎 订单管理/风控/绩效 事件驱动架构 事件总线/注册/分发 异步/优先级调度 数据与执行分离 数据层只提供数据 执行层只处理订单 设计原则总结 认清差异 → 拆分组装 → 事件驱动 → 分离数据与执行 目标:让回测尽可能接近实盘,减少「回测幻觉」

嗯,这张图其实是我自己画框架时常用的思路。从左到右,从上到下,一步步拆解。你照着这个结构去设计,至少不会漏掉关键模块。

个人习惯:我每次搭新框架,都会先画一张类似的图。画完你就知道哪些组件是必须的,哪些可以后面再加。别一上来就写代码,容易迷失。

最后说一句:回测框架没有「完美」的,只有「够用」的。关键是你要清楚每个设计决策的代价。比如你用事件驱动,性能会稍微下降,但换来的是更接近实盘的行为。值不值?你自己权衡。

专注资料整理