做市策略回测框架设计原则

做市策略的回测,说白了就是拿历史数据模拟交易,看看你的策略到底能不能赚钱。我做了这么多年量化,见过太多人一上来就写回测代码,结果改个参数要改半天,换个品种直接崩掉。嗯,这里面的坑,我基本都踩过。

今天我们就聊聊回测框架的设计原则。我个人习惯是,先把框架搭稳了,再往里填策略。你想想看,地基没打好,楼盖得再高也是危房。

核心原则一句话总结:模块化让你改得动,事件驱动让你跑得准,数据与策略分离让你换得爽,性能考量让你等得起。

一、模块化设计

模块化,说白了就是把一个复杂系统拆成一个个独立的小零件。每个零件只管自己的事,互不干扰。

为什么要模块化?

  • 好维护:改一个模块,不影响其他模块。我曾经因为耦合太紧,改了个风控逻辑,结果把订单管理模块搞崩了,排查了两天...
  • 好复用:写好的模块,换个策略照样用。比如数据加载模块,做市策略用,趋势策略也能用。
  • 好测试:每个模块单独测,出问题定位快。

常见的模块划分:

模块名称 职责 典型接口
数据模块 加载、清洗、对齐行情数据 load_data(symbol, start, end)
策略模块 根据行情生成买卖信号 on_tick(tick) -> signal
风控模块 检查订单是否合规 check_order(order) -> bool
订单管理模块 执行订单、管理持仓 execute_order(signal) -> trade
绩效模块 计算收益、夏普、最大回撤等 calculate_pnl(trades) -> metrics

我的小技巧:每个模块都定义好接口协议,用抽象类或者协议类来约束。这样换模块就像换乐高积木,插上就能用。

二、事件驱动架构

做市策略和普通策略不一样。普通策略可能每分钟算一次信号,但做市策略是逐笔驱动的——每来一个tick,你都得马上反应。

事件驱动是什么?

简单说,就是「发生什么事,就触发什么逻辑」。比如:

  • 来了一个新tick → 更新订单簿 → 检查是否需要调整报价
  • 成交了一笔 → 更新持仓 → 检查风险指标
  • 到了收盘时间 → 计算当日绩效 → 生成报告

我刚开始做回测时,用的是循环轮询的方式——每秒钟查一次有没有新数据。结果呢?性能差不说,还容易漏掉关键事件。后来改成事件驱动,整个框架清爽多了。

事件驱动的核心组件:

  • 事件队列:存放所有待处理的事件,按时间排序
  • 事件处理器:根据事件类型,调用对应的处理函数
  • 事件源:产生事件的地方,比如数据加载器、成交回报
# 一个简化的事件驱动回测引擎
class EventEngine:
    def __init__(self):
        self.queue = []
        self.handlers = {}
    
    def register_handler(self, event_type, handler):
        self.handlers[event_type] = handler
    
    def push_event(self, event):
        self.queue.append(event)
        self.queue.sort(key=lambda e: e.timestamp)  # 按时间排序
    
    def run(self):
        while self.queue:
            event = self.queue.pop(0)
            handler = self.handlers.get(event.type)
            if handler:
                handler(event)

注意:事件队列一定要按时间排序!我曾经因为排序逻辑写错了,导致回测结果完全不对,查了三天才发现是事件顺序乱了。血的教训。

三、数据与策略分离

这个原则,说白了就是「数据是数据,策略是策略,别混在一起」。

为什么必须分离?

  • 数据格式会变:今天用CSV,明天可能用数据库,后天可能用API。策略不应该关心数据从哪来。
  • 策略逻辑会变:同一个数据,你可能想试不同的策略。如果耦合在一起,每次换策略都得改数据代码。
  • 方便回测和实盘切换:回测用历史数据,实盘用实时数据,策略代码完全不用改。

怎么实现分离?

我习惯用「数据适配器」模式。数据模块提供一个统一的接口,不管底层数据源是什么,上层策略看到的都是一样的格式。

# 数据适配器示例
class DataAdapter:
    def get_tick(self, symbol, timestamp):
        """返回统一的Tick格式"""
        pass
    
    def get_orderbook(self, symbol, timestamp):
        """返回统一的订单簿格式"""
        pass

# CSV数据源
class CSVDataAdapter(DataAdapter):
    def get_tick(self, symbol, timestamp):
        # 从CSV文件读取
        pass

# 数据库数据源
class DBDataAdapter(DataAdapter):
    def get_tick(self, symbol, timestamp):
        # 从数据库读取
        pass

我的经验:数据接口定义得越通用越好。比如tick数据,至少包含:时间戳、买一价、买一量、卖一价、卖一量、最新成交价。这样大部分策略都能用。

四、性能考量

做市策略的回测,数据量通常很大。一天可能有几百万个tick,一年就是几亿条。性能不行,回测一次等半天,谁受得了?

性能瓶颈在哪?

  • I/O瓶颈:从磁盘读数据太慢
  • 计算瓶颈:每个tick都要算一遍信号,计算量很大
  • 内存瓶颈:数据量太大,内存装不下

优化思路:

优化方向 具体做法 效果
数据加载 使用二进制格式(如Parquet)代替CSV 加载速度提升10倍以上
计算加速 使用NumPy向量化计算,避免逐行循环 计算速度提升100倍
内存管理 分块加载数据,处理完一块释放一块 内存占用降低80%
并行处理 多品种回测时,用多进程并行 时间缩短到1/N

避坑指南:我曾经为了追求性能,把所有数据都加载到内存里。结果回测到一半,内存爆了,程序直接崩溃。后来改用分块加载,虽然代码复杂了点,但再也没出过问题。

知识体系总览

下面这张图,把四个设计原则的关系画出来了。你仔细看看,其实它们是环环相扣的。

做市策略回测框架设计原则 模块化设计 拆成独立小零件 好维护、好复用、好测试 数据/策略/风控/订单/绩效 → 改得动 事件驱动架构 逐笔触发,实时响应 事件队列 + 事件处理器 按时间排序,顺序执行 → 跑得准 数据与策略分离 数据适配器模式 统一接口,屏蔽数据源差异 回测实盘无缝切换 → 换得爽 性能考量 二进制格式 + 向量化计算 分块加载 + 并行处理 解决I/O/计算/内存瓶颈 → 等得起 四个原则缺一不可,共同构成健壮的回测框架

这四个原则,说白了就是一套方法论。模块化让你改得动,事件驱动让你跑得准,数据与策略分离让你换得爽,性能考量让你等得起。缺一个,你的回测框架就会出问题。

我个人习惯是,先按模块化把架子搭好,然后实现事件驱动引擎,再把数据接口抽象出来,最后做性能优化。这个顺序,我试过很多次,是最顺的。

记住:回测框架不是一次性写好的,而是不断迭代出来的。先跑起来,再优化,这是我一贯的做法。


公众号:蓝海资料掘金营,微信deep3321