2. 字段结构总览
逐笔成交(Trade-by-Trade,常简称逐笔成交或 Tick 成交)是行情数据中最细粒度的成交记录,每一条记录对应交易所撮合引擎中一次真实发生的撮合结果。理解它的字段结构,是后续高效读取、过滤与聚合的前提。本节不讨论具体文件格式的字节布局,而是先建立一张“字段地图”:一条逐笔成交记录通常由哪些字段组成、它们各自回答什么问题、命名上有哪些约定俗成的规范,以及哪些字段最容易在解析时被误读。
一条逐笔成交记录回答的四个问题
无论数据来自上交所、深交所的 Level-2 逐笔成交,还是券商柜台或第三方数据商的标准化推送,一条成交记录本质上都在回答四个问题:谁在什么时间、以什么价格、成交了多少、方向如何。围绕这四个问题,典型字段可以归为四组:
- 标识与定位组:证券代码、市场类型、成交序号(逐笔序号/成交编号)、频道号或应用标识。它们用于唯一确定“这是哪只股票的第几笔成交”,是排序与去重的依据。
- 时间组:成交时间(精确到毫秒或微秒)、部分数据源还带时间戳类型标记。时间字段决定事件顺序,也是回放与延迟分析的基础。
- 价格与数量组:成交价格、成交数量(股数或手数)、成交金额(部分数据源不直接给出,需价格×数量推算)。
- 方向与属性组:买卖方向标识(主动买/主动卖/未知)、成交类型(如集合竞价成交、连续竞价成交、盘后固定价格成交)、内外盘标志等。
需要强调的是,不同数据源对“方向”的定义并不一致:有的用 BS 标志(B/S),有的用买卖盘序号(BidOrderNo/AskOrderNo)反推,有的直接给出“主动方”字段。解析前必须先确认该字段的语义,否则后续的主动买卖统计会整体反向。
字段命名规范:同名不同义、同义不同名
逐笔成交字段没有全球统一的命名标准,实际工程中最大的坑往往不是“字段缺失”,而是“字段名相同但含义不同”或“含义相同但字段名不同”。常见情况有三类:
第一类是命名风格差异。交易所原始接口多用缩写,如 SecurityID、TradeTime、TradePrice、TradeVolume、BSFlag;而部分数据商标准化后改为下划线风格,如 symbol、trade_time、price、volume、side。读取时应先建立一张“源字段 → 标准字段”的映射表,而不是在代码里到处硬编码字段名。
第二类是单位差异。成交数量有的以“股”为单位,有的以“手”为单位(1 手 = 100 股);价格有的以“元”为单位,有的以“分”为整数存储。若不做统一,后续计算成交额会出现 100 倍或 10000 倍的偏差。建议在解析层就统一为“股”和“元”,并在映射表中显式标注换算系数。
第三类是语义差异。最典型的是 BSFlag:在部分数据源中,B 表示“主动买入”(即买方主动吃掉卖盘),S 表示“主动卖出”;而在另一些数据源中,它表示“该笔成交对应的是买盘队列还是卖盘队列”。两者在统计主动买卖净额时结果可能完全相反。工程上的做法是:不要凭字段名猜语义,而是用一笔已知方向的成交(例如涨停板上的大单)做一次小样本校验,确认后再批量解析。
字段之间的依赖关系
这些字段并非彼此独立,而是存在明确的依赖与推导关系,理解这一点对高效读取很重要:
- 成交序号依赖证券代码与市场类型:序号通常只在“同一市场、同一证券”内连续,跨证券比较序号没有意义。因此排序键应是(市场,代码,序号)三元组,而不是单独一个序号。
- 成交金额可由价格与数量推导:
Amount = Price × Volume(单位统一后)。若数据源同时给出金额,可用它做一致性校验,偏差超过阈值说明单位或精度处理有误。 - 方向可由买卖盘序号反推:当数据源不直接给
BSFlag时,可通过比较买盘序号与卖盘序号的大小关系,结合逐笔委托数据推断主动方,但这要求成交与委托数据能按序号对齐。 - 成交类型影响时间语义:集合竞价阶段的成交时间往往统一标记为开盘或收盘时刻,若与连续竞价成交混排,按时间排序会失真,需先按成交类型分组。
一个可复用的字段映射示例
下面给出一个简化的字段映射配置示例,用于把不同来源的逐笔成交字段统一到标准结构。示例中的字段名与换算系数属于教学假设,实际应以所用数据源的官方文档为准。
# 逐笔成交字段映射(示例,非实测配置)
standard_fields:
symbol: { source: ["SecurityID", "symbol"], type: string }
market: { source: ["MarketType", "exchange"], type: string }
trade_index: { source: ["TradeIndex", "trade_seq"], type: int64 }
trade_time: { source: ["TradeTime", "trade_time"], type: time }
price: { source: ["TradePrice", "price"], type: float, unit: yuan }
volume: { source: ["TradeVolume", "volume"], type: int64, unit: share, factor: 100 }
amount: { source: ["TradeAmount", "amount"], type: float, unit: yuan, optional: true }
bs_flag: { source: ["BSFlag", "side"], type: string, enum: {B: 主动买, S: 主动卖, N: 未知} }
trade_type: { source: ["TradeType", "trade_type"], type: string }
这张映射表的价值在于:读取层只认标准字段名,源字段差异被隔离在配置中;单位换算(如 factor: 100 表示源数据以手为单位、需乘 100 转为股)集中在一处,避免散落在业务代码里。实际使用时,建议先用少量样本数据跑一遍映射,检查 Amount 与 Price × Volume 是否吻合、BSFlag 的枚举值是否落在预期集合内,再放开全量读取。
把字段结构理清之后,后续的读取效率优化才有明确的着力点:知道哪些字段是定长、哪些是变长,哪些字段可以延迟解析,哪些字段必须参与排序与去重。字段地图不是背下来的,而是在映射表与校验中逐步固化的。