1. 逐笔CSV数据特征与读取瓶颈诊断
逐笔成交数据是量化研究中粒度最细的行情数据之一。与日线、分钟线不同,它记录的是每一笔撮合成交的明细,通常包含成交时间、证券代码、成交价格、成交数量、买卖方向、成交编号等字段。沪深两市的逐笔成交每天可达数千万笔,单只活跃股票一天就可能产生数十万条记录。当研究标的扩展到全市场、时间跨度拉长到数月甚至数年时,数据会以“按日分文件、按市场分目录”的形式组织,形成成千上万个 CSV 文件,总体积轻松达到几十 GB 甚至上百 GB。
这种数据形态决定了读取环节的三个典型压力点。第一是文件数量多:每个交易日、每个市场、甚至每只股票一个文件,打开和关闭文件的系统调用开销会累积成不可忽视的成本。第二是单文件体积大:一个交易日的全市场逐笔文件可能达到数 GB,一次性读入内存容易触发内存峰值。第三是字段解析重:时间戳、价格、数量等字段需要类型转换,字符串到数值的解析在千万行级别上会占据大量 CPU 时间。因此,在动手优化之前,必须先搞清楚时间到底花在哪里——是磁盘 I/O、CSV 解析,还是内存分配与垃圾回收。
上图把读取链路拆成“数据源—读取—解析—内存”四段,并标出三个常见瓶颈与对应的诊断工具。实际排查时,建议先用 cProfile 做函数级扫描,确认耗时集中在哪个函数;再对可疑函数用 line_profiler 做行级下钻,看具体是哪一行在“吃时间”。
下面给出一个可直接运行的诊断示例。假设数据目录 ./tick/20240102/ 下有若干逐笔 CSV,字段为 time,code,price,volume,direction。先用 cProfile 包住读取函数:
import cProfile, pstats, io
import pandas as pd
from pathlib import Path
def load_one(path):
df = pd.read_csv(path, encoding="utf-8")
df["time"] = pd.to_datetime(df["time"], format="%H:%M:%S")
df["price"] = df["price"].astype("float64")
df["volume"] = df["volume"].astype("int64")
return df
def load_all(folder):
frames = []
for p in Path(folder).glob("*.csv"):
frames.append(load_one(p))
return pd.concat(frames, ignore_index=True)
pr = cProfile.Profile()
pr.enable()
load_all("./tick/20240102/")
pr.disable()
s = io.StringIO()
pstats.Stats(pr, stream=s).sort_stats("cumtime").print_stats(15)
print(s.getvalue())
观察输出时重点看三列:ncalls 反映调用频次,tottime 是函数自身耗时,cumtime 是含子调用的累计耗时。如果 read_csv 的 cumtime 占了大头,说明瓶颈在解析与 I/O;如果 to_datetime 的 tottime 异常高,说明时间戳解析是热点;如果 concat 耗时突出,则要警惕内存拼接带来的复制开销。需要注意,cProfile 本身有开销,适合定位相对占比,不适合当作绝对性能基准。
确认了可疑函数后,用 line_profiler 做行级分析。给 load_one 加上 @profile 装饰器,然后执行 kernprof -l -v diagnose.py,输出会逐行给出 Hits、Time 和 % Time。例如可能看到 pd.to_datetime 那一行占了 60% 以上时间,这就直接指明了优化方向:改用 format 参数固定格式、或先以字符串读入再统一转换,甚至改用 engine="pyarrow" 加速解析。