1. 盘口快照数据结构与字段校验

为什么先做字段校验

逐笔盘口快照是后续所有事件识别(价差异常、量比突变、撤单潮等)的原料。原始快照常见四类脏数据:字段缺失(如某档 bid_px 为空)、时间戳乱序或重复、价量为负、价格档位不合理(买价高于卖价、档位不递增、价格偏离涨跌停区间)。若不先清洗,后续阈值统计会被污染。本例对单只股票的快照序列做四类校验,输出结构化异常清单。

快照数据结构与校验规则

约定一条快照记录包含:ts(毫秒时间戳)、bid_px[1..5]/bid_vol[1..5]、ask_px[1..5]/ask_vol[1..5]、last_px、limit_up、limit_down。校验规则如下:

  • 完整性:必需字段非空,五档价量齐全。
  • 时间戳单调性:ts 严格递增,重复或回退均记异常。
  • 非负性:所有价格、成交量 ≥ 0。
  • 档位合理性:买价从一档到五档递减,卖价递增;买一 ≤ 卖一(允许相等表示无成交挂单);价格落在 [limit_down, limit_up] 内。
原始快照序列 单只股票 · 逐笔 ts=093000120 bid1=10.20 ask1=10.21 ts=093000120 bid1=10.20 ask1=10.21 ts=093000420 bid1=-0.01 ask1=10.22 ts=093000720 bid1=10.25 ask1=10.24 四类校验器 ① 完整性 必需字段非空 · 五档齐全 ② 时间戳单调性 ts 严格递增 · 重复/回退 ③ 非负性 价格/量 ≥ 0 ④ 档位合理性 买降卖升 · 买一≤卖一 · 限价内 异常字段清单 ts 重复时间戳 规则② bid_px[1] 负值 -0.01 规则③ bid_px[1] 买一 > 卖一 规则④ ask_px[3] 档位未递增 规则④ bid_vol[5] 字段缺失 规则① 每条记录:ts + 字段名 + 规则编号 + 原因 干净快照 → 下游事件识别 异常记录隔离,不进入统计 保留原始行号便于回溯 逐条判定

实现:单条快照校验函数

用 Python 实现,返回该条记录的异常列表;空列表表示通过。价格档位用 bid_px 递减、ask_px 递增判断,并检查买一卖一关系与涨跌停区间。

from dataclasses import dataclass, field

REQUIRED = ["ts", "last_px", "limit_up", "limit_down"]
for i in range(1, 6):
    REQUIRED += [f"bid_px{i}", f"bid_vol{i}", f"ask_px{i}", f"ask_vol{i}"]

def check_snapshot(rec, prev_ts=None):
    """返回异常列表,每项为 (字段, 规则, 原因)。"""
    errs = []

    # ① 完整性
    for f in REQUIRED:
        if f not in rec or rec[f] is None:
            errs.append((f, "完整性", "字段缺失"))

    # ② 时间戳单调性
    ts = rec.get("ts")
    if ts is not None and prev_ts is not None and ts <= prev_ts:
        errs.append(("ts", "单调性", f"ts={ts} 未大于前值 {prev_ts}"))

    # ③ 非负性
    for f in REQUIRED:
        if f == "ts":
            continue
        v = rec.get(f)
        if v is not None and v < 0:
            errs.append((f, "非负性", f"负值 {v}"))

    # ④ 档位合理性
    bid_px = [rec.get(f"bid_px{i}") for i in range(1, 6)]
    ask_px = [rec.get(f"ask_px{i}") for i in range(1, 6)]
    if all(v is not None for v in bid_px):
        for i in range(4):
            if bid_px[i] < bid_px[i + 1]:
                errs.append((f"bid_px{i+1}", "档位", "买价未递减"))
    if all(v is not None for v in ask_px):
        for i in range(4):
            if ask_px[i] > ask_px[i + 1]:
                errs.append((f"ask_px{i+1}", "档位", "卖价未递增"))
    if bid_px[0] is not None and ask_px[0] is not None and bid_px[0] > ask_px[0]:
        errs.append(("bid_px1", "档位", "买一高于卖一"))
    lo, hi = rec.get("limit_down"), rec.get("limit_up")
    if lo is not None and hi is not None:
        for f in [f"bid_px{i}" for i in range(1, 6)] + [f"ask_px{i}" for i in range(1, 6)]:
            v = rec.get(f)
            if v is not None and not (lo <= v <= hi):
                errs.append((f, "档位", f"{v} 超出[{lo},{hi}]"))
    return errs

批量校验与异常清单输出

按时间顺序遍历快照,维护 prev_ts 实现单调性检查,把每条异常连同行号写入清单。

def validate_series(snapshots):
    report = []
    prev_ts = None
    for idx, rec in enumerate(snapshots):
        errs = check_snapshot(rec, prev_ts)
        for f, rule, reason in errs:
            report.append({"row": idx, "ts": rec.get("ts"),
                           "field": f, "rule": rule, "reason": reason})
        if rec.get("ts") is not None:
            prev_ts = rec["ts"]
    return report

# 示例数据(教学构造)
snaps = [
    {"ts": 93000120, "last_px": 10.20, "limit_up": 11.22, "limit_down": 9.18,
     "bid_px1": 10.20, "bid_vol1": 500, "bid_px2": 10.19, "bid_vol2": 300,
     "bid_px3": 10.18, "bid_vol3": 200, "bid_px4": 10.17, "bid_vol4": 100,
     "bid_px5": 10.16, "bid_vol5": 80,
     "ask_px1": 10.21, "ask_vol1": 400, "ask_px2": 10.22, "ask_vol2": 350,
     "ask_px3": 10.23, "ask_vol3": 250, "ask_px4": 10.24, "ask_vol4": 150,
     "ask_px5": 10.25, "ask_vol5": 90},
    {"ts": 93000120, "last_px": 10.20, "limit_up": 11.22, "limit_down": 9.18,
     "bid_px1": 10.20, "bid_vol1": 500, "bid_px2": 10.19, "bid_vol2": 300,
     "bid_px3": 10.18, "bid_vol3": 200, "bid_px4": 10.17, "bid_vol4": 100,
     "bid_px5": 10.16, "bid_vol5": 80,
     "ask_px1": 10.21, "ask_vol1": 400, "ask_px2": 10.22, "ask_vol2": 350,
     "ask_px3": 10.23, "ask_vol3": 250, "ask_px4": 10.24, "ask_vol4": 150,
     "ask_px5": 10.25, "ask_vol5": 90},
    {"ts": 93000420, "last_px": 10.20, "limit_up": 11.22, "limit_down": 9.18,
     "bid_px1": -0.01, "bid_vol1": 500, "bid_px2": 10.19, "bid_vol2": 300,
     "bid_px3": 10.18, "bid_vol3": 200, "bid_px4": 10.17, "bid_vol4": 100,
     "bid_px5": 10.16, "bid_vol5": 80,
     "ask_px1": 10.22, "ask_vol1": 400, "ask_px2": 10.22, "ask_vol2": 350,
     "ask_px3": 10.23, "ask_vol3": 250, "ask_px4": 10.24, "ask_vol4": 150,
     "ask_px5": 10.25, "ask_vol5": 90},
    {"ts": 93000720, "last_px": 10.20, "limit_up": 11.22, "limit_down": 9.18,
     "bid_px1": 10.25, "bid_vol1": 500, "bid_px2": 10.19, "bid_vol2": 300,
     "bid_px3": 10.18, "bid_vol3": 200, "bid_px4": 10.17, "bid_vol4": 100,
     "bid_px5": 10.16, "bid_vol5": 80,
     "ask_px1": 10.24, "ask_vol1": 400, "ask_px2": 10.22, "ask_vol2": 350,
     "ask_px3": 10.23, "ask_vol3": 250, "ask_px4": 10.24, "ask_vol4": 150,
     "ask_px5": 10.25, "ask_vol5": 90},
]

for r in validate_series(snaps):
    print(r)

结果判读与通过条件

对上述示例数据,预期输出(教学构造,非实测):

行号字段规则原因
1ts单调性ts=93000120 未大于前值 93000120
2bid_px1非负性负值 -0.01
2bid_px1档位-0.01 超出[9.18,11.22]
3bid_px1档位买一高于卖一
3ask_px1档位卖价未递增

通过条件:清单为空即该序列可直接进入下游;非空时按 row 隔离异常记录,其余记录保留。注意第 2 行 bid_px1=-0.01 会同时触发非负性与区间两条规则,这是预期行为——同一字段可命中多条规则,便于定位根因。校验器只做判定不做修复,修复策略(丢弃、前值填充、标记)由下游按业务决定。

时间戳单调性依赖遍历顺序,务必先按 ts 排序再校验,否则会把乱序误报为回退。若原始数据本身乱序,应先记录乱序事件再排序,两者分开统计。