4. 时间窗口划分与对齐

逐笔成交数据本身是离散、无固定节奏的事件流:一笔成交可能发生在 09:30:00.123,下一笔可能到 09:30:00.457 才出现,也可能同一毫秒内出现数十笔。要把这些事件聚合成分钟 K 线,第一步不是求和,而是回答一个更基础的问题——每一笔成交应该被归入哪一个时间窗口。窗口划分方式一旦确定,后续的开高低收、成交量、成交额、笔数统计才有统一的归属基准;窗口划分错了,聚合结果会出现跨窗口串数据、首尾分钟缺失、午休前后错位等难以察觉的问题。

本节围绕三种常用窗口划分方式展开:自然分钟窗口、滚动分钟窗口、交易所时间戳窗口,并重点处理集合竞价与午休这两个特殊时段。

一、三种窗口划分方式的定义与差异

自然分钟窗口以时钟整分钟为边界,把时间轴切成 [09:30:00, 09:31:00)、[09:31:00, 09:32:00) 这样的左闭右开区间。它的优点是边界固定、可预期,同一交易日的窗口集合与数据无关,便于跨品种、跨日期对齐。实现上通常把交易所时间戳转换为“当日第几秒”,再整除 60 得到窗口序号:

# 假设 ts 为已转换为本地交易时区的时间戳(单位:秒,含小数)
win_id = int(ts // 60)          # 自然分钟窗口序号
win_start = win_id * 60         # 窗口起始秒
# 归属规则:win_start <= ts < win_start + 60

注意左闭右开的选择必须全文统一:09:31:00.000 这一笔属于 09:31 窗口,而不是 09:30 窗口。若用右闭区间,边界笔会被重复计入相邻两个窗口。

滚动分钟窗口不以整分钟为边界,而是以某个锚点(如首笔成交时间、或固定的 09:30:00)为起点,按固定长度向后滑动。例如以 09:30:00 为锚、窗口长度 60 秒,则窗口为 [09:30:00, 09:31:00)、[09:31:00, 09:32:00)……看似与自然分钟相同,但当锚点取首笔成交时间 09:30:00.123 时,窗口变为 [09:30:00.123, 09:31:00.123),边界整体偏移。滚动窗口更适合研究“从某一事件起算的 N 秒内”的行为,但在生成标准分钟 K 线时,若锚点选择不当,会导致每根 K 线的起止时间与行情软件不一致,跨系统比对时出现系统性偏移。

交易所时间戳窗口则完全以交易所发布的行情时间字段为准,不依赖本地时钟。逐笔数据通常带有交易所撮合时间(如 SSE/SZSE 的 TradeTime),部分数据源还提供本地接收时间。二者可能相差几十毫秒甚至更多。正确做法是:窗口归属一律使用交易所时间戳,本地接收时间只用于延迟监控和乱序判断,绝不参与窗口计算。否则同一笔成交在不同机器上会被分到不同窗口,聚合结果不可复现。

三种窗口划分方式对比(以 09:30 附近为例) 自然分钟窗口(整分钟边界,左闭右开) 09:30:00 09:31:00 09:32:00 09:33:00 09:34:00 W1 W2 W3 W4 滚动分钟窗口(锚点 09:30:00.123,边界整体偏移) 09:30:00.123 09:31:00.123 09:32:00.123 09:33:00.123 09:34:00.123 R1 R2 R3 R4 交易所时间戳窗口(以 TradeTime 为准,本地接收时间不参与) 窗口归属 = f(交易所 TradeTime) 本地接收时间 → 仅用于延迟/乱序监控

二、集合竞价时段的窗口处理

A 股开盘集合竞价在 09:15–09:25 进行,09:25 产生开盘价并集中撮合,09:25–09:30 通常不接受撤单、也无连续撮合。逐笔数据中,集合竞价阶段可能出现两类记录:一是 09:25:00 附近集中产生的一批成交(开盘集合竞价成交),二是 09:15–09:25 期间的委托快照或虚拟成交。若直接按自然分钟切分,09:25 的集中成交会落入 [09:25:00, 09:26:00) 窗口,而 09:30 之后的第一笔连续竞价成交落入 [09:30:00, 09:31:00) 窗口,中间 09:26–09:30 出现空窗口。

工程上常见的处理策略有两种,需根据下游用途选择:

  • 合并入首根 K 线:把 09:25 集合竞价成交并入 09:30 这根“首分钟”K 线,作为其开盘价与初始成交量。这样首根 K 线的开盘价即当日开盘价,符合多数行情软件展示习惯。
  • 独立成竞价 K 线:为集合竞价单独生成一根标记为“集合竞价”的 K 线,时间戳记为 09:25:00,与连续竞价 K 线分开存储。适合需要区分竞价与连续竞价成交来源的研究场景。

无论哪种策略,都要在窗口元数据中显式标注该窗口的时段类型(集合竞价 / 连续竞价 / 收盘集合竞价),否则后续做成交量分布分析时会把竞价量误认为连续竞价量。

三、午休时段的边界处理

沪深市场上午连续竞价 09:30–11:30,下午 13:00–15:00(含 14:57–15:00 收盘集合竞价)。11:30:00 与 13:00:00 是两个关键边界。若简单按“当日秒数整除 60”生成窗口,11:30–13:00 之间会产生 90 个空窗口,既浪费存储,也会让按窗口序号连续遍历的代码误以为存在交易。

推荐做法是维护一张交易时段表,只对有效时段生成窗口,并用“时段内偏移”而非“当日绝对秒”计算窗口序号:

sessions = [
    ("09:30:00", "11:30:00", "continuous_am"),
    ("13:00:00", "15:00:00", "continuous_pm"),
]
# 对每笔成交,先定位所属 session,再计算 session 内偏移秒
# offset = trade_time - session_start
# win_id = session_index * 10000 + int(offset // 60)   # 加 session 前缀避免跨段冲突

边界归属仍需坚持左闭右开:11:30:00.000 的成交属于上午最后一根 K 线([11:29:00, 11:30:00) 不含 11:30:00,因此 11:30:00.000 应归入一个以 11:30:00 为起点的边界窗口,或按业务约定并入 11:29 窗口——这一点必须在实现前明确并写入文档)。实践中更稳妥的约定是:上午时段取 [09:30:00, 11:30:00),即 11:30:00.000 不计入上午;下午取 [13:00:00, 15:00:00)。若数据中确实出现 11:30:00.000 的成交(收盘集合竞价前的最后一笔),应单独记录并人工核对,而不是静默丢弃。

四、对齐校验:让窗口划分可验证

窗口划分实现完成后,不能只看“程序跑通了”,而要做三项对齐校验:

  1. 窗口总数校验:上午 120 分钟 + 下午 120 分钟 = 240 根连续竞价 K 线(不含竞价独立 K 线)。若输出数量不等于 240,说明时段表或边界处理有误。
  2. 成交量守恒校验:所有窗口成交量之和应等于当日逐笔成交量总和(允许因数据源截断存在极小差异,但差异应可解释)。
  3. 边界笔抽查:抽取 09:30:00.000、11:29:59.999、13:00:00.000 等边界附近的成交,逐笔核对归属窗口,确认左闭右开规则被一致执行。
建议在窗口元数据中同时保留 win_start、win_end、session_type、trade_count 四个字段。这样即使后续发现某根 K 线异常,也能快速定位是窗口边界问题还是聚合逻辑问题。

总结来说,窗口划分的核心不是“切时间”,而是先定义清楚边界规则、时段范围和归属依据,再让所有成交按同一套规则落位。自然分钟提供稳定可对齐的骨架,滚动分钟服务于特定锚点分析,交易所时间戳保证结果可复现;集合竞价与午休则通过时段表和显式元数据被妥善安放。把这四件事定死,分钟聚合的后续计算才有可靠的地基。