24、订单流与回测框架:构建基于订单流数据的回测系统
做量化交易这些年,我踩过最大的坑是什么?
不是策略亏钱,而是回测看着赚翻了,实盘一跑就崩。尤其是做订单流策略,这个问题更突出。为什么?因为订单流数据太细了,细到毫秒级,细到每一笔挂单和成交。你用日线回测跑得飞起,换成订单流数据,回测系统直接卡死。
今天我们就来聊聊,怎么搭建一个真正能用的订单流回测系统。嗯,这里要注意,不是那种「玩具级」的,而是能支撑你跑完一年数据、验证几十个参数组合的实战系统。
24.1 订单流回测的核心挑战
先说说难点在哪。我个人习惯把订单流回测的挑战归纳为三点:
- 数据量大:一天的数据量可能是日线的几万倍。你想想看,光是沪深300的逐笔成交,一天就几百万条。
- 时间精度高:订单流是毫秒级的,回测引擎必须能处理亚秒级的事件。
- 撮合逻辑复杂:你不能简单用「开盘价买入、收盘价卖出」这种逻辑。订单流策略往往涉及限价单、市价单、冰山订单等。
核心观点:订单流回测的本质,是「事件驱动」而非「K线驱动」。每一笔订单、每一次成交都是一个事件,你的回测引擎必须能按时间顺序处理这些事件。
24.2 回测系统的架构设计
我在项目中遇到过最头疼的事,就是回测系统写得太「重」,改一个参数要跑半小时。后来我学乖了,用分层架构来设计。
下面这张图是我现在常用的架构,说白了就是三层:数据层、引擎层、策略层。
你看,数据层在最底下,负责把原始订单流数据变成引擎能吃的格式。引擎层是核心,它模拟真实的交易所撮合逻辑。策略层在最上面,只管算信号、下订单,不用管底层细节。
24.3 核心代码实现
光说不练假把式。我们直接看代码。下面是一个简化版的事件驱动回测引擎骨架。
import pandas as pd
import numpy as np
from collections import deque
from dataclasses import dataclass
from typing import List, Dict, Optional
@dataclass
class OrderEvent:
"""订单事件"""
timestamp: int
symbol: str
side: str # 'buy' or 'sell'
price: float
volume: int
order_type: str # 'limit' or 'market'
@dataclass
class FillEvent:
"""成交事件"""
timestamp: int
symbol: str
side: str
price: float
volume: int
commission: float
class OrderBookSimulator:
"""订单簿模拟器"""
def __init__(self):
self.bids = [] # 买单队列
self.asks = [] # 卖单队列
def update(self, order: OrderEvent):
"""更新订单簿"""
if order.side == 'buy':
self.bids.append(order)
self.bids.sort(key=lambda x: x.price, reverse=True)
else:
self.asks.append(order)
self.asks.sort(key=lambda x: x.price)
def match(self) -> List[FillEvent]:
"""撮合订单"""
fills = []
while self.bids and self.asks:
best_bid = self.bids[0]
best_ask = self.asks[0]
if best_bid.price >= best_ask.price:
# 成交
volume = min(best_bid.volume, best_ask.volume)
price = best_ask.price # 以卖一价成交
fills.append(FillEvent(
timestamp=max(best_bid.timestamp, best_ask.timestamp),
symbol=best_bid.symbol,
side='buy',
price=price,
volume=volume,
commission=volume * price * 0.0003
))
# 更新剩余量
best_bid.volume -= volume
best_ask.volume -= volume
if best_bid.volume == 0:
self.bids.pop(0)
if best_ask.volume == 0:
self.asks.pop(0)
else:
break
return fills
class BacktestEngine:
"""回测引擎"""
def __init__(self, initial_capital: float = 1000000):
self.capital = initial_capital
self.positions = {}
self.order_book = OrderBookSimulator()
self.events = deque()
self.trades = []
def add_data(self, data: pd.DataFrame):
"""加载订单流数据"""
for _, row in data.iterrows():
event = OrderEvent(
timestamp=row['timestamp'],
symbol=row['symbol'],
side=row['side'],
price=row['price'],
volume=row['volume'],
order_type=row['order_type']
)
self.events.append(event)
def run(self, strategy):
"""运行回测"""
while self.events:
event = self.events.popleft()
# 更新订单簿
self.order_book.update(event)
# 撮合
fills = self.order_book.match()
for fill in fills:
self._process_fill(fill)
# 调用策略
strategy.on_event(event, self)
def _process_fill(self, fill: FillEvent):
"""处理成交"""
self.trades.append(fill)
if fill.side == 'buy':
self.positions[fill.symbol] = self.positions.get(fill.symbol, 0) + fill.volume
self.capital -= fill.price * fill.volume + fill.commission
else:
self.positions[fill.symbol] = self.positions.get(fill.symbol, 0) - fill.volume
self.capital += fill.price * fill.volume - fill.commission
个人经验:这个引擎虽然简单,但核心逻辑都在了。我在实际项目中,会在这个基础上加一个「时间窗口」机制。因为订单流数据太密了,如果每笔都触发策略,计算量扛不住。我一般会设一个10毫秒的窗口,窗口内的订单合并处理。
24.4 订单流特征计算
有了回测引擎,接下来就是算订单流特征了。常用的特征包括:
| 特征名称 | 计算公式 | 含义 |
|---|---|---|
| Delta | 主动买量 - 主动卖量 | 衡量买卖力量对比 |
| CVD (累计Delta) | 累计Delta值 | 趋势强度指标 |
| 订单簿不平衡 | (买一量 - 卖一量) / (买一量 + 卖一量) | 短期供需失衡 |
| 大单占比 | 大单成交量 / 总成交量 | 主力资金参与度 |
| 成交速度 | 单位时间成交笔数 | 市场活跃度 |
避坑指南:我曾经犯过一个错误,就是直接用原始Delta值做信号。后来发现,不同股票的Delta值量级差很多,茅台和工商银行的Delta完全不是一个数量级。一定要做标准化处理,或者用Z-score。
24.5 多周期共振的实现
订单流回测里,多周期共振怎么搞?我建议用「分层计算」的思路。
举个例子:
- Tick级:计算实时Delta,判断当前1秒内的买卖力量
- 1分钟级:计算CVD,看趋势方向
- 5分钟级:计算订单簿不平衡,看大资金意图
当三个周期同时发出买入信号时,才开仓。这样能过滤掉很多噪音。
class MultiTimeframeSignal:
"""多周期信号计算"""
def __init__(self):
self.tick_delta = 0
self.min1_cvd = 0
self.min5_imbalance = 0
self.tick_window = []
self.min1_window = []
self.min5_window = []
def update(self, delta: float, imbalance: float, timestamp: int):
"""更新各周期数据"""
# Tick级:直接累加
self.tick_delta += delta
# 1分钟级:滚动窗口
self.min1_window.append((timestamp, delta))
self.min1_window = [x for x in self.min1_window if timestamp - x[0] < 60000]
self.min1_cvd = sum(x[1] for x in self.min1_window)
# 5分钟级
self.min5_window.append((timestamp, imbalance))
self.min5_window = [x for x in self.min5_window if timestamp - x[0] < 300000]
self.min5_imbalance = np.mean([x[1] for x in self.min5_window])
def get_signal(self) -> int:
"""返回信号:1买入,-1卖出,0观望"""
if (self.tick_delta > 0 and
self.min1_cvd > 0 and
self.min5_imbalance > 0.3):
return 1
elif (self.tick_delta < 0 and
self.min1_cvd < 0 and
self.min5_imbalance < -0.3):
return -1
return 0
24.6 回测结果的评估
跑完回测,别只看收益率。订单流策略的评估指标和传统策略不太一样。我个人习惯看这几个:
- 胜率 vs 盈亏比:订单流策略胜率通常不高,但盈亏比要做上去
- 最大回撤:尤其是日内回撤,订单流策略容易在震荡市连续亏损
- 交易频率:一天交易多少次?太频繁了手续费吃不消
- 滑点影响:订单流策略对滑点极其敏感,一定要做滑点敏感性测试
关键提醒:回测只是起点,不是终点。我见过太多人回测跑出漂亮曲线,实盘一周就亏回去。订单流策略尤其如此,因为你的回测数据是历史数据,而实盘的市场微观结构随时在变。一定要做「样本外测试」和「参数稳定性测试」。
好了,关于订单流回测框架的核心内容就这些。记住,好的回测系统不是功能越多越好,而是「够用、稳定、可扩展」。从简单的开始,慢慢迭代,比一开始就想搞个大而全的系统靠谱得多。