4. 数据采集与原始文件组织

逐笔盘口回放的数据集,本质上是一批按时间顺序排列的原始行情快照与逐笔成交记录。在进入清洗、对齐和回放之前,原始文件必须先做到三件事:找得到、对得上、验得回。找得到,指任意一个交易日的任意合约数据都能通过路径直接定位;对得上,指文件名、目录层级与文件内容中的日期、市场、合约字段完全一致;验得回,指文件被压缩、搬运或部分损坏后,能够通过校验和判断完整性并重新采集。本节围绕这三点,给出可落地的命名规范、目录分层、压缩与校验方案,以及存储与批量加载策略。

一、命名规范:让文件名自带元数据

逐笔数据文件的命名应遵循“固定字段顺序 + 固定分隔符 + 固定时间粒度”的原则,避免出现 rb2405_20240102_final_v2(1).csv 这类无法程序化解析的名称。推荐采用如下结构:

{市场}_{合约代码}_{数据类型}_{交易日}_{采集批次}.{扩展名}

示例:
SHFE_rb2405_tick_20240102_b01.csv.gz
SHFE_rb2405_order_20240102_b01.csv.gz
CFFEX_IF2403_trade_20240102_b01.csv.gz
DCE_m2405_snapshot_20240102_b01.csv.gz

各字段含义需要固定下来:市场用交易所简称大写(SHFE、DCE、CZCE、CFFEX、INE、GFEX);合约代码保留交易所原始大小写与数字,不做额外转换;数据类型区分 tick(逐笔行情)、order(委托队列)、trade(逐笔成交)、snapshot(盘口快照);交易日统一为 YYYYMMDD,且必须使用交易日而非自然日,夜盘数据归属其对应的下一交易日;采集批次用 b01、b02 表示同一交易日的多次采集或补采,便于区分主采与补采。扩展名统一为 .csv.gz,压缩格式在名称中显式体现,避免加载端猜测。

需要特别强调交易日口径。以上期所螺纹钢为例,2024年1月2日晚上21:00开始的夜盘属于2024年1月3日交易日,因此该段数据应命名为 20240103,而不是 20240102。若命名口径与内容口径不一致,后续按日期批量加载时会出现夜盘数据错配到前一日的严重问题。建议在采集脚本中统一由“行情时间戳 → 交易日”的换算函数生成日期字段,禁止手工填写。

二、目录分层:按日期、市场、合约三级组织

目录结构应同时满足“按日批量加载”和“按合约回溯”两种访问模式。推荐采用日期在前、市场居中、合约在后的三级分层:

raw/
  20240102/
    SHFE/
      rb2405/
        SHFE_rb2405_tick_20240102_b01.csv.gz
        SHFE_rb2405_order_20240102_b01.csv.gz
      cu2403/
        SHFE_cu2403_tick_20240102_b01.csv.gz
    CFFEX/
      IF2403/
        CFFEX_IF2403_trade_20240102_b01.csv.gz
  20240103/
    SHFE/
      rb2405/
        ...

日期放在第一层,是因为回放与校验通常以交易日为单位成批进行,按日遍历目录即可一次性拿到当日全部市场、全部合约的文件;市场放第二层,便于按交易所分别处理交易时段、涨跌停规则和字段差异;合约放第三层,使单合约的历史回溯只需跨日期目录取同名子路径。每个合约目录下还可以放置一个 _manifest.json,记录该合约当日各数据类型的文件清单、行数、首末时间戳与校验和,供加载端先读清单再决定是否解压。

目录层级不宜过深。若在合约层之下再按小时或按采集批次建目录,会导致路径过长且批量加载时目录遍历开销上升。采集批次已经体现在文件名中,无需再单独建目录。

三、压缩与校验和:完整性的两道保障

逐笔数据体量大、重复模式强,采用 gzip 压缩通常可获得 5~10 倍的体积缩减,且 Python、pandas、DuckDB 等工具均可直接读取 .csv.gz,无需先解压落盘。压缩时应保持“一个逻辑文件对应一个压缩文件”,不要把多个合约打包进同一个 tar 包,否则无法按合约单独加载,也破坏了校验的粒度。

校验和推荐使用 SHA-256,为每个压缩文件生成一条记录,汇总到当日的校验清单文件中:

# checksums_20240102.sha256
9f2c...e1  SHFE/rb2405/SHFE_rb2405_tick_20240102_b01.csv.gz
4a71...b8  SHFE/rb2405/SHFE_rb2405_order_20240102_b01.csv.gz
c3d0...27  CFFEX/IF2403/CFFEX_IF2403_trade_20240102_b01.csv.gz

校验应在两个时机执行:采集落盘后立即计算并写入清单,作为“采集时基线”;批量加载前用 sha256sum -c checksums_20240102.sha256 复核,任何一条 FAILED 都说明文件在传输或存储中发生了变化,应触发重采而不是带着损坏文件继续回放。除文件级校验和外,还应在清单中记录每个文件的行数与首末时间戳,形成“文件级 + 内容级”双重核对:校验和保证字节未变,行数与时间戳范围保证内容语义合理。

逐笔原始数据:采集 → 命名 → 分层 → 压缩校验 → 批量加载 行情采集端 逐笔委托 / 成交 盘口快照 时间戳 → 交易日换算 命名规范 市场_合约_类型_交易日_批次 SHFE_rb2405_tick_ 20240102_b01.csv.gz 夜盘归属下一交易日 目录分层 raw/日期/市场/合约/ 20240102/SHFE/rb2405/ _manifest.json 清单 压缩与校验 gzip 单文件压缩 SHA-256 校验和 行数 + 首末时间戳 原始数据存储区(只读归档) 按交易日分区 · 保留采集批次 · 校验清单随日归档 主存储 + 冷备副本,禁止在原始区直接改写文件 批量加载与校验闸门 ① 读 _manifest.json 获取文件清单 ② sha256sum -c 复核 → ③ 校验行数与时间戳范围 ④ 通过后按日期/合约流式读取 .csv.gz 校验失败分支 FAILED / 行数不符 → 标记该批次异常 → 触发重采,不带病回放

四、存储策略:原始区只读、分层保留、可追溯

原始数据区应设为只读归档:采集程序只负责写入新文件,任何清洗、对齐、重采样都输出到独立的派生目录(如 derived/),不得回写原始区。这样做的目的是保证“回放结果可复现”——当回放出现异常时,能够确认问题来自原始数据还是处理逻辑。存储上建议采用“主存储 + 冷备副本”的双份策略,主存储用于日常批量加载,冷备按交易日打包归档,校验清单与数据文件一同归档,确保多年后仍能验证完整性。

保留周期可按用途分层:近 3 个月的逐笔数据保留在主存储,支持高频回放与调试;更早的数据迁移到冷备,仅保留校验清单和 _manifest.json 索引,需要时再按日期回取。无论哪一层,校验清单必须与数据同生命周期保存,否则归档数据将失去可验证性。

五、批量加载的落地要点

批量加载脚本应遵循“先清单、后校验、再读取”的顺序,而不是直接 glob 所有 .csv.gz。先读当日 _manifest.json 拿到文件清单与预期行数,再执行校验和复核,最后用流式读取逐文件加载并累加行数,与清单中的预期值比对。下面是一个可参考的加载骨架:

import gzip, json, hashlib, pandas as pd
from pathlib import Path

day_dir = Path("raw/20240102")
manifest = json.loads((day_dir / "_manifest.json").read_text(encoding="utf-8"))

for item in manifest["files"]:
    fp = day_dir / item["path"]
    digest = hashlib.sha256(fp.read_bytes()).hexdigest()
    assert digest == item["sha256"], f"校验和不符: {fp}"

    rows = 0
    for chunk in pd.read_csv(fp, compression="gzip", chunksize=200_000):
        rows += len(chunk)
        # 此处进入回放/对齐处理
    assert rows == item["rows"], f"行数不符: {fp} 期望{item['rows']} 实际{rows}"

这段骨架体现了两个判断依据:校验和不符说明文件字节层面已损坏或被替换;行数不符说明文件虽能解压但内容不完整(例如采集进程中途被杀导致尾部截断)。两类异常都应记录到当日的加载日志中,并阻止该批次进入回放,待补采后重新校验。只有清单、校验和、行数、时间戳范围四项全部通过,才认为该交易日原始数据具备可追溯、可批量加载的条件。

常见误区:把多个合约合并成一个按日大文件以“减少文件数”。这会破坏按合约回溯能力,也使单文件损坏影响整日数据。逐笔数据的正确粒度是“单合约、单数据类型、单交易日、单批次”,文件数量多但每个文件职责单一,反而更利于校验与重采。