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以内。如果偏差太大,八成是数据对齐出了问题。
最后说一句:回测只是起点,不是终点。再完美的回测,也替代不了小资金实盘验证。我习惯先跑一个月的历史回测,再用模拟盘跑两周,最后才敢上实盘。稳一点,总没错。
交易系统化学习资料 微信Strategy888888