3. 市场微观结构与数据模型:订单簿原理、Tick级与Bar级数据、Level2与Level3数据模型设计
做高频交易,说白了就是在跟市场最底层的「呼吸」打交道。你想想看,K线图上的每一根柱子,背后都是成千上万笔订单的博弈。我刚开始接触这个领域时,总觉得策略能赚钱就行,管它底层数据长什么样。直到有一次,我的策略在模拟盘上跑得风生水起,一上实盘就亏成狗……后来才发现,问题出在我对订单簿的理解太肤浅了。
嗯,今天我们就来聊聊市场微观结构。这部分内容,是搭建回测系统的地基。地基不稳,楼盖得再高也得塌。
3.1 订单簿(Order Book)原理
订单簿是什么?说白了,它就是交易所维护的一个「实时挂单账本」。所有想买和想卖的人,都在这里排队。
我个人习惯把订单簿想象成一个双层的停车场:
- 买盘(Bid Side):大家出价买入,价格高的排在前面。就像抢车位,谁出价高谁先停。
- 卖盘(Ask Side):大家出价卖出,价格低的排在前面。谁便宜谁先被买走。
中间那个价差,就是 Spread。做市商赚的就是这个差价。
核心概念:
- Bid Price:当前最高买入价
- Ask Price:当前最低卖出价
- Bid Size:买一价上的挂单量
- Ask Size:卖一价上的挂单量
- Spread = Ask Price - Bid Price
- Mid Price = (Bid + Ask) / 2
我在项目中遇到过一个问题:有些交易所的订单簿更新频率极高,每秒几千次。如果你直接拿原始数据去算,CPU 很快就烧了。所以,一定要做快照(Snapshot)和增量(Incremental)的区分。
3.2 Tick级与Bar级数据
这是两个完全不同的数据视角。我经常跟团队里的新人说:Tick 是显微镜,Bar 是望远镜。
3.2.1 Tick级数据
Tick 数据,就是每一笔成交的原始记录。它包含:
- 成交时间(精确到毫秒甚至微秒)
- 成交价格
- 成交量
- 成交方向(买方主动还是卖方主动)
为什么要用 Tick 数据?因为高频策略的决策窗口,可能只有几毫秒。你如果用 1 分钟 Bar 去回测,很多细节都被抹平了。我曾经吃过这个亏——一个基于订单流不平衡的策略,用 Bar 数据回测年化 30%,换成 Tick 数据一测,直接变成负收益。
避坑指南: 我曾经以为 Tick 数据越多越好,结果发现有些交易所的 Tick 数据里夹杂着「错误成交」或「测试订单」。一定要做数据清洗,否则你的回测结果就是垃圾。
3.2.2 Bar级数据
Bar 数据是对 Tick 数据的聚合。常见的 Bar 类型有:
- 时间Bar:固定时间间隔(如 1 分钟、5 分钟)
- 成交量Bar:固定成交量(如每 1000 手一根 Bar)
- 笔数Bar:固定成交笔数(如每 500 笔一根 Bar)
- 美元Bar:固定成交金额(如每 100 万美元一根 Bar)
我个人比较喜欢用成交量Bar。为什么?因为市场活跃的时候,时间Bar里可能塞满了数据;市场冷清的时候,时间Bar里可能就几笔成交。成交量Bar能更好地反映「市场参与度」。
| Bar类型 | 适用场景 | 缺点 |
|---|---|---|
| 时间Bar | 常规策略、趋势跟踪 | 活跃时段数据密集,冷清时段数据稀疏 |
| 成交量Bar | 高频策略、做市策略 | 计算复杂,需要实时统计 |
| 笔数Bar | 订单流分析 | 对异常大单敏感 |
| 美元Bar | 大资金策略 | 需要实时汇率转换 |
3.3 Level2与Level3数据模型设计
这里我要重点讲一下。很多新手分不清 Level2 和 Level3,以为只是「多几档行情」的区别。其实,这是两个完全不同的数据层级。
3.3.1 Level2 数据
Level2 数据,也叫「深度行情」。它展示的是订单簿上前 N 档的挂单情况。比如:
- 买一到买五的价格和挂单量
- 卖一到卖五的价格和挂单量
- 逐笔成交(部分交易所提供)
Level2 数据模型设计起来相对简单。我一般这样设计:
// Level2 快照数据结构示例
struct Level2Snapshot {
string symbol; // 合约代码
int64 timestamp; // 时间戳(纳秒)
vector<PriceLevel> bids; // 买盘,按价格降序排列
vector<PriceLevel> asks; // 卖盘,按价格升序排列
};
struct PriceLevel {
double price; // 价格
double size; // 挂单量
int orderCount; // 挂单笔数(部分交易所提供)
};
提示: 我个人习惯在 Level2 数据中额外存储一个「累计量」字段。这样在计算「价格冲击成本」时,可以快速定位到需要吃掉多少档才能成交。
3.3.2 Level3 数据
Level3 数据,也叫「全量深度行情」。它展示的是订单簿上每一笔挂单的详细信息。包括:
- 订单ID
- 价格
- 数量
- 时间戳
- 订单类型(限价单、市价单等)
- 订单状态(新增、撤销、成交、部分成交)
Level3 的数据量是 Level2 的几十倍甚至上百倍。为什么?因为 Level2 只告诉你「买一有 100 手」,而 Level3 告诉你「买一这 100 手是由 5 个不同的订单组成的,每个订单的 ID 分别是 xxx、yyy、zzz……」。你想想看,这数据量能不大吗?
Level3 数据模型设计要复杂得多。我建议使用「事件驱动」的方式:
// Level3 事件类型
enum OrderEventType {
ADD_ORDER, // 新增订单
CANCEL_ORDER, // 撤销订单
EXECUTE_ORDER, // 成交(全部或部分)
MODIFY_ORDER // 修改订单(价格或数量)
};
// Level3 事件数据结构
struct Level3Event {
string symbol;
int64 timestamp;
OrderEventType eventType;
string orderId; // 订单ID
double price;
double size;
double executedSize; // 已成交量(部分成交时使用)
string side; // 'buy' 或 'sell'
};
避坑指南: 我曾经在存储 Level3 数据时,直接用 JSON 格式存到数据库里。结果一天的数据量就超过了 100GB,查询慢得像蜗牛。后来我改用列式存储(如 Parquet)加上时间分区,才把查询时间降下来。记住:Level3 数据不适合用关系型数据库。
3.4 数据模型设计实战建议
嗯,这里我要分享一些实战中的经验。数据模型设计得好不好,直接决定了你的回测系统能跑多快。
- 区分快照与增量:每次全量快照太费带宽。我一般每 10 秒或每 1000 次更新做一次快照,中间用增量事件来恢复订单簿状态。
- 使用内存数据库:对于高频回测,磁盘 I/O 是最大的瓶颈。我建议用 Redis 或内存映射文件来存储订单簿状态。
- 预计算特征:不要在回测时实时计算订单簿特征(如买卖压力比、订单簿斜率等)。提前算好,存成字段,回测时直接读取。
- 数据压缩:Tick 数据和 Level3 数据体积巨大。我习惯用差分编码 + 字典压缩,能把体积压缩到原来的 1/5 左右。
一句话总结: 订单簿是市场的「心电图」,Tick 数据是「每一次心跳」,Bar 数据是「心率统计」。Level2 告诉你心跳有多快,Level3 告诉你每一次心跳的细节。搞懂了这些,你的回测系统才算真正「懂市场」。
最后说一句:数据模型设计没有银弹。不同的策略、不同的市场、不同的资金规模,适合的数据模型都不一样。我的建议是:先搞清楚你要做什么策略,再决定用什么数据模型。别一上来就追求 Level3 全量数据,那玩意儿处理起来真的很痛苦。