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 数据模型设计实战建议

嗯,这里我要分享一些实战中的经验。数据模型设计得好不好,直接决定了你的回测系统能跑多快。

  1. 区分快照与增量:每次全量快照太费带宽。我一般每 10 秒或每 1000 次更新做一次快照,中间用增量事件来恢复订单簿状态。
  2. 使用内存数据库:对于高频回测,磁盘 I/O 是最大的瓶颈。我建议用 Redis 或内存映射文件来存储订单簿状态。
  3. 预计算特征:不要在回测时实时计算订单簿特征(如买卖压力比、订单簿斜率等)。提前算好,存成字段,回测时直接读取。
  4. 数据压缩:Tick 数据和 Level3 数据体积巨大。我习惯用差分编码 + 字典压缩,能把体积压缩到原来的 1/5 左右。

一句话总结: 订单簿是市场的「心电图」,Tick 数据是「每一次心跳」,Bar 数据是「心率统计」。Level2 告诉你心跳有多快,Level3 告诉你每一次心跳的细节。搞懂了这些,你的回测系统才算真正「懂市场」。

市场微观结构与数据模型架构图 交易所原始数据流 订单簿(Order Book) Tick级数据 Bar级数据 订单簿核心要素 • Bid / Ask 价格与数量 • Spread 价差 • 深度(Depth) Tick级数据特征 • 逐笔成交记录 • 毫秒/微秒精度 • 包含成交方向 Bar级数据聚合方式 • 时间Bar / 成交量Bar • 笔数Bar / 美元Bar • OHLCV 结构 Level2 数据模型 Level3 数据模型 快照+增量模型 数据粒度从粗到细:Bar级 → Tick级 → 订单簿 → Level3

最后说一句:数据模型设计没有银弹。不同的策略、不同的市场、不同的资金规模,适合的数据模型都不一样。我的建议是:先搞清楚你要做什么策略,再决定用什么数据模型。别一上来就追求 Level3 全量数据,那玩意儿处理起来真的很痛苦。