27、订单流回测框架:构建订单流回测系统

做量化交易这些年,我踩过最大的坑,就是回测时跑得飞起,实盘时直接翻车。尤其是订单流策略——你想想看,它依赖的是逐笔成交和盘口数据,这些数据在回测里是静态的,但实盘里每一笔都有延迟和滑点。所以今天咱们就来聊聊,怎么搭一个靠谱的订单流回测系统。

为什么订单流回测这么特殊?

普通的K线回测,你只需要处理OHLC。但订单流回测,你得模拟每一笔订单的成交过程。说白了,就是要把「订单簿重建」和「撮合逻辑」都搬进回测引擎里。

我在项目中遇到过最典型的问题:回测时用tick数据做策略,收益曲线漂亮得不行。结果一上实盘,因为网络延迟和交易所撮合速度,策略直接变成反向指标。嗯,从那以后我学乖了——回测里必须加入滑点和延迟模型。

回测系统的核心架构

先画个图,看看整个框架长什么样:

原始订单流数据 数据清洗与对齐 去重、补全、时间戳对齐 回测引擎 订单簿重建模块 撮合引擎 滑点/延迟模拟 策略逻辑(信号生成 + 订单管理) 绩效评估(夏普、最大回撤、成交率)

订单簿重建:回测的基石

回测的第一步,就是把历史订单流数据还原成当时的盘口状态。我习惯用快照+增量更新的方式:

class OrderBookReconstructor:
    def __init__(self):
        self.bids = {}  # 价格 -> 数量
        self.asks = {}
        self.last_snapshot = None
    
    def apply_snapshot(self, snapshot):
        """应用一个快照,重置订单簿"""
        self.bids = {level.price: level.qty for level in snapshot.bids}
        self.asks = {level.price: level.qty for level in snapshot.asks}
        self.last_snapshot = snapshot.timestamp
    
    def apply_delta(self, delta):
        """应用增量更新"""
        for change in delta.changes:
            if change.side == 'bid':
                if change.qty == 0:
                    self.bids.pop(change.price, None)
                else:
                    self.bids[change.price] = change.qty
            else:
                if change.qty == 0:
                    self.asks.pop(change.price, None)
                else:
                    self.asks[change.price] = change.qty
    
    def get_top_n(self, n=5):
        """获取前n档盘口"""
        sorted_bids = sorted(self.bids.items(), reverse=True)[:n]
        sorted_asks = sorted(self.asks.items())[:n]
        return sorted_bids, sorted_asks
注意: 数据对齐是个大坑。不同交易所的时间戳精度不一样,有的到毫秒,有的到微秒。我曾经因为没对齐时间戳,导致订单簿重建后盘口价格差了整整一档,回测结果完全失真。

滑点模型:别让回测骗了你

订单流策略对滑点特别敏感。你想想看,策略信号往往基于盘口挂单的微小变化,如果滑点吃掉了一两个tick,信号可能就完全变了。

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

模型类型 适用场景 实现方式
固定滑点 流动性好的品种 成交价 = 信号价 ± 固定tick数
比例滑点 波动较大的品种 成交价 = 信号价 × (1 ± 滑点比例)
流动性滑点 订单流策略专用 根据盘口深度动态计算

我个人最推荐第三种——流动性滑点。它模拟的是真实场景:你下了一笔市价单,系统会按盘口挂单逐档吃掉。比如你想买10手,卖一只有5手,那剩下的5手就得用卖二的价格成交。

def simulate_market_order(order_book, side, qty):
    """模拟市价单成交,返回成交明细"""
    fills = []
    remaining = qty
    
    if side == 'buy':
        levels = sorted(order_book.asks.items())
    else:
        levels = sorted(order_book.bids.items(), reverse=True)
    
    for price, level_qty in levels:
        if remaining <= 0:
            break
        trade_qty = min(remaining, level_qty)
        fills.append({
            'price': price,
            'qty': trade_qty,
            'timestamp': current_time
        })
        remaining -= trade_qty
    
    # 如果还有剩余,说明流动性不足
    if remaining > 0:
        # 这里可以触发滑点惩罚
        fills.append({
            'price': price * 1.01 if side == 'buy' else price * 0.99,
            'qty': remaining,
            'timestamp': current_time,
            'slippage': True
        })
    
    return fills

延迟模型:模拟真实网络环境

回测里订单是瞬间成交的,但实盘不是。信号生成、订单发送、交易所确认,每一步都有延迟。我建议至少模拟两种延迟:

  • 固定延迟:比如统一加50ms,模拟网络传输时间
  • 随机延迟:服从正态分布,模拟网络抖动

核心经验: 延迟对订单流策略的影响比滑点更大。因为订单流信号往往在几毫秒内就失效了,你晚了一拍,看到的盘口已经是另一番景象。

import random
import time

class DelaySimulator:
    def __init__(self, base_delay_ms=50, jitter_ms=20):
        self.base_delay = base_delay_ms / 1000.0
        self.jitter = jitter_ms / 1000.0
    
    def apply_delay(self):
        """模拟网络延迟"""
        delay = self.base_delay + random.gauss(0, self.jitter)
        delay = max(0, delay)  # 延迟不能为负
        time.sleep(delay)
    
    def get_delayed_timestamp(self, original_ts):
        """返回延迟后的时间戳"""
        delay = self.base_delay + random.gauss(0, self.jitter)
        return original_ts + max(0, delay)

完整的回测引擎实现

把上面这些模块拼起来,就是一个能用的回测引擎了。我习惯用事件驱动的方式:

class OrderFlowBacktestEngine:
    def __init__(self, data, strategy, slippage_model='liquidity'):
        self.data = data  # 订单流数据
        self.strategy = strategy
        self.order_book = OrderBookReconstructor()
        self.delay = DelaySimulator()
        self.performance = {'trades': [], 'pnl': 0}
    
    def run(self):
        for event in self.data:
            # 1. 更新订单簿
            if event.type == 'snapshot':
                self.order_book.apply_snapshot(event)
            elif event.type == 'delta':
                self.order_book.apply_delta(event)
            
            # 2. 生成信号(考虑延迟)
            delayed_ts = self.delay.get_delayed_timestamp(event.timestamp)
            signal = self.strategy.on_tick(self.order_book, delayed_ts)
            
            # 3. 执行交易(考虑滑点)
            if signal:
                fills = simulate_market_order(
                    self.order_book, 
                    signal.side, 
                    signal.qty
                )
                self.performance['trades'].extend(fills)
                self.performance['pnl'] += self.calc_pnl(fills)
        
        return self.performance
    
    def calc_pnl(self, fills):
        """计算一笔成交的盈亏"""
        # 这里根据你的策略逻辑实现
        pass

回测中的常见陷阱

避坑指南:

  • 我曾经因为用了未来数据,回测夏普高达5.0,实盘直接亏到怀疑人生。检查一下你的信号是否用到了「未来」的盘口数据。
  • 订单流数据量很大,一天可能几百万条。回测时记得用迭代器,别一次性全加载到内存里。
  • 不同交易所的订单流格式不同,建议统一转成标准格式再进回测引擎。

嗯,回测框架搭好了,但别急着跑策略。先拿一段历史数据验证一下订单簿重建的准确性——对比一下重建后的盘口和实际快照,误差应该在1个tick以内。如果偏差太大,八成是数据对齐出了问题。

最后说一句:回测只是起点,不是终点。再完美的回测,也替代不了小资金实盘验证。我习惯先跑一个月的历史回测,再用模拟盘跑两周,最后才敢上实盘。稳一点,总没错。


无相订单流研究社 微信Lucian808555