3. 历史状态的数据获取与清洗

十档盘口历史状态可视化的第一步,不是画图,而是把“原始行情”变成“可信快照”。行情源通常以逐笔委托、逐笔成交或定时快照的形式落盘,文件可能是 CSV、Parquet,也可能存放在 ClickHouse、DuckDB 或时序数据库中。原始数据往往存在档位缺失、价格异常、同一时间戳重复推送等问题。如果直接拿原始记录绘图,盘口深度会忽厚忽薄,价格轴会跳变,甚至出现买一价高于卖一价的“交叉盘口”。因此本节的核心任务是:定义一条确定性的数据管道,让同一份原始数据每次运行都产出同一份清洗后的十档快照。

一、先明确“一条快照”的数据契约

在写任何代码之前,先固定输出结构。十档快照的每一行应对应“一个标的、一个时间戳、一个完整盘口状态”,字段至少包括:标的代码、快照时间戳(统一到毫秒或微秒)、买一至买十的价格与数量、卖一至卖十的价格与数量,以及数据来源标识。价格用定点整数(如“分”或“0.0001 元”为单位)存储,避免浮点误差;数量用整数股或手。建议再加两个派生字段:bid_levels_valid 与 ask_levels_valid,记录该侧实际有效的档位数,便于后续可视化时区分“真实空档”和“数据缺失”。

时间戳必须统一时区与精度。若原始文件混用交易所本地时间与 UTC,先在管道入口统一转换为同一时区,并保留原始时间戳字段以便追溯。排序键固定为 (symbol, ts, seq),其中 seq 是同一时间戳内的推送序号,用于消除重复与确定顺序。

二、提取:从文件与数据库读取十档快照

文件型数据源常见为按日分区的 CSV 或 Parquet。以 Parquet 为例,读取时只投影需要的列,避免把逐笔明细整表载入内存:

import duckdb

con = duckdb.connect()
con.execute("""
CREATE TABLE raw_snap AS
SELECT symbol, ts, seq,
       bid_px_1, bid_sz_1, ..., bid_px_10, bid_sz_10,
       ask_px_1, ask_sz_1, ..., ask_px_10, ask_sz_10
FROM read_parquet('market/snapshots/date=2024-06-03/*.parquet')
WHERE symbol IN ('600519', '000001')
""")

数据库型数据源则按分区条件下推过滤,例如 WHERE trade_date = '2024-06-03' AND symbol = '600519',并利用 (symbol, ts) 索引排序读取。无论哪种来源,都应在读取阶段记录行数、时间范围、标的不重复数,作为后续校验的基线。这一步的产物是“原始快照表”,不做任何修改,保证可回溯。

三、清洗:缺失档位、异常价格与重复记录

缺失档位分两种情况。第一种是某档价格或数量为空,但更深档位有值,这通常是上游截断或字段错位,应将该侧从首个空档起全部标记为无效,而不是用 0 填充——0 价会被误读为“该档存在但价格为零”。第二种是某侧不足十档(如涨停时卖盘为空),这是真实市场状态,应保留有效档位并把 ask_levels_valid 置为实际数量。判断依据是:价格单调性是否被破坏。买盘价格应随档位递增而递减,卖盘应递增;一旦出现反向,即视为该档之后无效。

异常价格包括:价格为 0 或负数、价格超出当日涨跌停范围、买一价大于等于卖一价(交叉盘口)、相邻档位价差超过合理阈值(如超过前收的 10%)。处理策略不是简单删除整行,而是先标记异常类型,再决定:交叉盘口若仅个别时间点出现,可标记 crossed=true 保留供排查;价格越界则整行隔离到 reject 表并记录原因。所有隔离记录都要带原始行号与时间戳,便于回查。

重复记录指同一 (symbol, ts, seq) 出现多次。常见于文件重传或数据库补数。去重规则应确定:按 ingest_time 保留最新一条,或按内容哈希保留首条。二者选其一并写入管道文档,不能随批次变化。去重后再次校验行数变化,若单日去重比例超过 5%,应触发告警,说明上游可能重复推送。

清洗顺序建议固定为:类型转换 → 去重 → 档位有效性判定 → 价格异常标记 → 隔离输出。顺序变化会导致统计口径不一致,破坏可复现性。

四、形成可复现的管道与校验

把上述步骤固化为“原始层 → 清洗层 → 快照层”三层,每层落盘并带版本号与运行参数(输入路径、过滤条件、代码提交号)。快照层按 symbol 与日期分区,输出 Parquet,并生成一份质量报告:总行数、去重删除数、档位无效行数、价格异常行数、交叉盘口行数、时间戳跨度。只要输入与参数相同,报告中的数字应完全一致,这就是可复现的判据。

十档快照数据管道:原始层 → 清洗层 → 快照层 原始层 raw_snap Parquet / CSV 按日分区 数据库按 trade_date 下推 字段:symbol, ts, seq bid/ask_px_sz_1..10 只读不改,保留可回溯 清洗层 clean_snap ① 类型与时区统一 ② 按 (symbol,ts,seq) 去重 ③ 档位有效性判定 ④ 价格异常标记 顺序固定,保证口径一致 快照层 snap_10 按 symbol + 日期分区 Parquet + 版本号 bid/ask_levels_valid 质量报告 quality.json 同输入同参数 → 同输出 隔离区 reject_snap 价格越界 / 交叉盘口 / 字段错位 保留原始行号、时间戳、异常原因 异常记录分流 质量报告字段 总行数 / 去重删除数 档位无效数 / 价格异常数 可复现校验(每次运行比对) · 输入路径与过滤条件哈希一致 · 代码提交号一致 · 去重比例 ≤ 5%,否则告警 · 买盘价格随档位递减、卖盘递增 · 时间戳单调不减 · 报告数字逐项相等即判定通过

上图把三层结构与分流关系放在一起:原始层只读,清洗层按固定顺序处理,异常记录进入隔离区而不是被静默丢弃,快照层与质量报告同时产出。实际运行时,建议把清洗逻辑写成纯函数,输入是原始表与参数,输出是快照表与报告,不依赖当前时间或随机数。这样在复盘某一天的盘口异常时,可以精确重跑并得到相同结果。

五、一个可操作的检查清单

检查项判断依据不通过时的动作
时间戳精度与时区全表统一到毫秒,无混用时区在入口统一转换并记录原值
去重键唯一(symbol, ts, seq) 无重复按 ingest_time 保留最新并记录删除数
档位单调性买盘递减、卖盘递增从首个破坏点起标记该侧无效
价格范围在当日涨跌停区间内整行隔离并记录原因
交叉盘口买一价 < 卖一价标记 crossed,保留供排查
可复现性两次运行报告数字完全一致检查是否引入时间、随机或未固定排序

完成这一步后,得到的快照表才适合进入可视化环节:每一行都代表一个结构完整、口径一致的盘口状态,档位有效数明确,异常有据可查。后续绘制历史盘口热力图或深度演化动画时,图形上的每一次变化都能对应到真实的市场状态或已记录的清洗决策,而不是数据噪声。