3. 市场数据事件模型:Tick数据、Bar数据、订单簿快照的事件定义与数据结构
在事件驱动回测框架中,市场数据是驱动策略逻辑运转的“燃料”。不同的数据粒度对应不同的事件类型,其数据结构直接影响策略的计算效率与信号精度。本章将系统定义三种核心市场数据事件:Tick、Bar 与 订单簿快照,并给出标准化的数据结构与事件触发规则。
3.1 事件模型总览
市场数据事件模型遵循“生产者-消费者”模式:数据源(交易所、数据文件)将原始行情封装为事件对象,推入事件队列;策略模块通过订阅特定事件类型,在事件循环中消费并响应。下表对比了三种事件的核心特征:
| 事件类型 | 数据粒度 | 触发频率 | 典型用途 | 数据量级 |
|---|---|---|---|---|
| TickEvent | 逐笔成交 | 毫秒级/随机 | 高频策略、盘口分析 | 极大 |
| BarEvent | 聚合K线 | 固定时间间隔 | 趋势跟踪、均值回归 | 中等 |
| OrderBookEvent | 深度快照 | 事件驱动/定时 | 流动性分析、订单簿冲击 | 大 |
设计原则:所有市场数据事件应继承自同一个基类
MarketEvent,包含 timestamp、symbol、event_type 三个公共字段,便于事件循环统一调度。
3.2 Tick 数据事件
Tick 数据是市场交易的最小粒度记录,每一条 Tick 代表一笔真实的成交或一次报价变动。在回测中,TickEvent 通常由逐笔成交数据生成,用于模拟最真实的交易环境。
3.2.1 数据结构定义
class TickEvent:
def __init__(self, timestamp, symbol, last_price, volume,
bid_price, ask_price, bid_size, ask_size):
self.event_type = 'TICK'
self.timestamp = timestamp # datetime 对象
self.symbol = symbol # 合约代码
self.last_price = last_price # 最新成交价
self.volume = volume # 该笔成交量
self.bid_price = bid_price # 当前买一价
self.ask_price = ask_price # 当前卖一价
self.bid_size = bid_size # 买一挂单量
self.ask_size = ask_size # 卖一挂单量
3.2.2 事件触发规则
- 逐笔触发:每读取一条原始 Tick 记录,立即生成一个 TickEvent 并推入队列。
- 时间戳对齐:使用交易所原始时间戳,回测引擎不做插值或平滑。
- 异常处理:对于价格为零、买卖价差为负等异常 Tick,应标记为无效事件并跳过。
实战注意:使用 Tick 数据回测时,务必考虑交易成本与滑点。Tick 级别的模拟可以精确计算冲击成本,但数据量巨大,建议对回测周期进行分段处理。
3.3 Bar 数据事件
Bar 数据是将一段时间内的 Tick 数据聚合而成的统计摘要,是量化策略中最常用的数据形态。BarEvent 支持多种时间周期(1分钟、5分钟、日线等),其数据结构需包含开盘、最高、最低、收盘、成交量等核心字段。
3.3.1 数据结构定义
class BarEvent:
def __init__(self, timestamp, symbol, open_price, high_price,
low_price, close_price, volume, vwap=None):
self.event_type = 'BAR'
self.timestamp = timestamp # Bar 的结束时间
self.symbol = symbol
self.open = open_price
self.high = high_price
self.low = low_price
self.close = close_price
self.volume = volume
self.vwap = vwap # 成交量加权均价(可选)
3.3.2 聚合方式对比
| 聚合方式 | 触发条件 | 优点 | 缺点 |
|---|---|---|---|
| 时间周期 | 固定时间间隔(如60秒) | 实现简单,与策略逻辑对齐 | 交易稀疏时段数据失真 |
| 成交量周期 | 累计成交量达到阈值 | 反映真实交易活跃度 | 时间不固定,策略逻辑复杂 |
| Tick 数周期 | 累计 Tick 笔数达到阈值 | 适合高频统计 | 受 Tick 频率波动影响 |
3.4 订单簿快照事件
订单簿快照(Order Book Snapshot)记录了某一时刻市场所有限价订单的分布情况,包括买盘和卖盘的多档价格与挂单量。OrderBookEvent 是构建高级交易策略(如做市、套利)的基础数据源。
3.4.1 数据结构定义
class OrderBookEvent:
def __init__(self, timestamp, symbol, bids, asks):
self.event_type = 'ORDER_BOOK'
self.timestamp = timestamp # 快照时间
self.symbol = symbol
self.bids = bids # 列表,每项为 (price, size)
self.asks = asks # 列表,每项为 (price, size)
# 示例:bids = [(100.5, 200), (100.4, 500), ...]
# 示例:asks = [(100.6, 300), (100.7, 400), ...]
3.4.2 快照 vs 增量更新
- 全量快照:每次事件包含完整的买卖盘口数据,数据冗余但处理简单,适合回测场景。
- 增量更新:只记录相对于上一次快照的变化(新增、删除、修改),节省存储空间,但需要维护状态机。
回测限制:大多数回测框架仅支持全量快照模式。若使用增量更新,需确保状态机在回测启动时从第一个全量快照开始重建,否则会导致盘口数据错乱。
3.5 事件模型架构图
下图展示了三种市场数据事件在回测引擎中的流转关系:数据源经过解析器生成事件对象,事件队列统一调度,策略模块通过事件类型订阅消费。
3.6 事件订阅与分发机制
为了实现灵活的策略组合,事件模型需要支持多策略订阅与选择性消费。以下是一个简化的事件分发流程:
- 注册:策略在初始化时向事件总线注册感兴趣的事件类型(如
subscribe('TICK', handler))。 - 分发:事件循环从队列中取出事件,根据
event_type查找所有已注册的回调函数,依次调用。 - 过滤:策略可在回调函数内部根据
symbol字段进一步过滤,只处理自己关注的合约。 - 性能优化:对于高频 Tick 事件,建议使用
symbol级别的预过滤,避免不必要的函数调用开销。
代码示例:事件订阅与分发核心逻辑
class EventBus:
def __init__(self):
self.subscribers = {} # event_type -> [handler1, handler2, ...]
def subscribe(self, event_type, handler):
if event_type not in self.subscribers:
self.subscribers[event_type] = []
self.subscribers[event_type].append(handler)
def publish(self, event):
handlers = self.subscribers.get(event.event_type, [])
for handler in handlers:
handler(event)
3.7 数据质量与异常处理
市场数据在采集、传输、存储过程中可能引入各种异常。回测框架必须对数据事件进行严格校验,避免“垃圾进,垃圾出”。
| 异常类型 | 典型表现 | 处理策略 |
|---|---|---|
| 价格异常 | 价格为0、负数或超出合理范围 | 丢弃该事件,记录日志 |
| 时间戳乱序 | 后一个事件的时间戳早于前一个 | 按时间戳排序后重放,或丢弃乱序事件 |
| 买卖价差为负 | bid_price > ask_price | 标记为无效,不用于策略计算 |
| 成交量异常 | 单笔成交量超过市场合理阈值 | 截断或丢弃,根据交易所规则处理 |
| 数据缺失 | 某段时间内无任何事件 | 策略应具备空值处理逻辑,避免除零错误 |
重要提示:回测中使用的数据应尽量与实盘数据源保持一致。如果回测数据经过了清洗或插值,务必在策略文档中说明,否则回测结果可能产生误导。