3、回测系统搭建:回测框架选型、数据准备、模拟撮合引擎设计

好,咱们进入第三章。回测系统,说白了就是你的策略实验室。没有它,你所有的参数调优都是闭着眼睛开车。我个人习惯把回测系统拆成三块:框架选型、数据准备、撮合引擎。一块一块来聊。

3.1 回测框架选型:别盲目追新,合适最重要

市面上回测框架不少,我这些年用过不下七八个。踩过坑,也捡过宝。选框架这事,没有银弹。

主流框架对比

框架 语言 适合场景 我的评价
Backtrader Python 股票、期货、简单策略 入门快,文档全,但性能一般
Zipline Python A股、美股、学术研究 Quantopian遗产,生态好但维护慢
vn.py Python 期货、CTA、实盘对接 国内最强,但学习曲线陡
自研框架 任意 高频、复杂撮合逻辑 灵活但开发成本高

我个人建议:做市商策略,优先考虑自研或基于vn.py二次开发。为什么?因为市商策略对撮合逻辑的精度要求极高,通用框架往往简化了订单簿和成交细节。

我的经验: 之前用Backtrader跑一个简单的价差策略,回测年化20%,实盘直接亏。后来发现是框架默认用了「收盘价成交」,而市商策略实际成交价和订单簿深度强相关。从那以后,我对通用框架的撮合引擎就多留了个心眼。

3.2 数据准备:垃圾进,垃圾出

回测的命根子就是数据。数据不对,后面全是白费功夫。我见过太多人花三个月调参数,最后发现数据里有个跳空缺口没处理。

数据源选择

  • 交易所官方数据:最可靠,但获取门槛高,通常需要付费或API权限
  • 第三方数据商:如Wind、聚宽、Tushare,覆盖全但可能有延迟
  • 自建爬虫:灵活但维护成本高,容易被封

数据清洗要点

嗯,这里要注意几个坑:

  1. 缺失值处理:不要简单填充。我曾经用前值填充,结果把一次熔断行情给填平了,回测结果虚高。
  2. 复权处理:做市商策略建议用后复权,因为前复权会扭曲历史价格。
  3. 时间对齐:不同交易所的撮合时间不同,比如币圈是7x24,A股有午休。对齐不好,你的买卖信号会错位。
避坑指南: 我曾经在回测一个BTC市商策略时,用了某数据商的1分钟K线。结果发现他们的时间戳是UTC+0,而我的策略逻辑是按UTC+8跑的。整整三天,我都在纳闷为什么策略在凌晨两点疯狂开仓。后来一查,原来是时区没对齐。

3.3 模拟撮合引擎设计:核心中的核心

撮合引擎,说白了就是模拟「你的订单怎么被市场吃掉」。市商策略的成败,很大程度上取决于撮合引擎的逼真程度。

三种撮合模式

模式 原理 适用场景 缺点
Tick级撮合 逐笔成交,完全复刻订单簿 高频、做市商策略 数据量大,回测慢
K线级撮合 用OHLCV模拟成交 中低频、趋势策略 精度低,忽略滑点
事件驱动撮合 按订单簿快照模拟 中高频、兼顾精度和速度 实现复杂

我个人强烈建议:做市商策略至少用事件驱动撮合。Tick级虽然最准,但数据量太大,回测一次可能要跑几个小时。事件驱动能在精度和速度之间取得平衡。

核心代码示例:一个简化的订单簿撮合

class OrderBook:
    def __init__(self):
        self.bids = []  # 买单队列,按价格降序
        self.asks = []  # 卖单队列,按价格升序
    
    def match_order(self, order):
        """模拟订单簿撮合"""
        if order.side == 'buy':
            # 吃卖单
            while order.remaining > 0 and self.asks:
                best_ask = self.asks[0]
                if order.price < best_ask.price:
                    break  # 价格不够,吃不动了
                trade_qty = min(order.remaining, best_ask.quantity)
                trade_price = best_ask.price
                # 记录成交
                self.record_trade(order, best_ask, trade_qty, trade_price)
                order.remaining -= trade_qty
                best_ask.quantity -= trade_qty
                if best_ask.quantity == 0:
                    self.asks.pop(0)
        else:
            # 吃买单,逻辑对称
            pass
注意: 上面的代码是简化版。实际生产中,你还要考虑冰山订单、部分成交、撤单重入等场景。我见过一个团队,因为没处理冰山订单,回测结果比实盘高了30%。

滑点模型

做市商策略最怕的就是滑点。你想想看,你挂单在买一,结果一个大单砸下来,你的单子被吃掉了,但成交价可能比预期差了好几个tick。

我常用的滑点模型有两种:

  • 固定滑点:每笔成交加固定tick数,简单粗暴
  • 流动性滑点:根据订单簿深度动态计算,更真实

举个例子:如果订单簿买一有100个币,卖一有50个币。你下了一个80个币的卖单,那前50个币按卖一成交,后30个币就要吃卖二甚至卖三的价。这个滑点就是动态的。

我的习惯: 回测时我会同时跑两种滑点模型。如果固定滑点和流动性滑点的结果差异超过10%,说明策略对流动性敏感,实盘时要格外小心。

3.4 回测系统的验证与调试

系统搭完了,别急着跑参数。先做一件事:回测复现

找一段历史行情,手动算一笔交易的成交价和滑点,然后跟回测系统的输出对比。如果对不上,说明你的撮合引擎或者数据有问题。

我曾经花了一周时间,发现回测系统里一个浮点数精度问题——Python的float在累加时会有微小误差,导致订单簿的累计数量差了0.0001个币。这个误差在单笔交易里可以忽略,但跑100万笔交易后,误差累积到足以影响策略评价。

解决方案?用Decimal代替float,或者用整数表示价格和数量(比如用最小tick的整数倍)。

好了,回测系统搭建就聊这么多。下一章咱们进入参数调优的核心——如何设计你的参数搜索空间。到时候我会分享一个我压箱底的「网格搜索+贝叶斯优化」混合策略,保证让你眼前一亮。