2. 逐笔数据源与字段解析
逐笔数据质量监控的第一步,不是写规则,而是把“数据长什么样”彻底弄清楚。逐笔成交与逐笔委托是两类不同的消息流:前者记录“已经发生的撮合结果”,后者记录“进入撮合系统的意图”。两者在字段结构、时间戳语义和编码体系上都有明显差异,而不同交易所的数据源又各有约定。如果字段解析阶段就出现偏差,后续的完整性、时序性、方向一致性等指标都会建立在错误的基础上。
逐笔成交与逐笔委托的字段结构
以沪深两市的 Level-2 逐笔数据为参照,逐笔成交通常包含:成交序号、频道号、证券代码、成交时间、成交价格、成交数量、成交金额、买卖方向标识、买方订单号、卖方订单号等字段。逐笔委托则包含:委托序号、频道号、证券代码、委托时间、委托价格、委托数量、买卖方向、订单类型等字段。两者的关键区别在于:成交记录的是“撮合后的结果”,因此带有买卖双方的订单号,可用于与委托流做关联;委托记录的是“撮合前的意图”,因此带有订单类型编码,用于区分市价、限价、本方最优等委托方式。
在监控面板中,字段解析层需要为每条消息建立统一的内部结构,例如将交易所原始字段映射为 seq_no、symbol、event_time、price、volume、side、order_type、buy_order_id、sell_order_id 等标准字段。映射表本身应当作为监控对象:一旦某交易所新增字段或调整字段顺序,映射失败率会立即上升,这正是数据质量面板需要捕获的信号。
时间戳语义:三个时间不能混用
逐笔数据中至少存在三类时间:交易所撮合系统产生的业务时间(如成交时间、委托时间)、数据源在消息头中携带的发送时间、以及本地接收系统记录的落地时间。业务时间通常精确到毫秒甚至微秒,是判断事件先后顺序的主要依据;发送时间用于评估链路延迟;落地时间用于评估本地处理延迟。三者语义不同,绝不能混为一谈。
一个常见的错误是直接用落地时间排序逐笔成交,然后与交易所公布的成交序号比对。由于网络抖动和批量推送,落地时间可能出现乱序,导致“序号递增但时间倒退”的假象。正确的做法是:以交易所业务时间为主序,以成交序号或委托序号为次序校验,落地时间仅用于延迟统计。监控面板中应分别设置“业务时间单调性”和“落地延迟分布”两个独立指标,而不是合并成一个“时间异常”。
买卖方向标识与订单类型编码
买卖方向在不同数据源中的表达方式并不统一。有的交易所用 B/S 字符,有的用 1/2 数字,还有的用 0/1 且含义相反。更需要注意的是,逐笔成交中的“买卖方向”通常指主动方方向,即买方主动成交还是卖方主动成交,而逐笔委托中的方向则是委托本身的买卖属性。两者语义不同,不能直接比较。监控中若要校验方向一致性,应明确比较的是“成交主动方”与“对应委托方向”的关系,而不是简单比较两个字段是否相等。
订单类型编码同样因市场而异。以深市为例,逐笔委托中的订单类型可能包括限价、市价、本方最优等,编码值需要对照交易所接口文档逐一映射。若映射表缺失或错位,市价单可能被误判为限价单,进而影响后续的订单类型分布统计和异常委托识别。建议在解析层维护一张显式的编码映射表,并在监控面板中展示“未知订单类型占比”指标,一旦出现未映射的编码值,立即告警。
不同市场数据源的差异与对齐要点
沪深两市在逐笔数据的推送机制上存在差异:深市逐笔委托与逐笔成交分频道推送,沪市的部分逐笔数据则采用不同的频道划分和序号体系。这意味着同一只证券的成交与委托可能来自不同频道,序号空间彼此独立。做关联分析时,不能假设“成交序号等于委托序号”,而应通过买方订单号、卖方订单号与委托序号建立映射。
对齐工作通常分三步。第一步是字段级对齐:将各市场原始字段映射到统一内部结构,并记录映射版本。第二步是时间级对齐:以业务时间为主序,检查同一证券的成交与委托在时间上是否满足“委托时间不晚于成交时间”的基本约束。第三步是标识级对齐:利用订单号将成交记录回溯到对应的委托记录,统计“无法回溯的成交占比”。这个占比是数据质量面板中非常有价值的指标——如果某段时间该比例突然升高,往往意味着委托流丢失、订单号字段解析错误,或两路数据的频道配置不一致。
下面是一段字段映射与对齐校验的示例代码,用于说明解析层应如何落地。示例中的编码值为合理假设,实际使用需以交易所接口文档为准。
# 示例:逐笔字段映射与基础对齐校验(参数为教学假设)
FIELD_MAP = {
"成交": {"seq_no": "TradeSeq", "symbol": "Code",
"event_time": "TradeTime", "price": "Price",
"volume": "Volume", "side": "BSFlag",
"buy_order_id": "BuyOrderNo", "sell_order_id": "SellOrderNo"},
"委托": {"seq_no": "OrderSeq", "symbol": "Code",
"event_time": "OrderTime", "price": "Price",
"volume": "Volume", "side": "OrderBS",
"order_type": "OrderType"}
}
SIDE_MAP = {"B": "BUY", "S": "SELL", "1": "BUY", "2": "SELL"}
ORDER_TYPE_MAP = {"1": "LIMIT", "2": "MARKET", "U": "BEST_OWN"}
def normalize(msg, kind):
m = FIELD_MAP[kind]
rec = {k: msg.get(v) for k, v in m.items()}
rec["side"] = SIDE_MAP.get(str(rec["side"]), "UNKNOWN")
if kind == "委托":
rec["order_type"] = ORDER_TYPE_MAP.get(str(rec["order_type"]), "UNKNOWN")
return rec
# 对齐校验:委托时间应不晚于对应成交时间
def check_align(trade, order):
if order is None:
return "ORPHAN_TRADE" # 成交无法回溯到委托
if order["event_time"] > trade["event_time"]:
return "TIME_INVERSION" # 委托晚于成交,疑似解析或频道错配
return "OK"
这段代码的价值不在于逻辑复杂,而在于它把“字段映射”和“对齐判断”变成了可监控的输出:UNKNOWN 方向、UNKNOWN 订单类型、ORPHAN_TRADE、TIME_INVERSION 都可以直接作为面板上的计数指标。实际运行时,建议按证券、按频道、按分钟窗口分别统计这些计数,并与总量做比值,形成可比较的比率指标,而不是只看绝对数量。
最后需要强调一点:字段解析不是一次性工作。交易所接口会升级,频道配置会调整,新订单类型会出现。监控面板应当把“解析异常”当作一等公民指标来对待,与完整性、时序性指标并列展示。只有当解析层的未知编码率、孤儿成交率、时间倒挂率都处于受控范围时,后续的逐笔质量分析才具备可信的起点。