2. 买卖价差的数据结构与获取

要识别买卖价差异常扩大,第一步不是写判断规则,而是把“价差”这个数字从行情数据里正确地取出来。很多初学者直接拿行情软件上显示的“买一价、卖一价”相减,却忽略了行情层级、时间戳口径、字段单位与数据来源的差异,导致算出的价差在不同数据源之间不可比,异常判断自然失真。本节围绕 Level-1 与 Level-2 两类行情的字段构成,讲清楚价差在数据结构中“长什么样”,以及从交易所接口、历史数据库、数据供应商三条路径获取逐笔与快照报价数据时需要注意的关键点。

一、Level-1 行情:单一最优报价下的价差

Level-1 行情只提供最优一档买卖报价,即买一价(Bid Price)与买一量(Bid Size)、卖一价(Ask Price)与卖一量(Ask Size),外加最新成交价、成交量、成交笔数等汇总字段。对价差而言,最核心的两个字段就是买一价与卖一价,价差定义为:

spread = ask_price_1 - bid_price_1
相对价差 = spread / mid_price,其中 mid_price = (bid_price_1 + ask_price_1) / 2

这里必须先明确方向约定:本文统一采用“卖方报价减买方报价”,因此正常市场中价差恒为非负数;若出现负值,说明数据存在错位、延迟拼接或字段映射错误,本身就是需要排查的信号。Level-1 快照通常按固定频率(如每 3 秒)推送,字段结构大致如下(以常见快照表为例,字段名为示例):

字段含义单位/口径
symbol证券代码含市场后缀
timestamp快照时间交易所时间,毫秒或纳秒
bid_price_1 / bid_size_1买一价 / 买一量价格按最小价位单位,量为股/手
ask_price_1 / ask_size_1卖一价 / 卖一量同上
last_price / volume最新成交价 / 累计成交量用于辅助校验

Level-1 的优点是数据量小、获取门槛低,适合做全市场扫描和粗筛;缺点是看不到盘口深度,当买一或卖一被瞬间抽空时,Level-1 只能看到价差跳变的结果,无法判断是“某一档撤单”还是“整条盘口变薄”。因此 Level-1 更适合作为异常扩大的第一层触发器,而不是最终定性依据。

二、Level-2 行情:多档盘口与逐笔数据

Level-2 行情在 Level-1 基础上提供多档买卖盘(常见 5 档、10 档,部分市场提供 20 档甚至全深度),以及逐笔委托与逐笔成交。价差分析中真正有价值的不只是买一卖一,而是整条盘口的形状。常用字段包括各档的 bid_price_i、bid_size_i、ask_price_i、ask_size_i(i 从 1 到 N),以及逐笔成交中的成交价、成交量、成交方向(主动买/主动卖)。

基于多档数据,可以构造比单一价差更有解释力的指标,例如:

  • 盘口深度:买一至买五量之和与卖一至卖五量之和,用于判断价差扩大是否伴随流动性整体撤离。
  • 加权中间价:按各档量加权,比简单中间价更稳健,适合作为相对价差的分母。
  • 档位缺口:相邻档位价格间隔是否异常,例如买一与买二之间出现远超最小价位的空档,往往意味着盘口被大额撤单打薄。

下图展示了同一时刻 Level-1 与 Level-2 的字段关系,以及价差在两类数据结构中的位置:

Level-1 与 Level-2 行情字段结构与价差计算 Level-1 快照(最优一档) bid_price_1 ask_price_1 bid_size_1 ask_size_1 Level-2 多档盘口 + 逐笔 bid_price_1..N ask_price_1..N bid_size_1..N 逐笔成交/委托 价差 = ask_price_1 − bid_price_1 相对价差 = 价差 ÷ 中间价(中间价 = (买一 + 卖一) / 2) 交易所接口(实时推送) 历史数据库(快照/逐笔) 数据供应商(清洗后)

三、三条获取路径的差异与取舍

交易所接口是最原始、延迟最低的来源,通常以行情网关或 API 形式提供实时推送,字段最贴近原始定义,但需要自行处理断线重连、序列号校验、时间戳对齐等问题。以常见的逐笔快照接口为例,返回结构中往往包含序列号与交易所时间,必须用序列号去重、用交易所时间而非本地接收时间做排序,否则跨源合并时会出现“时间倒流”。

历史数据库适合回测与复盘,通常以快照表加逐笔表的形式存储。使用时要注意两点:一是快照频率是否固定,若交易所只在盘口变化时推送,则相邻快照的时间间隔本身携带信息,不能简单按等间隔处理;二是价格字段是否已按最小价位单位归一化,不同市场的 tick size 不同,直接比较原始整数价格会引入偏差。

数据供应商提供清洗、对齐后的数据,字段命名更统一,但存在“二次加工”风险:部分供应商会对异常快照做平滑或剔除,导致真实的价差瞬间扩大被抹掉。做异常识别时,应优先确认供应商是否保留了原始快照,必要时用交易所原始数据做交叉验证。

无论走哪条路径,落地时都应先做一次字段体检:确认买卖方向没有颠倒、价格单位一致、时间戳时区统一、缺失值与零值有明确约定。这四项检查能排除大部分“假异常”。

四、一个可复用的取数与校验流程

假设我们要对某只股票构建分钟级价差序列,可以按以下方式落地。首先从快照数据中按交易所时间排序,取每分钟最后一个快照作为该分钟的盘口状态;然后计算价差与相对价差,并记录该分钟内的快照数量与盘口总深度。示例参数(仅为教学假设):某股票买一价 10.00 元、卖一价 10.02 元,则价差 0.02 元,中间价 10.01 元,相对价差约 0.20%;若某分钟相对价差升至 0.80% 且买五量之和下降超过一半,则标记为候选异常点。

# 伪代码:快照取数与价差计算
df = snapshots[snapshots.symbol == "600XXX"]
df = df.sort_values("exchange_ts").drop_duplicates("seq_no")
df["spread"] = df["ask_price_1"] - df["bid_price_1"]
df["mid"] = (df["ask_price_1"] + df["bid_price_1"]) / 2
df["rel_spread"] = df["spread"] / df["mid"]
df["depth5"] = df[[f"bid_size_{i}" for i in range(1,6)]].sum(axis=1) \
             + df[[f"ask_size_{i}" for i in range(1,6)]].sum(axis=1)
minute = df.set_index("exchange_ts").resample("1min").last().dropna()

校验环节重点看三件事:价差是否出现负值(方向或拼接错误)、相对价差是否在无成交的静默时段异常放大(可能是盘口被抽空而非真实交易行为)、以及快照数量骤降是否与价差跳变同时发生(提示数据中断而非市场变化)。只有把字段口径和数据来源的差异控制住,后续的异常扩大识别才有可靠的基础。