2. 回测系统架构设计:核心模块、事件驱动与数据流
做量化做市策略,最怕什么?
怕策略在实盘里跑得好好的,突然给你来个滑点爆仓。或者回测时收益曲线漂亮得像艺术品,一上实盘就变成灾难现场。
我见过太多人,花几个月写策略,结果回测系统本身就有bug。说白了,回测系统才是整个量化交易的地基。地基不稳,上面盖多高的楼都没用。
2.1 回测系统的核心模块
一个靠谱的回测系统,至少得包含这几个模块。我习惯把它们分成四块:
| 模块 | 职责 | 我踩过的坑 |
|---|---|---|
| 数据引擎 | 管理历史行情数据,提供时间序列切片 | 数据对齐没做好,导致买卖信号错位 |
| 策略引擎 | 执行做市逻辑,生成报价和订单 | 策略状态没重置,回测结果被污染 |
| 模拟撮合 | 模拟订单簿,处理成交与滑点 | 撮合逻辑太理想,忽略了排队延迟 |
| 风控与绩效 | 监控风险指标,计算收益曲线 | 风控阈值设得太松,回测里没暴露问题 |
这四个模块缺一不可。你想想看,数据引擎如果给错了时间戳,策略引擎再牛也是白搭。我曾经在一个项目里,因为数据引擎的时区没处理好,导致所有订单都提前了15分钟发出——回测结果漂亮得离谱,实盘直接亏到怀疑人生。
2.2 事件驱动架构:为什么非它不可?
做市策略和普通趋势策略不一样。趋势策略可能每天只交易几次,但做市策略每秒都在跟订单簿打交道。
这时候,事件驱动架构就派上用场了。
什么叫事件驱动?说白了就是:系统不主动轮询,而是等事情发生了再响应。比如:
- 新的tick数据来了 → 触发行情事件
- 订单被成交了 → 触发成交事件
- 风控阈值触发了 → 触发风控事件
我习惯把事件驱动架构画成下面这样:
看到这个图了吗?事件队列就像一个邮局,所有事件都先扔进去。事件循环就是邮递员,按顺序把信送到对应的处理器手里。
这样做的好处很明显:
- 解耦:数据源和策略逻辑互不依赖
- 可扩展:加一个新的事件类型,不影响现有代码
- 可回放:把历史数据变成事件流,回测和实盘用同一套代码
2.3 数据流设计:从tick到信号
做市策略的数据流,跟普通策略不太一样。普通策略可能只看1分钟K线,但做市策略需要看逐笔tick数据。
数据流大概是这样的链路:
- 原始tick数据 → 交易所推送的逐笔成交和报价
- 数据清洗 → 去重、补全、时间对齐
- 订单簿重建 → 根据tick数据实时构建买卖盘口
- 特征计算 → 价差、深度、订单不平衡等指标
- 策略信号 → 根据特征生成报价决策
这里有个关键点:订单簿重建。做市策略的核心就是看订单簿,你得知道买一卖一在哪里,深度够不够。
# 一个简化的订单簿重建逻辑
class OrderBook:
def __init__(self):
self.bids = {} # 价格 -> 数量
self.asks = {}
def update(self, tick):
# tick包含:价格、数量、买卖方向
if tick.side == 'buy':
self.bids[tick.price] = tick.volume
else:
self.asks[tick.price] = tick.volume
def get_spread(self):
best_bid = max(self.bids.keys())
best_ask = min(self.asks.keys())
return best_ask - best_bid
def get_mid_price(self):
return (best_bid + best_ask) / 2
嗯,这里要注意。订单簿重建的精度直接影响策略表现。我见过有人用1秒快照代替逐笔更新,结果价差计算偏差了30%以上。做市策略对价差极其敏感,差一个tick可能就是盈亏的分界线。
2.4 订单流设计:从决策到成交
订单流是回测系统的另一个核心。做市策略的订单流,跟普通策略最大的区别在于:做市策略是双向报价的。
也就是说,你同时挂买单和卖单,赚取价差。订单流大概是这样的:
- 报价决策 → 策略决定在什么价格挂单、挂多少
- 订单发送 → 把订单提交到模拟撮合引擎
- 撮合处理 → 模拟对手方是否吃掉你的订单
- 成交反馈 → 更新持仓和资金
- 撤单重报 → 如果市场变了,撤销旧单重新报价
这里有个容易忽略的点:撤单重报的频率。做市策略需要频繁调整报价,但撤单本身也有成本(交易所收撤单费,或者模拟时的时间成本)。
2.5 模拟撮合:最容易被低估的模块
模拟撮合是回测系统里最复杂的部分。很多人随便写个「价格到了就成交」,结果回测和实盘差距巨大。
做市策略的撮合,需要考虑:
| 因素 | 说明 | 我的建议 |
|---|---|---|
| 排队位置 | 你的订单在队列里排第几 | 按价格-时间优先级模拟 |
| 部分成交 | 对手方只吃掉你一部分订单 | 必须支持,否则高估成交率 |
| 滑点模型 | 成交价格和报价之间的偏差 | 用历史数据拟合滑点分布 |
| 冰山订单 | 大单隐藏真实数量 | 至少支持基础版本 |
我个人习惯用「事件驱动+状态机」来实现撮合。每个订单都是一个状态机:
订单状态流转:
NEW → PARTIALLY_FILLED → FILLED
→ CANCELLED
→ REJECTED
每次tick进来,撮合引擎遍历所有活跃订单,检查是否满足成交条件。满足就更新状态,生成成交事件。
你想想看,如果撮合逻辑太简单,比如假设所有订单都能立即成交,那回测出来的夏普比率可能比实盘高出一倍。我见过有人因为这个原因,实盘直接亏了30%才发现问题。
2.6 数据流与订单流的协同
最后,把数据流和订单流串起来看:
看到这个闭环了吗?数据流从左到右进入策略,策略生成订单流向右进入撮合,撮合结果再反馈回策略。这个闭环必须完整,缺一环都不行。
我见过最离谱的回测系统,数据流和订单流是分开跑的——数据流跑完了再跑订单流。结果就是策略明明应该根据最新行情调整报价,但回测里报价是固定的,完全失真。
好了,以上就是回测系统架构设计的核心内容。说白了,就是四个模块、一个事件循环、两条流。把这套框架搭好了,后面的策略实现就是水到渠成的事。
公众号:蓝海资料掘金营,微信deep3321