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”长整型。解析时应统一转换为毫秒级时间戳,并明确时区与交易时段边界,避免跨午休时段排序错乱。
三、解析工具选型
工具选型取决于数据规模与回放精度要求。对于单日单只股票、百万行以内的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,多半是分隔符或编码判断错误;若时间列出现负值或跳变,说明时间格式假设不成立,需要回到原始文件抽样核对。
完成上述步骤后,你应当得到两份结构清晰、时间有序、带委托号关联的内存表。它们就是逐笔同步回放的输入契约:成交表回答“发生了什么”,委托表回答“谁在排队”,两者通过委托号与统一毫秒时间轴对齐,后续的撮合还原与委托簿演进才有可靠依据。