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 的字段关系,以及价差在两类数据结构中的位置:
三、三条获取路径的差异与取舍
交易所接口是最原始、延迟最低的来源,通常以行情网关或 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()
校验环节重点看三件事:价差是否出现负值(方向或拼接错误)、相对价差是否在无成交的静默时段异常放大(可能是盘口被抽空而非真实交易行为)、以及快照数量骤降是否与价差跳变同时发生(提示数据中断而非市场变化)。只有把字段口径和数据来源的差异控制住,后续的异常扩大识别才有可靠的基础。