2. 行情数据源与文件格式解析

逐笔回放的第一道门槛不是回放算法,而是数据本身。拿到一份Level-2逐笔文件,如果不知道每一列是什么、时间戳精度到哪一位、成交与委托如何配对,后面的撮合还原和委托簿重建就无从谈起。本节围绕“文件里到底有什么、怎么读进来、用什么读”三个问题展开,为后续的逐笔同步回放打好数据底座。

一、逐笔成交与逐笔委托的字段结构

沪深两市的Level-2逐笔数据在字段命名上略有差异,但核心语义一致。逐笔成交(Trade)描述的是“某一笔撮合已经发生”,关键字段包括:证券代码、成交时间(精确到毫秒或10毫秒)、成交价格、成交数量、成交金额、买卖方向标识(部分数据源用BS标志,部分用主动方方向)、成交编号(用于去重与排序)。逐笔委托(Order)描述的是“某一笔委托进入撮合队列”,关键字段包括:证券代码、委托时间、委托价格、委托数量、买卖方向、委托编号、委托类型(限价/市价/本方最优等)。

需要特别注意的是,逐笔委托中的“委托编号”与逐笔成交中的“买方委托编号/卖方委托编号”是关联两套数据的桥梁。上交所Level-2逐笔成交通常带有买方委托号和卖方委托号,深交所则通过成交编号与委托编号的对应关系还原。若数据源缺少这一关联字段,就只能退化为按价格优先、时间优先的近似撮合,回放精度会明显下降。因此解析前必须先确认:成交记录是否携带双边委托号,这直接决定后续委托簿重建采用精确匹配还是近似匹配。

二、编码方式与存储格式

常见的逐笔数据存储格式有三类。第一类是纯文本CSV,字段以逗号或竖线分隔,首行可能带表头,编码多为GBK(老版本通达信导出)或UTF-8(券商API导出)。GBK编码下中文表头容易出现乱码,读取时应显式指定编码,例如Python中pd.read_csv(path, encoding='gbk')。第二类是二进制定长记录,如部分柜台系统导出的.dat文件,每条记录固定字节数,需要按结构体偏移解析,优点是体积小、读取快,缺点是可读性差、字段顺序依赖文档。第三类是压缩归档,如按日切分的.zip或.gz文件,通常内含多个CSV,需要先解压再逐文件读取。

时间字段的编码也值得留意:有的数据源用“HHMMSSmmm”整数表示,有的用“自开盘起的毫秒数”,还有的用“YYYYMMDDHHMMSS”长整型。解析时应统一转换为毫秒级时间戳,并明确时区与交易时段边界,避免跨午休时段排序错乱。

逐笔数据解析链路:从原始文件到可回放内存结构 原始文件 trade_20240603.csv order_20240603.csv 编码:GBK / UTF-8 分隔符:逗号 / 竖线 可能为 .zip 归档 读取 解析与规范化 指定编码读取 字段重命名统一 时间转毫秒时间戳 成交↔委托编号关联 按时间排序去重 输出 可回放内存结构 trades[]:价格、数量、方向、时间 orders[]:价格、数量、方向、委托号 orderMap:委托号 → 委托对象 统一时间轴(毫秒) 按证券代码分组索引 逐笔成交 / 逐笔委托 关键字段对照(示例) 字段 逐笔成交(Trade) 逐笔委托(Order) 解析要点 证券代码 600000 600000 统一为6位字符串 时间 093015250 093015120 HHMMSSmmm → 毫秒 价格 10.52 10.52 保留两位小数 数量 300 500 单位:股 方向 B / S B / S 统一为主动方方向 编号 买/卖委托号 委托编号 关联两套数据的桥梁

三、解析工具选型

工具选型取决于数据规模与回放精度要求。对于单日单只股票、百万行以内的CSV,Python的pandas配合dtype显式声明即可胜任,读取时把价格列设为float32、数量列设为int32,可显著降低内存占用。对于全市场多日数据(动辄数GB),建议使用DuckDB或Polars做列式读取与过滤,先按证券代码和时间范围裁剪,再进入回放引擎。若数据为二进制定长记录,Python的struct模块或NumPy的fromfile配合自定义dtype是更直接的选择。

回放引擎侧,若追求低延迟与高吞吐,C++配合内存映射文件(mmap)是常见方案;若以教学和快速验证为主,Python + NumPy数组 + 二分查找定位时间切片已经足够。选型时不必追求“最强”,而应关注三点:能否正确处理编码、能否按时间有序输出、能否保留委托号关联字段。

四、环境准备与一次最小可运行解析

以Python为例,建议建立独立虚拟环境,安装pandas、numpy、pyarrow(用于Parquet缓存)。下面是一段最小解析示例,假设文件为GBK编码、竖线分隔、无表头:

import pandas as pd

cols = ['code','time','price','volume','direction','trade_no','buy_order_no','sell_order_no']
df = pd.read_csv('trade_20240603.csv', sep='|', names=cols,
                 encoding='gbk', dtype={'code':str,'price':'float32','volume':'int32'})

# 时间 HHMMSSmmm → 当日毫秒偏移
t = df['time'].astype(str).str.zfill(9)
df['ms'] = (t.str[:2].astype(int)*3600 + t.str[2:4].astype(int)*60
            + t.str[4:6].astype(int))*1000 + t.str[6:].astype(int)

df = df.sort_values(['code','ms']).drop_duplicates(subset=['trade_no'])
print(df[['code','ms','price','volume','direction']].head())

运行后应观察三点:行数是否与源文件记录数一致(去重前)、ms列是否在开盘时段内单调不减、direction取值是否只有预期的几种。若出现大量NaN,多半是分隔符或编码判断错误;若时间列出现负值或跳变,说明时间格式假设不成立,需要回到原始文件抽样核对。

建议在解析完成后立即将规范化结果缓存为Parquet格式,按“日期/证券代码”分区。后续回放调试时直接读取Parquet,可避免反复解析原始CSV,单次加载时间通常能从数十秒降到秒级。此为工程经验性建议,具体耗时随数据规模与硬件而异。
不要假设所有数据源的“方向”字段语义一致:有的以买方主动为B,有的以委托方向为B。解析阶段必须结合数据源文档确认,并在字段映射表中固定下来,否则回放出的成交方向会整体反转,委托簿重建随之失真。

完成上述步骤后,你应当得到两份结构清晰、时间有序、带委托号关联的内存表。它们就是逐笔同步回放的输入契约:成交表回答“发生了什么”,委托表回答“谁在排队”,两者通过委托号与统一毫秒时间轴对齐,后续的撮合还原与委托簿演进才有可靠依据。