4. 盘口信号样本构建实战

盘口信号持有期敏感性分析的第一步,不是算收益,而是把原始行情数据加工成“可比较、可回测、可分组”的信号样本。这一步做不干净,后面无论换多少个持有期,得到的敏感性曲线都不可信。本节围绕三个最容易出问题的环节展开:时间对齐、前视偏差、样本重叠,并给出一套可直接落地的样本构建流程。

一、明确样本的四个字段

一条盘口信号样本,本质上是一行结构化记录,至少包含四类信息:信号时间戳(信号被观测到的时刻)、信号特征(盘口快照或由其派生的指标,如买卖一档量比、委比、盘口斜率)、持有期标签(该信号在持有 h 个周期后的收益或方向)、样本标识(用于去重、分组和交叉验证的键)。持有期敏感性分析的核心变量就是 h,因此标签必须显式携带 h,而不是事后临时计算。

假设我们使用 1 分钟 K 线与逐笔盘口快照,定义:信号在 t 时刻收盘后生成,持有期为 h 分钟,则标签为 t+h 时刻收盘价相对 t 时刻收盘价的对数收益。这个定义必须在全流程中保持一致,包括后面所有持有期。

二、时间对齐:先统一时间轴,再谈信号

盘口快照、逐笔成交、K 线往往来自不同频率、不同时区甚至不同交易所时钟。常见错误是把“快照时间”和“K 线时间”直接按字符串拼接。正确做法是先建立统一的时间轴:以 K 线周期为基准,把盘口快照按“最近一个不晚于 K 线收盘时刻的快照”对齐到该根 K 线,并记录对齐偏差。若某根 K 线找不到有效快照,应标记为缺失而不是用前值无限填充。

下面的示意图展示了一条信号样本从原始数据到标签的完整链路,重点标出了三个风险点所在的位置:

原始数据源 逐笔盘口快照 tick / 3s 快照 1 分钟 K 线 OHLCV 逐笔成交 trade / 成交明细 时间对齐 统一到 K 线时间轴 快照取 ≤ 收盘时刻 记录对齐偏差 Δt 缺失标记,不无限填充 ⚠ 风险点 1 时区 / 时钟漂移 快照晚于 K 线收盘 信号特征 买卖一档量比 委比 / 盘口斜率 仅用 t 时刻及之前 数据计算 ⚠ 风险点 2 前视偏差 误用未来快照 持有期标签 r(t, h) = ln(P(t+h)/P(t)) h ∈ {1,3,5,10,20} 分钟 标签与 h 显式绑定 尾部 h 根 K 线无标签 ⚠ 风险点 3 样本重叠 相邻信号共享收益区间 输出:带持有期标签的信号样本表(示例行) signal_ts | bid_ask_ratio | slope | r_h1 | r_h3 | r_h5 | r_h10 | r_h20 | sample_id 10:01:00 | 1.82 | 0.31 | 0.0012 | 0.0021 | -0.0008 | 0.0034 | 0.0051 | 20240102_1001 注:示例数值为教学假设,非实测结果

三、前视偏差:特征只能用“当时可见”的数据

前视偏差最隐蔽的来源,是特征计算窗口越过了信号时刻。例如用 t 时刻的盘口快照生成信号,却用包含 t+1 分钟成交的滚动均值做特征。判断标准很简单:任何进入特征计算的原始记录,其时间戳必须 ≤ 信号时间戳。实操中建议在特征函数入口处强制截断数据,并保留一个“特征最大可见时间”字段,便于审计。

另一个易被忽略的点是盘口快照的“发布延迟”。交易所快照通常有毫秒到秒级延迟,若把 t 时刻快照当作 t 时刻完全可见,在极短持有期(如 h=1 分钟)下会高估信号质量。稳妥做法是给快照加一个保守延迟 δ(例如 1 秒),只用 t−δ 之前的快照生成 t 时刻信号。

四、样本重叠:持有期越长,重叠越严重

当信号按每分钟生成、持有期为 h 分钟时,相邻 h 条样本的收益区间相互重叠。h=1 时几乎无重叠,h=20 时一条样本的收益窗口覆盖了后续 19 条样本的信号时刻。这会导致样本并非独立,直接用于统计显著性检验或交叉验证会低估方差、夸大信号稳定性。

处理方式有三类,按严格程度递增:其一,在敏感性分析中如实报告重叠度(如重叠比例 = (h−1)/h),并对显著性结论保持谨慎;其二,对长持有期采用非重叠抽样,即每隔 h 条取一条样本,代价是样本量下降;其三,采用分块交叉验证(按时间分块,块长 ≥ h),避免训练集与验证集共享收益区间。三种方式没有绝对优劣,但必须在报告中写明所用方式,否则不同 h 之间的比较会失真。

五、可执行的构建流程与检查要点

把上述要点串成一条可复现的流水线,关键步骤如下(以 Python 伪代码示意,参数为教学示例):

# 1. 统一时间轴:K 线为基准,快照向后对齐
snap = snap.sort_values("ts")
snap["ts_aligned"] = snap["ts"].apply(lambda x: floor_to_minute(x))
snap = snap.groupby("ts_aligned").last()          # 取该分钟内最后一条快照
snap["lag_sec"] = (snap.index - snap["ts"]).dt.total_seconds()

# 2. 特征计算:强制截断到 t 时刻之前(含保守延迟 delta)
delta = pd.Timedelta(seconds=1)
feat = compute_features(snap[snap["ts"] <= t - delta])

# 3. 标签生成:对每个 h 分别计算,尾部 h 根置空
for h in [1, 3, 5, 10, 20]:
    df[f"r_h{h}"] = np.log(close.shift(-h) / close)

# 4. 重叠标记:记录每条样本的收益区间与重叠比例
df["overlap_ratio"] = (h - 1) / h

落地时建议逐项核对:时间对齐后是否统计了对齐偏差分布(若大量快照晚于 K 线收盘,说明对齐规则需调整);特征函数是否只读取信号时刻之前的数据;每个 h 的标签是否与 h 显式绑定、尾部是否正确置空;重叠比例是否随 h 变化被记录并在结论中披露。这四项检查通过后,得到的样本表才适合进入持有期敏感性分析。

建议在样本表中额外保留 signal_ts、lag_sec、overlap_ratio 三个审计字段。它们不参与建模,但在排查“某持有期收益异常偏高”时,往往能快速定位到对齐偏差或重叠未处理的问题。
常见误区:为了让长持有期样本量看起来充足,对 h=20 仍按每分钟生成样本且不做任何重叠说明。这样得到的敏感性曲线在短持有期与长持有期之间不具备可比性,结论容易被误读为“信号在长持有期更稳定”。

完成本节后,你应当得到一张结构清晰、字段完整、风险点已标注的信号样本表。它是后续所有持有期敏感性计算与对比的共同底座,样本构建阶段的任何妥协,都会在敏感性曲线上被放大。