量化学习(一):初识A股Level2数据集
摘要
Level2数据记录了证券市场的十档盘口、逐笔委托和逐笔成交信息。相较于日线、分钟线和Level1行情,Level2能够进一步描述订单如何进入市场、如何撤销、如何成交,以及这些行为怎样影响盘口和价格。
本文面向零基础读者,使用一份真实A股Level2数据,介绍三类原始文件及其全部字段,解释订单簿、主动成交、盘口失衡、订单流失衡、VWAP和价格冲击等术语,并给出可以直接运行的Python代码。实验部分以600009.SH在20260626 的数据为例,展示原始数据清洗前后的变化、重复字段、低信息字段和具有研究价值的核心字段。
关键词: Level2;订单簿;逐笔委托;逐笔成交;市场微观结构;量化交易
20260626 Level2 数据:
通过网盘分享的文件:20260626.7z
链接: https://pan.baidu.com/s/1-wDwn9ffkIYDHPDdf5XNxw 提取码: pita
一、研究背景
1.1 什么是Level2数据
股票软件中常见的日线和分钟线,主要由开盘价、最高价、最低价、收盘价、成交量和成交额组成。这些数据描述了某段时间内形成的最终结果,却无法完整解释价格形成过程。
Level1行情通常提供最新成交价、累计成交量以及有限档位的买卖盘口。Level2进一步提供十档盘口、逐笔委托、逐笔成交和撤单事件。
例如,某只股票从10.00元上涨至10.10元,分钟线只能告诉我们价格上涨了1%。Level2可以继续回答以下问题:
| 问题 | 对应Level2数据 |
|---|---|
| 上涨由主动买入推动,还是卖盘主动撤单造成 | 逐笔成交、逐笔委托 |
| 买一到买十共有多少挂单 | 十档盘口 |
| 是否出现连续大额主动买入 | 逐笔成交 |
| 是否存在频繁挂单和快速撤单 | 逐笔委托 |
| 资金集中在开盘、盘中还是尾盘交易 | 毫秒级时间字段 |
| 大额成交发生后价格是否明显变化 | 成交记录与行情快照 |
Level2研究属于市场微观结构研究。市场微观结构关注订单、流动性、交易机制、买卖价差和价格发现过程。
1.2 为什么量化学习需要了解Level2
日线和分钟线主要描述价格结果,Level2能够描述形成结果的交易行为。对于短周期交易、盘口因子、成交执行、异常交易识别和资金行为分析,Level2通常具有更高的信息密度。
Level2的研究价值主要体现在以下方面:
| 研究方向 | Level2提供的信息 |
|---|---|
| 短周期价格预测 | 盘口失衡、订单流失衡、主动成交方向 |
| 流动性研究 | 买卖价差、盘口深度、撤单率 |
| 交易执行 | 成交概率、滑点、订单等待时间 |
| 行为识别 | 大额成交、拆单、快速挂撤、脉冲交易 |
| 价格发现 | 成交后价格变化、大单价格冲击 |
| 日内结构 | 开盘、午间、尾盘成交集中度 |
Level2也存在明确边界。它能够观察订单和成交行为,却通常不能直接观察交易账户身份。仅凭Level2无法确认某笔交易来自散户、量化机构或游资,只能判断其行为更接近哪一种交易模式。
二、Level2数据集结构与参数
2.1 数据目录
本文使用的数据按"交易日/股票代码"组织:
text
20260626/
└── 600009.SH/
├── 行情.csv
├── 逐笔委托.csv
└── 逐笔成交.csv
三张表分别表示市场状态、委托事件和成交事件。
| 文件 | 行数示例 | 字段数 | 数据性质 |
|---|---|---|---|
行情.csv |
4820 | 66 | 盘口状态快照 |
逐笔委托.csv |
44533 | 10 | 委托事件流 |
逐笔成交.csv |
29209 | 12 | 成交事件流 |
原始CSV使用GB18030编码。价格字段以万分之一元存储,数量字段通常以股为单位。本文后续字段名称、事件代码和缩放规则均以这份数据为准;更换数据商、交易所或证券品种时,应先核对对应数据字典。
python
from pathlib import Path
import pandas as pd
stock_dir = Path("20260626/600009.SH")
market = pd.read_csv(stock_dir / "行情.csv", encoding="gb18030")
orders = pd.read_csv(stock_dir / "逐笔委托.csv", encoding="gb18030")
trades = pd.read_csv(stock_dir / "逐笔成交.csv", encoding="gb18030")
print(market.shape)
print(orders.shape)
print(trades.shape)
输出为:
text
(4820, 66)
(44533, 10)
(29209, 12)
2.2 三张表共有字段
| 字段 | 含义 | 是否进入模型 |
|---|---|---|
万得代码 |
完整证券代码,例如600009.SH |
用于索引,不直接作为数值特征 |
交易所代码 |
不带后缀的证券代码,例如600009 |
与万得代码重复,可删除 |
自然日 |
交易日期,格式为YYYYMMDD |
用于日期索引 |
时间 |
HHMMSSmmm格式的日内时间 |
核心字段 |
例如:
text
92501000 → 09:25:01.000
145700090 → 14:57:00.090
同一毫秒可能发生多笔委托或成交,因此时间不能单独充当唯一主键。本文样本中,逐笔委托有21073行与其他记录共享时间戳,逐笔成交有21366行共享时间戳,但委托编号和成交编号均没有重复。
2.3 行情快照字段
行情.csv共66列。除4个公共字段外,其余字段如下。
| 编号 | 字段 | 字段含义 | 研究价值 |
|---|---|---|---|
| 1 | 万得代码 | 万得统一证券代码,例如600000.SH |
用于识别股票和关联其他数据,属于公共标识字段 |
| 2 | 交易所代码 | 当前数据中为不带市场后缀的证券代码,例如600009 |
与万得代码重复,可用于跨表校验;字段名沿用数据源命名 |
| 3 | 自然日 | 行情快照对应的自然日期 | 用于按交易日分组、过滤和回放,属于公共时间字段 |
| 4 | 时间 | 行情快照对应的盘中时间 | 用于恢复盘口时序、划分集合竞价与连续竞价,属于公共时间字段 |
| 5 | 成交价 | 当前快照记录的最新成交价格 | 用于计算收益率、价格趋势、波动率和价格冲击,研究价值高 |
| 6 | 成交量 | 当前快照对应的增量成交量 | 当前样本中恒为0,应优先使用累计成交量差分,直接研究价值低 |
| 7 | 成交额 | 当前快照对应的增量成交金额 | 当前样本中恒为0,应优先使用累计成交额差分,直接研究价值低 |
| 8 | 成交笔数 | 截至当前快照的累计成交笔数 | 可通过相邻快照差分估计成交活跃度,研究价值高 |
| 9 | IOPV | ETF盘中参考净值 | 对ETF估值和折溢价分析有用,普通股票中通常为0 |
| 10 | 成交标志 | 当前快照的成交状态标志 | 可辅助判断行情事件类型,当前样本为空,直接研究价值低 |
| 11 | BS标志 | 当前快照的买卖方向标志 | 理论上可辅助识别买卖方向,当前样本为空,直接研究价值低 |
| 12 | 当日累计成交量 | 截至当前时点的累计成交数量 | 差分后可得到区间成交量,是成交活跃度与量价分析的核心字段 |
| 13 | 当日成交额 | 截至当前时点的累计成交金额 | 差分后可得到区间成交额,是资金规模与时段集中度分析的核心字段 |
| 14 | 最高价 | 截至当前时点的日内最高成交价 | 用于计算日内位置、突破行为和最大上涨幅度,研究价值中高 |
| 15 | 最低价 | 截至当前时点的日内最低成交价 | 用于计算日内位置、回撤行为和最大下跌幅度,研究价值中高 |
| 16 | 开盘价 | 当日开盘成交价格 | 用于计算开盘后收益、跳空幅度和竞价强弱,研究价值中高 |
| 17 | 前收盘 | 上一交易日收盘价格 | 用于计算涨跌幅、跳空幅度和涨跌停参考价格,研究价值高 |
| 18 | 申卖价1 | 当前最低卖出报价,即卖一价 | 用于计算买卖价差、成交成本和最优卖价变化,研究价值高 |
| 19 | 申卖价2 | 当前第二低的卖出报价 | 用于观察近端卖盘价格梯度和卖一消耗后的潜在阻力,研究价值高 |
| 20 | 申卖价3 | 当前第三低的卖出报价 | 用于分析近端卖盘深度与价格阶梯,研究价值高 |
| 21 | 申卖价4 | 当前第四低的卖出报价 | 用于分析卖方报价层次和短期上行阻力,研究价值中高 |
| 22 | 申卖价5 | 当前第五低的卖出报价 | 用于构建五档卖盘价格结构,研究价值中高 |
| 23 | 申卖价6 | 当前第六低的卖出报价 | 用于观察五档之外的深层卖盘价格,研究价值中高 |
| 24 | 申卖价7 | 当前第七低的卖出报价 | 用于分析深层卖盘分布与潜在阻力,研究价值中高 |
| 25 | 申卖价8 | 当前第八低的卖出报价 | 用于分析远端卖盘价格梯度,研究价值中等 |
| 26 | 申卖价9 | 当前第九低的卖出报价 | 用于补充深层卖盘结构,研究价值中等 |
| 27 | 申卖价10 | 当前第十低的卖出报价 | 用于确定十档卖盘边界和完整深度范围,研究价值中等 |
| 28 | 申卖量1 | 卖一价格上的未成交委托量 | 反映最优卖价即时抛压,是盘口分析的核心字段 |
| 29 | 申卖量2 | 卖二价格上的未成交委托量 | 反映卖一被消耗后的下一层抛压,研究价值高 |
| 30 | 申卖量3 | 卖三价格上的未成交委托量 | 用于分析近端卖盘深度与上行阻力,研究价值高 |
| 31 | 申卖量4 | 卖四价格上的未成交委托量 | 用于分析卖方流动性分布,研究价值中高 |
| 32 | 申卖量5 | 卖五价格上的未成交委托量 | 用于计算五档卖盘总量和集中度,研究价值中高 |
| 33 | 申卖量6 | 卖六价格上的未成交委托量 | 用于观察深层卖方流动性,研究价值中高 |
| 34 | 申卖量7 | 卖七价格上的未成交委托量 | 用于分析潜在远端抛压,研究价值中高 |
| 35 | 申卖量8 | 卖八价格上的未成交委托量 | 用于补充深层卖盘数量结构,研究价值中等 |
| 36 | 申卖量9 | 卖九价格上的未成交委托量 | 用于识别远端大额挂单和卖盘堆积,研究价值中等 |
| 37 | 申卖量10 | 卖十价格上的未成交委托量 | 用于计算完整十档卖盘深度,研究价值中等 |
| 38 | 申买价1 | 当前最高买入报价,即买一价 | 用于计算买卖价差、即时支撑和最优买价变化,研究价值高 |
| 39 | 申买价2 | 当前第二高的买入报价 | 用于观察买一消耗后的下一层价格支撑,研究价值高 |
| 40 | 申买价3 | 当前第三高的买入报价 | 用于分析近端买盘价格梯度与承接结构,研究价值高 |
| 41 | 申买价4 | 当前第四高的买入报价 | 用于分析买方报价层次和短期支撑,研究价值中高 |
| 42 | 申买价5 | 当前第五高的买入报价 | 用于构建五档买盘价格结构,研究价值中高 |
| 43 | 申买价6 | 当前第六高的买入报价 | 用于观察五档之外的深层买盘价格,研究价值中高 |
| 44 | 申买价7 | 当前第七高的买入报价 | 用于分析深层买盘分布与潜在支撑,研究价值中高 |
| 45 | 申买价8 | 当前第八高的买入报价 | 用于分析远端买盘价格梯度,研究价值中等 |
| 46 | 申买价9 | 当前第九高的买入报价 | 用于补充深层买盘结构,研究价值中等 |
| 47 | 申买价10 | 当前第十高的买入报价 | 用于确定十档买盘边界和完整深度范围,研究价值中等 |
| 48 | 申买量1 | 买一价格上的未成交委托量 | 反映最优买价即时承接,是盘口分析的核心字段 |
| 49 | 申买量2 | 买二价格上的未成交委托量 | 反映买一被消耗后的下一层承接,研究价值高 |
| 50 | 申买量3 | 买三价格上的未成交委托量 | 用于分析近端买盘深度与下行支撑,研究价值高 |
| 51 | 申买量4 | 买四价格上的未成交委托量 | 用于分析买方流动性分布,研究价值中高 |
| 52 | 申买量5 | 买五价格上的未成交委托量 | 用于计算五档买盘总量和集中度,研究价值中高 |
| 53 | 申买量6 | 买六价格上的未成交委托量 | 用于观察深层买方流动性,研究价值中高 |
| 54 | 申买量7 | 买七价格上的未成交委托量 | 用于分析潜在远端承接力量,研究价值中高 |
| 55 | 申买量8 | 买八价格上的未成交委托量 | 用于补充深层买盘数量结构,研究价值中等 |
| 56 | 申买量9 | 买九价格上的未成交委托量 | 用于识别远端大额挂单和买盘堆积,研究价值中等 |
| 57 | 申买量10 | 买十价格上的未成交委托量 | 用于计算完整十档买盘深度,研究价值中等 |
| 58 | 加权平均叫卖价 | 按委托量加权计算的卖方平均报价 | 用于衡量整体卖盘成本、卖盘重心及其时序变化,研究价值中高 |
| 59 | 加权平均叫买价 | 按委托量加权计算的买方平均报价 | 用于衡量整体买盘成本、买盘重心及其时序变化,研究价值中高 |
| 60 | 叫卖总量 | 当前全部有效卖出委托的总数量 | 用于衡量整体卖方供给和深层抛压,研究价值高 |
| 61 | 叫买总量 | 当前全部有效买入委托的总数量 | 用于衡量整体买方需求和深层承接,研究价值高 |
| 62 | 不加权指数 | 数据商预留的市场指数类字段 | 当前个股样本中恒为0,直接研究价值低 |
| 63 | 品种总数 | 当前市场统计口径下的证券品种数量 | 当前个股样本中恒为0,直接研究价值低 |
| 64 | 上涨品种数 | 当前市场中价格上涨的证券数量 | 理论上可衡量市场广度,当前个股样本中恒为0 |
| 65 | 下跌品种数 | 当前市场中价格下跌的证券数量 | 理论上可衡量市场弱势广度,当前个股样本中恒为0 |
| 66 | 持平品种数 | 当前市场中价格持平的证券数量 | 理论上可补充市场广度结构,当前个股样本中恒为0 |
所有价格类字段都需要除以10000。例如:
text
成交价 232000 → 23.20元
申买价1 231900 → 23.19元
申卖价1 232000 → 23.20元
前收盘 233000 → 23.30元
申买价1是当前最高买价,申卖价1是当前最低卖价。档位数字越大,报价距离当前成交价格通常越远。
叫买总量和十档买量之和含义不同。前者可能覆盖更完整的订单簿,后者只表示当前显示的十档深度。因此,这两个字段不能视为简单重复字段。
2.4 逐笔委托字段
| 编号 | 逐笔委托.csv字段 |
字段含义 | 研究价值 |
|---|---|---|---|
| 1 | 万得代码 | 完整证券代码,例如600009.SH |
股票标识和跨表关联 |
| 2 | 交易所代码 | 不带市场后缀的证券代码 | 与万得代码重复,可用于校验 |
| 3 | 自然日 | 委托事件对应的交易日期 | 日期分组和多日研究 |
| 4 | 时间 | 委托事件发生的毫秒级时间 | 订单排序、节奏和撤单速度分析 |
| 5 | 委托编号 | 数据中的委托事件顺序编号 | 事件排序和唯一性检查 |
| 6 | 交易所委托号 | 交易所分配的订单编号 | 关联新增、撤销和成交事件 |
| 7 | 委托类型 | 新增、撤销或状态事件类型 | 识别订单进入和退出过程 |
| 8 | 委托代码 | 买卖方向或状态代码 | 区分买单、卖单和状态记录 |
| 9 | 委托价格 | 订单申报价格 | 挂单位置、侵略性和订单金额 |
| 10 | 委托数量 | 订单申报或撤销数量 | 订单规模、撤单量和流动性分析 |
当前样本中的委托类型如下:
| 委托类型 | 数量 | 含义 |
|---|---|---|
A |
32101 | 新增委托 |
D |
12428 | 撤销委托 |
S |
4 | 市场状态记录 |
当委托类型为A或D时,委托代码=B表示买单,委托代码=S表示卖单。状态记录中还出现I、O、J和C等代码,其精确语义需要对应数据商字典确认。
一条原始新增委托为:
text
时间 委托类型 委托代码 委托价格 委托数量
91500070 A S 239800 5000
清洗后可以解释为:
text
09:15:00.070新增一笔卖单
委托价格:23.98元
委托数量:5000股
委托金额:119900元
2.5 逐笔成交字段
| 编号 | 逐笔成交.csv字段 |
字段含义 | 研究价值 |
|---|---|---|---|
| 1 | 万得代码 | 完整证券代码,例如600009.SH |
股票标识和跨表关联 |
| 2 | 交易所代码 | 不带市场后缀的证券代码 | 与万得代码重复,可用于校验 |
| 3 | 自然日 | 成交事件对应的交易日期 | 日期分组和多日研究 |
| 4 | 时间 | 成交事件发生的毫秒级时间 | 成交排序、节奏和时段分析 |
| 5 | 成交编号 | 当日成交事件编号 | 成交排序和唯一性检查 |
| 6 | 成交代码 | 成交记录附带的状态代码 | 当前样本恒为0x00,直接研究价值低 |
| 7 | 委托代码 | 成交记录附带的委托状态代码 | 当前样本恒为0x00,直接研究价值低 |
| 8 | BS标志 | 主动买入或主动卖出方向 | 主动成交、订单流和资金方向分析 |
| 9 | 成交价格 | 本笔交易的实际成交价格 | 收益、VWAP和价格冲击分析 |
| 10 | 成交数量 | 本笔交易的成交股数 | 成交金额和订单规模分层 |
| 11 | 叫卖序号 | 参与成交的卖方订单序号 | 关联卖方委托及恢复订单生命周期 |
| 12 | 叫买序号 | 参与成交的买方订单序号 | 关联买方委托及恢复订单生命周期 |
原始成交记录为:
text
时间 BS标志 成交价格 成交数量 叫卖序号 叫买序号
92501000 S 232000 100 62071 53871
清洗后表示:
text
09:25:01.000发生主动卖出成交
成交价格:23.20元
成交数量:100股
成交金额:2320元
在本文数据口径中,B表示主动买入,S表示主动卖出。主动买入通常意味着买方接受当前卖价,主动卖出通常意味着卖方接受当前买价。
2.6 重复字段和低信息字段
字段重复可以分为结构重复、可校验重复和低信息重复。
| 字段关系 | 判断 | 处理方式 |
|---|---|---|
万得代码与交易所代码 |
表达同一证券身份 | 保留万得代码 |
| 三张表中的代码、日期和时间 | 为独立文件提供索引 | 合并后保留一份 |
当日累计成交量与逐笔成交数量之和 |
可以相互推导 | 保留一份建模,另一份用于校验 |
当日成交额与逐笔成交金额之和 |
可以相互推导 | 保留一份建模,另一份用于校验 |
| 行情开高低价与逐笔成交价格 | 可以重新计算 | 快照字段用于审计 |
| 十档买卖量与叫买、叫卖总量 | 统计范围可能不同 | 两者均保留 |
| 加权买卖价与十档报价 | 数据商计算口径可能覆盖更多深度 | 两者均保留 |
行情成交量、成交额 |
当前样本恒为0 | 删除 |
行情成交标志、BS标志 |
当前样本全空 | 删除 |
成交代码、成交表委托代码 |
当前样本恒为0x00 |
删除 |
| 指数、品种数、涨跌家数 | 当前个股文件恒为0 | 删除 |
本文样本中,行情末行累计成交量为15024986股,与逐笔成交数量之和完全一致;行情末行累计成交额为349450088元,与逐笔成交计算结果349450087.92元一致;累计成交笔数为29209,也与逐笔成交行数一致。
这些字段虽然具有重复性,却非常适合用于数据完整性验证。
三、相关术语和数学原理
3.1 买卖价差
买卖价差是卖一价与买一价之差:
S p r e a d t = A s k 1 , t − B i d 1 , t Spread_t=Ask_{1,t}-Bid_{1,t} Spreadt=Ask1,t−Bid1,t
假设买一为10.00元,卖一为10.01元,则价差为0.01元。价差越小,交易成本通常越低,市场流动性通常越好。
相对价差可以消除不同股价的影响:
R e l a t i v e S p r e a d t = A s k 1 , t − B i d 1 , t ( A s k 1 , t + B i d 1 , t ) / 2 RelativeSpread_t= \frac{Ask_{1,t}-Bid_{1,t}} {(Ask_{1,t}+Bid_{1,t})/2} RelativeSpreadt=(Ask1,t+Bid1,t)/2Ask1,t−Bid1,t
3.2 中间价
中间价是买一与卖一的平均值:
M i d P r i c e t = A s k 1 , t + B i d 1 , t 2 MidPrice_t=\frac{Ask_{1,t}+Bid_{1,t}}{2} MidPricet=2Ask1,t+Bid1,t
买一10.00元、卖一10.02元时,中间价为10.01元。中间价常用于衡量盘口中心和计算短周期价格变化。
3.3 盘口深度
盘口深度表示各档位能够承接的挂单数量。十档买盘深度为:
B i d D e p t h t = ∑ i = 1 10 B i d V o l u m e i , t BidDepth_t=\sum_{i=1}^{10}BidVolume_{i,t} BidDeptht=i=1∑10BidVolumei,t
十档卖盘深度为:
A s k D e p t h t = ∑ i = 1 10 A s k V o l u m e i , t AskDepth_t=\sum_{i=1}^{10}AskVolume_{i,t} AskDeptht=i=1∑10AskVolumei,t
深度较大通常意味着市场能够吸收更大的订单,但挂单随时可能撤销,因此深度不能视为确定的成交承诺。
3.4 盘口失衡
盘口失衡衡量买卖两侧挂单数量差异:
I m b a l a n c e t = B i d D e p t h t − A s k D e p t h t B i d D e p t h t + A s k D e p t h t Imbalance_t= \frac{BidDepth_t-AskDepth_t} {BidDepth_t+AskDepth_t} Imbalancet=BidDeptht+AskDepthtBidDeptht−AskDeptht
假设十档买盘为120万股,十档卖盘为80万股:
I m b a l a n c e = 120 − 80 120 + 80 = 0.2 Imbalance=\frac{120-80}{120+80}=0.2 Imbalance=120+80120−80=0.2
结果为0.2,表示买盘数量相对占优。该指标取值范围为 − 1 , 1 -1,1 −1,1。
3.5 微价格
中间价只使用买卖报价,没有考虑一档数量。微价格将一档挂单量加入计算:
M i c r o P r i c e t = A s k 1 , t × B i d V o l u m e 1 , t + B i d 1 , t × A s k V o l u m e 1 , t B i d V o l u m e 1 , t + A s k V o l u m e 1 , t MicroPrice_t= \frac{Ask_{1,t}\times BidVolume_{1,t} +Bid_{1,t}\times AskVolume_{1,t}} {BidVolume_{1,t}+AskVolume_{1,t}} MicroPricet=BidVolume1,t+AskVolume1,tAsk1,t×BidVolume1,t+Bid1,t×AskVolume1,t
买一挂单量明显高于卖一时,微价格会更接近卖一价,表示买方承接力量较强。
3.6 主动买卖
成交金额为:
A m o u n t i = P r i c e i × V o l u m e i Amount_i=Price_i\times Volume_i Amounti=Pricei×Volumei
主动买入金额占比为:
A c t i v e B u y R a t i o = ∑ i : B S i = B A m o u n t i ∑ i : B S i ∈ { B , S } A m o u n t i ActiveBuyRatio= \frac{\sum_{i:BS_i=B}Amount_i} {\sum_{i:BS_i\in\{B,S\}}Amount_i} ActiveBuyRatio=∑i:BSi∈{B,S}Amounti∑i:BSi=BAmounti
主动净成交方向为:
A c t i v e N e t = B u y A m o u n t − S e l l A m o u n t B u y A m o u n t + S e l l A m o u n t ActiveNet= \frac{BuyAmount-SellAmount} {BuyAmount+SellAmount} ActiveNet=BuyAmount+SellAmountBuyAmount−SellAmount
ActiveNet接近1表示主动买入占优,接近-1表示主动卖出占优,接近0表示双方较均衡。
主动成交方向只能描述谁接受了当前报价,无法直接证明资金最终流入或流出。
3.7 VWAP
VWAP是成交量加权平均价格:
V W A P = ∑ i P r i c e i × V o l u m e i ∑ i V o l u m e i VWAP= \frac{\sum_i Price_i\times Volume_i} {\sum_i Volume_i} VWAP=∑iVolumei∑iPricei×Volumei
假设有两笔成交:
| 成交价格 | 成交数量 |
|---|---|
| 10.00 | 1000 |
| 10.10 | 3000 |
则:
V W A P = 10.00 × 1000 + 10.10 × 3000 4000 = 10.075 VWAP=\frac{10.00\times1000+10.10\times3000}{4000} =10.075 VWAP=400010.00×1000+10.10×3000=10.075
最新价格高于VWAP,说明价格相对日内平均成交成本偏高;最新价格低于VWAP,说明价格相对平均成本偏低。
3.8 撤单事件占比
本文计算的是撤单事件在新增与撤单事件总数中的占比:
C a n c e l E v e n t S h a r e = C a n c e l C o u n t A d d C o u n t + C a n c e l C o u n t CancelEventShare= \frac{CancelCount} {AddCount+CancelCount} CancelEventShare=AddCount+CancelCountCancelCount
该指标描述委托事件构成,不等同于"被撤订单数占新增订单数的比例",也不表示撤销数量占申报数量的比例。撤单事件占比较高可能来自算法做市、行情快速变化、流动性管理或试探性挂单,仍需与撤单速度、撤单方向、挂单位置和后续成交共同分析。
3.9 变异系数
变异系数用于衡量数据相对波动程度:
C V = σ ∣ μ ∣ CV=\frac{\sigma}{|\mu|} CV=∣μ∣σ
其中 σ \sigma σ是标准差, μ \mu μ是均值。
对于成交间隔,较低的CV通常表示交易节奏较均匀;较高的CV表示交易集中在少数时段或存在明显脉冲。
午间休市和集合竞价会显著放大成交间隔CV,因此应分别计算连续竞价时段,避免将休市时间误认为交易节奏异常。
3.10 信息熵
信息熵可以衡量成交金额在时间上的分散程度:
H = − ∑ j = 1 n p j ln p j H=-\sum_{j=1}^{n}p_j\ln p_j H=−j=1∑npjlnpj
其中 p j p_j pj表示第 j j j个时间窗口的成交金额占比。
成交金额均匀分布在多个时间窗口时,熵较高;成交集中在少数窗口时,熵较低。为了便于跨样本比较,可以除以 ln n \ln n lnn得到0到1之间的归一化熵。
3.11 简化订单事件净流
为了把新增与撤销事件统一到同一个方向上,本文定义简化订单事件净流:
N e t O r d e r E v e n t F l o w = N e w B u y − N e w S e l l − C a n c e l B u y + C a n c e l S e l l NetOrderEventFlow= NewBuy-NewSell-CancelBuy+CancelSell NetOrderEventFlow=NewBuy−NewSell−CancelBuy+CancelSell
新增买单提高买方压力,新增卖单提高卖方压力;买单撤销削弱买方压力,卖单撤销削弱卖方压力。计算时应统一使用委托数量或委托金额,并固定统计窗口:
N o r m a l i z e d N e t O r d e r E v e n t F l o w = N e t O r d e r E v e n t F l o w N e w B u y + N e w S e l l + C a n c e l B u y + C a n c e l S e l l + ϵ NormalizedNetOrderEventFlow= \frac{NetOrderEventFlow} {NewBuy+NewSell+CancelBuy+CancelSell+\epsilon} NormalizedNetOrderEventFlow=NewBuy+NewSell+CancelBuy+CancelSell+ϵNetOrderEventFlow
这个指标是当前数据口径下的简化统计。Cont、Kukanov与Stoikov提出的经典订单流失衡(OFI)跟踪最优买卖价及其队列数量变化,定义范围更严格。使用"OFI"名称时,应明确价格层级、事件映射和时间窗口。
3.12 价格冲击
本文使用成交方向调整后的未来中间价收益,作为短期价格响应的代理指标:
S i g n e d M i d R e s p o n s e i = D i r e c t i o n i × M i d P r i c e i + Δ t − M i d P r i c e i M i d P r i c e i SignedMidResponse_i= Direction_i\times \frac{MidPrice_{i+\Delta t}-MidPrice_i} {MidPrice_i} SignedMidResponsei=Directioni×MidPriceiMidPricei+Δt−MidPricei
主动买入的方向取1,主动卖出的方向取-1。如果一笔主动买入后中间价上涨,该指标为正。实际计算必须固定 Δ t \Delta t Δt,处理同毫秒事件顺序,并避免跨越午间休市或集合竞价边界。
该指标描述成交方向与随后价格变化的一致程度,不能单独证明某笔交易对价格变化具有因果作用。它可以帮助区分"成交金额很大但随后价格变化较小"和"成交金额较小但随后价格快速变化"两类现象。
四、方法论与真实数据实验
4.1 数据清洗代码
下面的代码可以直接读取三张表、转换价格和时间、过滤状态记录,并删除当前样本中的重复或低信息字段。
python
from pathlib import Path
import numpy as np
import pandas as pd
PRICE_SCALE = 10000.0
def time_to_ms(series: pd.Series) -> pd.Series:
text = series.astype("int64").astype(str).str.zfill(9)
return (
text.str[0:2].astype(int) * 3_600_000
+ text.str[2:4].astype(int) * 60_000
+ text.str[4:6].astype(int) * 1_000
+ text.str[6:9].astype(int)
)
def load_level2(stock_dir: str):
root = Path(stock_dir)
market = pd.read_csv(root / "行情.csv", encoding="gb18030")
orders = pd.read_csv(root / "逐笔委托.csv", encoding="gb18030")
trades = pd.read_csv(root / "逐笔成交.csv", encoding="gb18030")
market["time_ms"] = time_to_ms(market["时间"])
orders["time_ms"] = time_to_ms(orders["时间"])
trades["time_ms"] = time_to_ms(trades["时间"])
market_price_columns = [
"成交价", "最高价", "最低价", "开盘价", "前收盘",
"加权平均叫卖价", "加权平均叫买价",
*[f"申卖价{i}" for i in range(1, 11)],
*[f"申买价{i}" for i in range(1, 11)],
]
market[market_price_columns] = (
market[market_price_columns].astype(float) / PRICE_SCALE
)
orders["order_price"] = orders["委托价格"] / PRICE_SCALE
orders["order_amount"] = (
orders["order_price"] * orders["委托数量"]
)
trades["trade_price"] = trades["成交价格"] / PRICE_SCALE
trades["trade_amount"] = (
trades["trade_price"] * trades["成交数量"]
)
market_drop_columns = [
"交易所代码",
"成交量",
"成交额",
"IOPV",
"成交标志",
"BS标志",
"不加权指数",
"品种总数",
"上涨品种数",
"下跌品种数",
"持平品种数",
]
market = market.drop(columns=market_drop_columns)
orders = orders[
orders["委托类型"].isin(["A", "D"])
].copy()
orders = orders.drop(columns=["交易所代码"])
trades = trades[
trades["BS标志"].isin(["B", "S"])
& (trades["成交价格"] > 0)
& (trades["成交数量"] > 0)
].copy()
trades = trades.drop(
columns=["交易所代码", "成交代码", "委托代码"]
)
valid_market = market[
(market["申买价1"] > 0)
& (market["申卖价1"] > 0)
].copy()
valid_market.attrs["valid_two_sided_quote_ratio"] = (
len(valid_market) / len(market)
)
return valid_market, orders, trades
运行:
python
market, orders, trades = load_level2(
"20260626/600009.SH"
)
print(market.shape)
print(orders.shape)
print(trades.shape)
这里的valid_market只保留买一和卖一同时大于0的双边有效盘口,用于计算价差和常规盘口失衡。集合竞价转换状态以及涨跌停等单边盘口具有独立研究价值,正式研究时应单独标记,不能直接当作异常记录删除。
数据变化如下:
| 数据表 | 清洗前 | 清洗后 | 主要变化 |
|---|---|---|---|
| 行情 | 4820×66 | 4817×56 | 删除10个无信息字段和交易所代码,过滤3条无有效一档盘口记录,新增time_ms |
| 委托 | 44533×10 | 44529×12 | 删除4条状态记录和交易所代码,新增时间、价格、金额字段 |
| 成交 | 29209×12 | 29209×12 | 删除3个冗余字段,新增时间、价格、金额字段 |
4.2 特征构造代码
python
def safe_ratio(numerator, denominator):
return float(numerator / denominator) if denominator else 0.0
def coefficient_of_variation(values):
values = pd.Series(values).replace(
[np.inf, -np.inf], np.nan
).dropna()
if len(values) == 0 or abs(values.mean()) < 1e-12:
return 0.0
return float(values.std(ddof=0) / abs(values.mean()))
def build_level2_features(market, orders, trades):
amount = trades["trade_amount"]
buy = trades["BS标志"].eq("B")
sell = trades["BS标志"].eq("S")
buy_amount = amount[buy].sum()
sell_amount = amount[sell].sum()
small = amount < 40_000
medium = (amount >= 40_000) & (amount < 100_000)
large = (amount >= 100_000) & (amount < 500_000)
mega = amount >= 500_000
add = orders["委托类型"].eq("A")
cancel = orders["委托类型"].eq("D")
buy_order = orders["委托代码"].eq("B")
sell_order = orders["委托代码"].eq("S")
bid_volume_columns = [
f"申买量{i}" for i in range(1, 11)
]
ask_volume_columns = [
f"申卖量{i}" for i in range(1, 11)
]
bid_depth = market[bid_volume_columns].sum(axis=1)
ask_depth = market[ask_volume_columns].sum(axis=1)
spread = market["申卖价1"] - market["申买价1"]
imbalance = (
(bid_depth - ask_depth)
/ (bid_depth + ask_depth).replace(0, np.nan)
)
full_book_imbalance = (
(market["叫买总量"] - market["叫卖总量"])
/ (market["叫买总量"] + market["叫卖总量"])
.replace(0, np.nan)
)
trade_intervals = (
trades["time_ms"].sort_values().diff().dropna()
)
minute_count = trades.groupby(
trades["time_ms"] // 60_000
).size()
total_amount = amount.sum()
open_30 = trades["time_ms"].between(
9 * 3_600_000 + 30 * 60_000,
10 * 3_600_000,
)
close_10 = trades["time_ms"].between(
14 * 3_600_000 + 50 * 60_000,
15 * 3_600_000,
)
return {
"trade_count": len(trades),
"total_trade_amount": float(total_amount),
"average_trade_amount": float(amount.mean()),
"active_buy_ratio": safe_ratio(
buy_amount, buy_amount + sell_amount
),
"active_sell_ratio": safe_ratio(
sell_amount, buy_amount + sell_amount
),
"active_net": safe_ratio(
buy_amount - sell_amount,
buy_amount + sell_amount,
),
"small_trade_count_ratio": float(small.mean()),
"small_trade_amount_ratio": safe_ratio(
amount[small].sum(), total_amount
),
"medium_trade_amount_ratio": safe_ratio(
amount[medium].sum(), total_amount
),
"large_trade_amount_ratio": safe_ratio(
amount[large].sum(), total_amount
),
"mega_trade_amount_ratio": safe_ratio(
amount[mega].sum(), total_amount
),
"cancel_event_share": safe_ratio(
cancel.sum(), add.sum() + cancel.sum()
),
"buy_share_of_cancels": safe_ratio(
(cancel & buy_order).sum(), cancel.sum()
),
"sell_share_of_cancels": safe_ratio(
(cancel & sell_order).sum(), cancel.sum()
),
"valid_two_sided_quote_ratio": float(
market.attrs["valid_two_sided_quote_ratio"]
),
"spread_mean": float(spread.mean()),
"book_imbalance_mean": float(imbalance.mean()),
"full_book_imbalance_mean": float(
full_book_imbalance.mean()
),
"within_100ms_trade_ratio": float(
trade_intervals.lt(100).mean()
),
"minute_count_cv": coefficient_of_variation(
minute_count
),
"raw_trade_interval_cv_diagnostic": (
coefficient_of_variation(trade_intervals)
),
"open_30_amount_ratio": safe_ratio(
amount[open_30].sum(), total_amount
),
"close_10_amount_ratio": safe_ratio(
amount[close_10].sum(), total_amount
),
}
features = build_level2_features(market, orders, trades)
for name, value in features.items():
print(f"{name}: {value}")
raw_trade_interval_cv_diagnostic只用于展示未分时段计算造成的失真,不应直接作为建模特征。within_100ms_trade_ratio使用严格小于100毫秒的定义,当前样本结果约为65.35%。
4.3 实验结果
600009.SH在20260626的成交规模统计如下:
| 指标 | 数值 |
|---|---|
| 成交笔数 | 29209 |
| 成交量 | 15024986股 |
| 成交金额 | 349450087.92元 |
| 平均每笔成交金额 | 11963.78元 |
| 主动买入金额占比 | 47.88% |
| 主动卖出金额占比 | 52.12% |
| 主动净方向 | -4.24% |
主动净方向略微为负,说明当日主动卖出金额略高于主动买入金额。该差异较小,整体仍接近双向均衡成交。
不同成交金额层级的结果如下:
| 成交层级 | 成交笔数 | 笔数占比 | 金额占比 |
|---|---|---|---|
| 小于4万元 | 27808 | 95.20% | 53.17% |
| 4万至10万元 | 963 | 3.30% | 16.41% |
| 10万至50万元 | 399 | 1.37% | 21.07% |
| 50万元以上 | 39 | 0.13% | 9.35% |
结果说明,小额成交贡献了95.20%的成交笔数,却只贡献53.17%的成交金额;50万元以上成交只有39笔,却贡献了9.35%的成交金额。
仅使用成交笔数容易让研究者认为市场几乎完全由小单主导。加入成交金额后,可以看到少数大额交易仍然具有显著影响。
委托事件统计如下:
| 指标 | 数值 |
|---|---|
| 新增委托 | 32101笔 |
| 撤销委托 | 12428笔 |
| 状态记录 | 4笔 |
| 撤单事件占全部委托事件 | 27.91% |
| 买单撤销占全部撤单 | 59.73% |
| 卖单撤销占全部撤单 | 40.27% |
买单撤销数量高于卖单撤销数量。该结果可以描述订单流变化,但仍需结合撤单发生位置和撤单后的价格变化进行解释。
盘口统计如下:
| 指标 | 数值 |
|---|---|
| 有效一档盘口比例 | 99.94% |
| 平均买卖价差 | 0.0119元 |
| 十档盘口平均失衡 | -1.54% |
| 全盘口总量平均失衡 | -34.45% |
十档失衡接近0,而叫买总量和叫卖总量计算出的全盘口失衡明显为负。这证明十档挂单量与全盘口总量覆盖范围不同,两组字段具有互补信息。
时间结构统计如下:
| 指标 | 数值 |
|---|---|
| 开盘后30分钟成交金额占比 | 23.85% |
| 收盘前10分钟成交金额占比 | 7.50% |
| 相邻成交间隔小于100毫秒的占比 | 65.35% |
| 每分钟成交笔数CV | 0.667 |
| 原始成交间隔CV(诊断值) | 46.08 |
原始成交间隔CV诊断值达到46.08,主要受到集合竞价、大量同时间戳成交和午间休市影响。这个结果说明,直接对全天成交时间计算间隔CV会产生严重失真,因此该值不进入默认建模特征。
更合理的处理方式是分别计算以下时间段:
text
09:30:00至11:30:00
13:00:00至14:57:00
14:57:00至15:00:00
集合竞价成交也应单独统计,避免与连续竞价混合。
4.4 数据一致性验证
python
trade_volume = trades["成交数量"].sum()
trade_amount = trades["trade_amount"].sum()
trade_count = len(trades)
raw_market = pd.read_csv(
"20260626/600009.SH/行情.csv",
encoding="gb18030",
)
print(raw_market["当日累计成交量"].iloc[-1], trade_volume)
print(raw_market["当日成交额"].iloc[-1], trade_amount)
print(raw_market["成交笔数"].iloc[-1], trade_count)
输出为:
text
15024986 15024986
349450088 349450087.92
29209 29209
累计成交量、成交额和成交笔数均能与逐笔成交数据对应,说明三张表之间具有良好的一致性。
4.5 字段使用建议
| 字段等级 | 推荐字段 | 原因 |
|---|---|---|
| 第一优先级 | 成交价格、成交数量、BS标志 | 构造成交金额、主动方向和价格冲击 |
| 第一优先级 | 委托类型、委托代码、委托价格、委托数量 | 构造新增、撤单和订单流 |
| 第一优先级 | 十档买卖价量 | 构造价差、深度和盘口失衡 |
| 第一优先级 | 时间 | 构造交易节奏和时段特征 |
| 第二优先级 | 交易所委托号、叫买序号、叫卖序号 | 恢复订单生命周期 |
| 第二优先级 | 累计成交量、成交额、成交笔数 | 活跃度和数据校验 |
| 第二优先级 | 叫买总量、叫卖总量 | 完整盘口压力 |
| 第三优先级 | 开高低价、前收盘 | 日内收益和价格路径 |
| 删除候选 | 恒为0、全空或单一取值字段 | 无法提供样本区分信息 |
建模时应优先使用比例、差值、变化率、时间结构和交互特征。股票代码、日期、原始成交额等字段容易让模型记忆股票身份或流动性规模,需要谨慎处理。
五、总结
Level2数据由行情快照、逐笔委托和逐笔成交共同组成。行情快照描述订单簿状态,逐笔委托描述订单进入和退出市场的过程,逐笔成交描述买卖双方最终撮合的结果。
本文样本中最有价值的原始字段包括十档买卖价量、逐笔成交BS标志、成交价格、成交数量、委托类型、委托方向、委托价格、委托数量以及毫秒级时间。累计成交量、累计成交额和成交笔数与逐笔成交存在可推导关系,适合用于完整性校验。恒为0、全空和单一取值字段缺乏区分能力,可以在完成审计后删除。
数学指标能够把原始事件转换为可比较的行为特征。买卖价差描述交易成本,盘口失衡描述买卖深度差异,主动净方向描述成交发起方向,撤单事件占比描述委托事件构成,VWAP描述成交量加权均价,信息熵和变异系数描述交易节奏,短期有符号中间价响应描述成交方向与后续价格变化的一致程度。
Level2可以帮助研究者观察交易行为,却无法直接提供交易者身份。大额成交、小额高频、快速撤单和盘口失衡都需要结合市场状态进行解释。有效的Level2研究应从字段口径、交易机制和数据质量出发,再进入特征工程和模型训练。
参考文献
- Stoikov, S. The Micro-Price: A High Frequency Estimator of Future Prices. Quantitative Finance, 2018. DOI: https://doi.org/10.1080/14697688.2018.1489139
- Cont, R., Kukanov, A., & Stoikov, S. The Price Impact of Order Book Events. Journal of Financial Econometrics, 2014. https://arxiv.org/abs/1011.6402
- 上海证券交易所:《上海证券交易所交易规则(2023年修订)》。https://www.sse.com.cn/lawandrules/sselawsrules2025/trade/universal/c/c_20250612_10781695.shtml
- 深圳证券交易所:《深圳证券交易所交易规则(2026年修订)》。https://docs.static.szse.cn/www/lawrules/rule/trade/current/W020260424690713155663.pdf