2. 回测框架设计原则:模块化设计、事件驱动架构、数据与逻辑分离
做市策略的回测,说白了就是一场「模拟战争」。你想想看,真金白银投进去之前,总得先在沙盘上推演几轮吧?我做了这么多年量化,见过太多人一上来就写个几百行的脚本,跑完发现bug一堆,改起来想死的心都有。
为什么会这样?因为回测框架的设计,从一开始就没想清楚。今天我就跟你聊聊,我个人习惯遵循的三个核心原则。嗯,这三个原则帮我少踩了无数坑。
2.1 模块化设计:拆开,别揉在一起
模块化,说白了就是「各司其职」。我见过最糟糕的代码,是把数据读取、策略逻辑、订单管理、绩效统计全部塞进一个函数里。你想想看,想改个手续费计算方式,结果把策略逻辑也带崩了。
我个人习惯把回测框架拆成这几个模块:
- 数据模块:负责行情数据的加载、清洗、对齐。只输出标准化的DataFrame。
- 策略模块:接收行情数据,输出交易信号。它不关心数据从哪来,也不关心订单怎么执行。
- 执行模块:接收信号,模拟撮合、成交、滑点、手续费。它只负责「模拟交易所」。
- 分析模块:接收成交记录,计算夏普比率、最大回撤、胜率等指标。
核心原则:每个模块只做一件事,且只通过接口通信。改一个模块,不影响其他模块。
我在项目中遇到过一件事:有个同事把数据清洗逻辑写在了策略类里。后来换了数据源,他得改策略类里的清洗代码,结果策略逻辑也被改坏了。嗯,从那以后,我强制要求数据模块必须独立。
2.2 事件驱动架构:让系统「活」起来
做市策略跟传统趋势策略不一样。趋势策略可能一天只交易几次,但做市策略每秒都可能产生信号。如果用循环遍历K线的方式,根本扛不住高频场景。
事件驱动架构,就是让系统「主动响应」而不是「被动轮询」。我常用的设计是这样的:
class EventEngine:
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 put(self, event):
# 事件来了,通知所有注册的处理器
for handler in self.handlers.get(event.type, []):
handler(event)
你看,核心就三个动作:注册事件、产生事件、分发事件。行情来了是一个事件,订单成交了是一个事件,定时器到了也是一个事件。
我曾经犯过一个错误:把所有事件处理逻辑写在一个大循环里,结果行情快了就卡死。后来改成事件驱动,每个处理器独立运行,系统瞬间流畅了。说白了,这就是「异步」的朴素实现。
小技巧:做市策略里,建议把「订单簿更新」和「成交回报」拆成两个独立事件。因为它们的处理逻辑完全不同,混在一起容易出bug。
2.3 数据与逻辑分离:别让数据「绑架」策略
这个原则,我是在一次惨痛教训后才真正理解的。当时我写了一个回测,数据直接硬编码在策略类里。后来想换一个时间区间,得改策略代码。更糟的是,数据格式一变,策略逻辑全废了。
数据与逻辑分离,核心就一句话:策略不应该知道数据是怎么来的。
我建议的做法:
- 数据模块输出统一的接口,比如
get_bars(symbol, start, end, freq) - 策略模块只调用这个接口,不关心背后是CSV、数据库还是API
- 数据清洗、对齐、重采样等操作,全部在数据模块内部完成
举个例子:
# 不好的做法:策略里直接读CSV
class MyStrategy:
def run(self):
df = pd.read_csv('data.csv')
# ... 策略逻辑
# 好的做法:策略只依赖接口
class MyStrategy:
def __init__(self, data_provider):
self.data = data_provider
def run(self):
bars = self.data.get_bars('BTCUSDT', '2024-01-01', '2024-06-01', '1s')
# ... 策略逻辑
你想想看,如果以后数据源从CSV换成数据库,或者换成实时行情,策略代码一行都不用改。这就是「分离」带来的好处。
注意:数据与逻辑分离,不代表数据模块可以随意设计。接口一定要稳定,一旦确定就不要轻易改。我曾经因为改了接口参数名,导致三个策略同时报错,改了一下午。
2.4 三个原则如何协同工作?
这三个原则不是孤立的。模块化是骨架,事件驱动是血脉,数据与逻辑分离是灵魂。我画了一张图,帮你理解它们的关系:
从图上你能看到:数据模块把清洗好的行情推给事件引擎,事件引擎分发给策略模块。策略模块生成信号后,交给执行模块模拟撮合。执行模块再把成交结果作为新事件,反馈给事件引擎。整个过程环环相扣,但每个模块都是独立的。
我建议你在搭建框架时,先画一张类似的图。把模块边界、事件流向、数据接口都标清楚。嗯,这一步花不了多少时间,但能帮你省下后面大量的调试时间。
2.5 避坑指南
最后,分享几个我踩过的坑:
- 模块间耦合太紧:我曾经把数据模块和策略模块写在同一个类里,结果想换数据源,得重写整个类。后来强制用接口隔离,才解决。
- 事件类型定义太粗:一开始我只定义了「行情事件」和「成交事件」,后来发现还需要「定时事件」「风控事件」。建议一开始就把事件类型规划好,留扩展余地。
- 数据格式不统一:不同数据源的时间戳格式、列名都不一样。我建议在数据模块入口处,统一转换成内部标准格式。别等到策略模块再去处理。
一句话总结:模块化让你改得动,事件驱动让你跑得快,数据与逻辑分离让你活得久。这三个原则,是做市策略回测框架的基石。
好了,这一章就聊到这儿。记住,框架设计不是一次性的工作,它会在你不断迭代策略的过程中,慢慢完善。别追求完美,先跑起来再说。