回测框架设计原则:模块化设计、事件驱动、数据与策略分离
做市策略的回测,说白了就是一场「模拟考试」。你想想看,真金白银冲进去之前,总得先知道自己的策略靠不靠谱吧?我做了这么多年量化,见过太多人上来就写个脚本跑回测,结果换到实盘就崩了。问题出在哪?框架设计没想清楚。
今天咱们聊聊回测框架的三个核心设计原则。嗯,这三个原则是我自己踩坑踩出来的。刚开始做回测时,我也喜欢把所有代码揉在一个文件里,后来发现改个参数都要翻半天,别提多痛苦了。
模块化设计:拆开才能复用
模块化,说白了就是「各司其职」。一个回测框架,至少应该拆成这几个模块:
- 数据模块:负责获取、清洗、存储行情数据
- 策略模块:实现具体的做市逻辑,比如报价、风控
- 执行模块:模拟订单簿、撮合、滑点
- 分析模块:计算收益、夏普、最大回撤等指标
我个人习惯,每个模块只干一件事。比如数据模块,它只管把 tick 数据喂进来,至于策略怎么用,它不关心。这样好处很明显——哪天你想换数据源,改一个模块就行,其他代码动都不用动。
核心原则:每个模块的输入输出要清晰,模块之间通过接口通信,不要直接调用内部函数。
我在项目中遇到过一件事:有个同事把数据清洗逻辑直接写在策略类里。结果换了个交易所的数据,策略代码改得面目全非。嗯,这就是耦合太紧的代价。
事件驱动:让系统自己动起来
做市策略和普通策略不一样。普通策略可能每天收盘后算一次信号,但做市策略是高频的,每笔 tick 都可能触发报价更新。这时候,事件驱动就派上用场了。
事件驱动的核心思想很简单:系统不主动轮询,而是等事件来了再响应。比如:
- 新 tick 数据到达 → 触发策略更新报价
- 订单成交 → 触发仓位更新和风控检查
- 定时器到期 → 触发统计报告生成
你想想看,如果每来一个 tick 都去轮询一遍所有模块,CPU 全浪费在空转上了。事件驱动的好处就是「按需干活」,效率高得多。
小技巧:用 Python 的 queue.Queue 或者 asyncio 的事件循环来实现事件总线。我个人偏好用队列,调试起来更直观。
我曾经犯过一个错:把所有事件处理都放在主线程里,结果一个耗时操作卡住了整个回测。后来改成异步事件处理,每个事件类型有自己的处理器,问题就解决了。
数据与策略分离:别让数据绑架你的逻辑
这个原则,我愿称之为「回测框架的命根子」。为什么?因为做市策略对数据极其敏感。同样的策略,用不同频率的数据跑,结果可能天差地别。
数据与策略分离,具体来说:
- 数据层:只负责提供标准化的行情接口,比如
get_tick()、get_orderbook() - 策略层:只关心业务逻辑,比如「当买卖价差超过 0.01 时,挂单吃单」
- 两者之间通过事件通信:数据来了,触发策略;策略下单,触发执行
这样做的好处,我举个例子你就明白了。假设你原来用 1 秒的快照数据做回测,后来想换成 tick 级数据。如果数据和策略混在一起,你得改策略代码。但如果分离了,你只需要换数据模块,策略代码一行都不用改。
注意:数据与策略分离,不代表它们完全独立。策略需要知道数据的格式和频率,否则没法用。我的做法是定义一个「数据契约」——比如所有 tick 数据必须包含时间戳、价格、成交量三个字段。策略按契约来写,数据模块按契约来提供。
我记得有一次,团队里有人把数据清洗逻辑写进了策略的回调函数里。结果数据源换了格式,策略直接报错。嗯,这就是典型的「数据绑架策略」。
一张图看懂框架设计
下面这张图,是我自己画的一个简化版回测框架结构。你看一眼就明白了:
你看,数据模块产生 tick 事件,通过事件总线发给策略模块。策略模块根据逻辑生成订单事件,再发给执行模块。执行模块撮合后产生成交事件,最后分析模块汇总出报告。整个过程,每个模块只关心自己的事。
代码示例:一个极简的事件驱动回测骨架
光说不练假把式。我写个最简单的例子,你感受一下:
# 事件基类
class Event:
def __init__(self, event_type, data):
self.type = event_type
self.data = data
# 事件总线
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)
# 数据模块
class DataModule:
def __init__(self, bus):
self.bus = bus
def feed_tick(self, tick):
event = Event('tick', tick)
self.bus.emit(event)
# 策略模块
class StrategyModule:
def __init__(self, bus):
self.bus = bus
bus.register('tick', self.on_tick)
def on_tick(self, event):
# 做市逻辑:如果价差大于0.01,挂单
tick = event.data
if tick['ask'] - tick['bid'] > 0.01:
order = {'side': 'buy', 'price': tick['bid'] + 0.005}
self.bus.emit(Event('order', order))
# 执行模块
class ExecutionModule:
def __init__(self, bus):
self.bus = bus
bus.register('order', self.on_order)
def on_order(self, event):
# 模拟撮合
print(f"执行订单: {event.data}")
# 组装
bus = EventBus()
data = DataModule(bus)
strategy = StrategyModule(bus)
execution = ExecutionModule(bus)
# 模拟数据
data.feed_tick({'bid': 100.00, 'ask': 100.02})
data.feed_tick({'bid': 100.01, 'ask': 100.03})
这个例子虽然简单,但骨架是完整的。你想想看,如果以后要加风控模块,只需要注册一个 'order' 事件的处理器,在策略和执行之间插一脚就行。模块化、事件驱动、数据与策略分离,全都有了。
避坑指南:我曾经在事件总线上踩过一个坑——事件处理顺序。如果多个处理器注册了同一个事件,它们的执行顺序可能影响结果。我的建议是:给每个处理器加一个优先级字段,按优先级排序执行。
总结一下
这三个原则,说白了就是「高内聚、低耦合」在回测框架里的具体落地。模块化让你能拆开改,事件驱动让系统自动响应,数据与策略分离让你换数据不换逻辑。嗯,我做了这么多年,发现凡是能稳定跑上几个月的回测框架,都离不开这三个原则。
你如果刚开始搭框架,别急着写代码。先画张图,把模块边界划清楚。磨刀不误砍柴工,框架设计好了,后面写代码就是填坑的事。