2. 盘口信号的数据结构与特征提取

盘口信号持有期敏感性分析的第一步,不是急着算收益,而是把“盘口”这件事变成可计算的数据。很多回测结论不稳定,根源往往不在策略逻辑,而在数据层:快照时间戳对不齐、逐笔委托与成交混用、特征在生成时已经隐含未来信息。本节围绕三类原始数据——订单簿快照、逐笔委托、逐笔成交——讲清字段含义,并给出信号构造所需的关键特征与清洗要点。

三类原始数据的字段含义

订单簿快照是某一时刻买卖两侧挂单的切片。典型字段包括:快照时间戳(精确到毫秒或更高)、买一至买N的价格与数量、卖一至卖N的价格与数量,以及由这些档位派生的中间价、买卖价差。快照是“状态量”,它告诉你此刻市场长什么样,但不告诉你这个状态是怎么形成的。因此快照适合构造盘口形态类特征,如价差、盘口不平衡、深度斜率。

逐笔委托记录每一笔新进入订单簿的委托,字段通常有:委托时间、委托编号、方向(买/卖)、价格、数量、订单类型(限价/市价)。它是“流量”的起点,反映交易者主动挂单的意图。逐笔委托的关键价值在于能区分“新增挂单”与“撤单”——撤单往往以数量为负或独立撤单记录出现,是判断盘口虚假挂单的重要线索。

逐笔成交记录每一笔撮合结果,字段包括:成交时间、成交编号、价格、数量、主动方向(买方主动/卖方主动)。主动方向是判断“谁在吃单”的核心:买方主动成交意味着有人愿意以卖价买入,属于进攻性买盘。逐笔成交是信号强度最直接的来源。

三类数据的关系可以这样理解:逐笔委托是“挂上去”,逐笔成交是“打掉”,快照是两者累积后某一时刻的“截面”。信号构造时,快照给形态,逐笔给动能,二者互补。
盘口三类原始数据与特征提取链路 订单簿快照 时间戳 / 买一~买N 卖一~卖N / 价差 状态量:截面形态 逐笔委托 时间 / 编号 / 方向 价格 / 数量 / 撤单 流量:挂单意图 逐笔成交 时间 / 编号 / 价格 数量 / 主动方向 流量:进攻动能 关键特征层(按数据来源分组) 价差 / 盘口不平衡 深度斜率 / 挂单集中度 委托流不平衡 / 撤单率 新增挂单强度 主动买卖比 / 成交强度 大单占比 / 成交方向 清洗与对齐要点 时间戳统一时区与精度 · 快照与逐笔按时间对齐 去重与异常值剔除 · 特征只用当前及历史数据,避免未来函数

信号构造所需的关键特征

从上述字段出发,盘口信号常用的特征大致分三组。第一组是盘口形态特征,来自快照:买卖价差反映流动性松紧;盘口不平衡度用(买一量−卖一量)/(买一量+卖一量)刻画即时压力;深度斜率衡量挂单量随档位递减的快慢,斜率越陡说明流动性越集中在前几档。第二组是委托流特征,来自逐笔委托:委托流不平衡统计一段时间内新增买单量与新增卖单量之差;撤单率反映挂单的“虚”程度,撤单率突然升高常意味着盘口支撑不可靠。第三组是成交动能特征,来自逐笔成交:主动买卖比、单位时间成交强度、大单成交占比,直接刻画资金进攻方向。

以盘口不平衡度为例,假设某时刻买一价10.00元挂8000股,卖一价10.01元挂2000股,则不平衡度为(8000−2000)/(8000+2000)=0.6,偏向买方。但若紧接着逐笔委托显示买方在10.00元的挂单被大量撤掉,这个0.6就具有欺骗性。这正是需要把快照与逐笔结合的原因:形态给出“看起来怎样”,流量给出“是否可信”。

清洗要点:决定信号是否可复现

清洗不是可选项,而是信号有效性的前提。第一,时间对齐。快照与逐笔的时间戳精度可能不同,必须统一到同一时区和精度,并明确“快照时刻之前发生的逐笔才可用于该快照的特征计算”,否则会引入未来信息。第二,去重与异常值。逐笔数据可能因重传出现重复记录,需按成交编号或委托编号去重;价格为0、数量为负(非撤单场景)等异常记录应剔除或标记。第三,缺失档位处理。快照若只有五档而策略需要十档,不能简单补零,应标记为缺失并让特征计算跳过该样本。第四,特征生成时点。所有滚动窗口特征(如过去30秒主动买卖比)必须严格使用窗口结束时刻之前的数据,窗口边界要写死在代码里,避免“用未来算现在”。

常见错误:用当日全天的平均撤单率去标准化每一笔撤单,这实际上用了未来数据。正确做法是使用滚动历史窗口,或只用截至当前时刻的累计统计。

一个可执行的特征构造示例

下面给出以Python伪代码表达的特征构造骨架,重点在时间对齐与窗口边界,而非具体库的选择。示例参数(30秒窗口、五档快照)属于教学假设,实际使用需按标的流动性调整。

# 假设 snapshots 与 trades 均已按时间升序排列
# 快照字段: ts, bid_px[5], bid_sz[5], ask_px[5], ask_sz[5]
# 成交字段: ts, price, size, side  # side: 'B'买方主动 / 'S'卖方主动

WINDOW = 30  # 秒,教学示例参数

def imbalance(snap):
    b, a = snap['bid_sz'][0], snap['ask_sz'][0]
    return (b - a) / (b + a) if (b + a) > 0 else None

def active_ratio(trades, t_end, window=WINDOW):
    # 只取 (t_end - window, t_end] 区间,严格不含未来
    lo = t_end - window
    buy = sum(x['size'] for x in trades if lo < x['ts'] <= t_end and x['side']=='B')
    sell = sum(x['size'] for x in trades if lo < x['ts'] <= t_end and x['side']=='S')
    return buy / (buy + sell) if (buy + sell) > 0 else None

# 对每个快照时刻生成特征,trades 需先按 ts 建立索引以提升效率
for snap in snapshots:
    f_imb = imbalance(snap)
    f_ar  = active_ratio(trades, snap['ts'])
    # 二者组合成信号输入,缺失则跳过该样本

这段骨架体现了两个关键约束:窗口区间写成左开右闭 (t_end - window, t_end],确保只用历史;分母为0时返回 None 而非强行填0,让下游明确知道该样本特征缺失。实际工程中还应记录每个特征的生成时间戳与所用数据版本,便于在持有期敏感性分析中复现同一批信号。

把数据层做扎实之后,后续讨论不同持有期下信号衰减、换手成本与收益分布才有意义。字段含义清楚、特征口径统一、清洗规则固定,是整条敏感性分析链路可信的底座。