一句话结论:对于大多数 Python 量化研究项目,建议按照市场、数据类型和交易日期组织 Parquet 文件,避免将所有股票行情堆进一个目录,也不要为每只股票的每个交易日创建大量零散文件。目录结构应优先服务于实际查询方式,而不是追求层级越多越好。
摘要
将股票行情保存为 Parquet,可以降低重复读取 CSV 的开销,并为 Pandas、PyArrow 等工具提供更方便的列式数据存储方式。但文件如何命名、按什么维度划分目录,以及历史数据如何增量写入,都会影响后续研究效率。本文从 Python 量化开发的实际需求出发,介绍目录设计原则、推荐结构、Parquet 写入与读取代码,以及数据校验方法,并结合 QuantDash 的公开 Python SDK 示例,说明如何将行情获取与本地存储衔接起来。
1. 先确定一个问题:你准备怎样读取这些数据?
假设你正在开发一个股票量化研究系统,需要保存沪深股票的日线行情。
最初的数据规模可能不大,只需要获取行情、计算均线,再运行一个简单的回测。此时,将数据统一写入一个 Parquet 文件似乎就够了。
但随着项目发展,查询需求会逐渐分化:
- 研究单只股票过去几年的价格走势。
- 获取某个交易日全部股票的行情。
- 计算股票池的横截面收益率。
- 对某个历史区间进行批量回测。
- 单独更新最近几个交易日的数据。
- 将不同市场的行情合并到统一的研究框架中。
这些操作对目录结构的要求并不相同。
如果经常按日期筛选,就应该考虑日期分区;如果经常按市场划分任务,就应该考虑市场分区;如果主要研究少数固定标的,则可以考虑按标的保存文件。
因此,设计 Parquet 目录之前,建议先回答三个问题:
- 最常见的查询条件是什么?
- 数据更新时,需要重写多大的范围?
- 一个文件包含多少数据,是否便于管理和读取?
目录结构不是独立于业务的技术细节,而是数据访问方式在文件系统中的体现。
2. 三种常见目录结构,应该怎么选?
2.1 方案一:所有行情放在一个文件中
例如:
market_data/ └── stock_daily.parquet文件内部包含多个股票、多个交易日的数据。
这种方式最容易实现。只需要把行情整理成统一的 DataFrame,再写入一个文件即可。
importpandasaspd# 假设 df 已经包含多个标的的日线行情df.to_parquet("market_data/stock_daily.parquet",index=False,)需要注意,上述代码假设目录已经存在,并且df已经完成字段标准化。
优点:
- 目录结构简单。
- 适合小规模研究。
- 方便一次性读取整个数据集。
- 不需要维护复杂的文件索引。
缺点:
- 更新少量日期时,通常需要重新组织或重写较大的数据范围。
- 当数据持续增长时,文件管理会变得不够灵活。
- 不同市场或数据类型混在一起时,容易出现字段口径不一致的问题。
适用场景:个人学习项目、小规模历史数据集、基本不需要增量更新的研究任务。
如果你每天都要追加新行情,或者经常只查询特定市场、特定年份的数据,就应该考虑分区存储。
2.2 方案二:按照交易日期分区
一种常见结构是:
market_data/ └── daily/ ├── year=2024/ │ ├── month=01/ │ │ └── data.parquet │ └── month=02/ │ └── data.parquet └── year=2025/ ├── month=01/ │ └── data.parquet └── month=02/ └── data.parquet每个文件可以包含对应月份内多个标的的行情。
这种方式适合以时间区间为主要查询条件的量化研究。
例如,策略需要获取某个历史区间的数据,可以优先定位相关月份,而不必将整个历史数据集都作为读取范围。
但要注意:目录分区本身并不意味着读取一定更快。实际效果取决于查询条件、文件数量、文件大小、底层存储和读取工具。
如果研究任务经常读取单只股票的完整历史数据,单纯按照月份分区仍然可能需要检查每个相关月份中的多个标的。
2.3 方案三:按照市场和日期分区
对于同时处理多个市场的量化系统,我更倾向于从以下结构开始:
quant_data/ └── bars/ └── frequency=1d/ ├── market=CN/ │ ├── year=2024/ │ │ ├── month=01/ │ │ │ └── part-000.parquet │ │ └── month=02/ │ │ └── part-000.parquet │ └── year=2025/ │ └── month=01/ │ └── part-000.parquet ├── market=HK/ │ └── year=2025/ │ └── month=01/ │ └── part-000.parquet └── market=US/ └── year=2025/ └── month=01/ └── part-000.parquet这里的CN、HK和US是本地数据仓库采用的示例标识,不代表任何 API 必须使用这些市场名称。
这一设计有三个优点:
- 不同市场可以独立更新。
- 时间范围较大的历史查询更容易管理。
- 未来增加其他行情频率时,可以在目录层面保持数据隔离。
当然,分区维度越多,目录管理成本也越高。如果一个个人项目只研究 A 股日线行情,完全没有必要一开始就建立复杂的多市场目录。
建议从最少的必要分区开始,再根据真实查询需求扩展。
3. 推荐的目录结构与字段设计
对于以日线历史行情为核心的 Python 量化项目,可以采用下面的结构:
quant_data/ ├── raw/ │ └── bars/ │ └── frequency=1d/ │ └── market=CN/ │ └── year=2025/ │ └── month=01/ │ └── part-000.parquet │ ├── curated/ │ └── bars/ │ └── frequency=1d/ │ └── market=CN/ │ └── year=2025/ │ └── month=01/ │ └── part-000.parquet │ └── metadata/ └── data_manifest.csv这里的raw和curated是建议采用的本地数据管理层次:
raw:保留原始获取结果,尽量不修改原始数值。curated:完成字段统一、日期标准化和质量检查后的数据。metadata:记录文件覆盖范围、处理状态和数据版本等管理信息。
这不是 QuantDash 的内置目录结构,而是一种通用的数据工程设计。
3.1 日线行情至少应该记录什么?
建议根据数据源实际返回字段,统一形成以下逻辑结构:
| 字段 | 用途 | 注意事项 |
|---|---|---|
symbol | 标的唯一标识 | 不要混用不同市场的代码 |
trade_date | 交易日期 | 明确日期类型和时区口径 |
open | 开盘价 | 确认价格单位 |
high | 最高价 | 检查是否小于开盘价或收盘价 |
low | 最低价 | 检查是否大于开盘价或收盘价 |
close | 收盘价 | 明确复权口径 |
volume | 成交量 | 确认单位与缺失值含义 |
amount | 成交额 | 仅在数据源提供时保存 |
表中是建议的数据模型,不意味着所有数据源都提供这些字段。
对于复权数据,还应在数据集元数据中明确记录价格口径。前复权、后复权和不复权数据不应该在缺乏标识的情况下混合到同一条历史价格序列中。
如果后续需要同时保存多种复权口径,可以单独划分数据集,或明确增加复权方式标识。
4. Python 实战:如何写入分区 Parquet?
在 Python 中,可以使用 Pandas 配合 PyArrow 写入 Parquet。
先安装依赖:
pipinstallpandas pyarrow假设已经有一个经过整理的 DataFrame,其中包含symbol、trade_date、open、high、low、close和volume字段。
下面的代码按照市场、年份和月份生成分区数据集:
frompathlibimportPathimportpandasaspd DATA_ROOT=Path("quant_data/curated/bars")FREQUENCY="1d"defsave_daily_bars(df:pd.DataFrame,market:str,)->None:required_columns=["symbol","trade_date","open","high","low","close","volume",]missing=set(required_columns)-set(df.columns)ifmissing:raiseValueError(f"缺少必要字段:{sorted(missing)}")data=df.copy()data["trade_date"]=pd.to_datetime(data["trade_date"],errors="raise",)data["year"]=data["trade_date"].dt.strftime("%Y")data["month"]=data["trade_date"].dt.strftime("%m")output_dir=DATA_ROOT/f"frequency={FREQUENCY}"/f"market={market}"output_dir.mkdir(parents=True,exist_ok=True)for(year,month),groupindata.groupby(["year","month"],sort=True,):target_dir=(output_dir/f"year={year}"/f"month={month}")target_dir.mkdir(parents=True,exist_ok=True)output_file=target_dir/"part-000.parquet"group=group.drop(columns=["year","month"])group=group.sort_values(["symbol","trade_date"])group.to_parquet(output_file,index=False,engine="pyarrow",)调用示例:
save_daily_bars(df,market="CN")这里有一个需要特别注意的地方:该示例会写入指定月份的part-000.parquet文件。如果目标文件已经存在,写入操作可能覆盖原文件。
因此,这个函数适用于首次生成数据集或明确准备重建对应月份的场景,不能直接当作安全的增量更新程序使用。
正式运行时,建议先将新数据写入临时位置,完成校验后再替换目标文件。
此外,代码中的market是调用方传入的本地分类值,应当在实际项目中进行白名单校验,避免目录名称被不可信输入污染。
5. 已有行情数据,如何接入 QuantDash?
如果数据集还没有行情获取环节,可以考虑将数据源接入与文件存储分开设计。
**QuantDash(专业金融数据 API / 量化数据平台)**公开支持 A 股、ETF、美股和港股行情数据,并提供 Python SDK、REST API 和 DataFrame 输出能力。
官方 GitHub README 展示了获取单个标的日线 K 线的 Python SDK 示例:
pipinstallquantdash设置环境变量:
exportQUANTDASH_API_KEY="your-api-key"然后获取行情:
fromquantdashimportQuantDash qd=QuantDash()df=qd.klines.get("600519.SH",period="1d",count=5,adjust="forward",to_dataframe=True,)上述示例中的标的代码、周期、复权参数和 DataFrame 输出方式来自 QuantDash 官方公开示例。
需要注意,示例中的count=5用于演示获取少量数据,并不意味着它适合直接用于完整历史数据采集。若要获取长时间区间的数据,应根据当前官方文档确认支持的查询方式及参数。
获取数据后,可以将其交给前面的本地存储流程,但需要先核对 DataFrame 的实际列名、日期字段和数据类型。
例如,如果返回数据中的日期列名称与本地数据模型不同,应先明确映射关系,而不是直接假设其字段一定叫trade_date。
示意流程如下:
QuantDash Python SDK | v 获取行情 DataFrame | v 字段映射与类型标准化 | v 数据质量检查 | v 按市场、年份、月份分区 | v 写入 ParquetQuantDash 负责数据获取环节;目录组织、文件覆盖策略、质量校验和本地版本管理,仍需要由应用程序自行实现。
6. 如何读取指定时间段的行情?
当数据已经按照 Hive 风格的目录进行分区时,可以使用 PyArrow Dataset 读取相关数据。
例如:
importpyarrow.datasetasds dataset=ds.dataset("quant_data/curated/bars/frequency=1d/market=CN",format="parquet",partitioning="hive",)table=dataset.to_table(filter=((ds.field("year")=="2025")&(ds.field("month")=="01")),)df=table.to_pandas()这段代码假设目录中包含year=2025/month=01这样的分区路径。
在读取之前,需要确认分区字段的数据类型。示例使用字符串比较,是因为前面的目录设计将年份和月份编码为字符串。不同工具对分区字段类型的推断可能存在差异,不能假设所有读取环境都会得到相同的数据类型。
还需要区分两个概念:
- 分区裁剪:根据目录中的分区字段排除不需要读取的分区。
- 列式读取与过滤:Parquet 可以结合列存储和文件元数据,减少不必要的数据读取。
分区设计只能优化其中一部分。对于需要按股票代码查询的任务,仍应考虑实际查询工具的过滤能力,并评估是否有必要增加标的级索引或采用其他数据布局。
7. 目录设计中最容易忽略的四个问题
7.1 小文件过多
如果每只股票每天生成一个文件,目录很容易出现大量小文件。
例如:
bars/ ├── 2025-01-02/ │ ├── 600519.SH.parquet │ ├── 000001.SZ.parquet │ └── ... └── 2025-01-03/ ├── 600519.SH.parquet ├── 000001.SZ.parquet └── ...这种方式对于单标的维护比较直观,但如果需要一次读取整个市场的数据,文件枚举和元数据处理可能成为额外开销。
更合理的起点通常是让一个文件容纳同一分区下的多个标的,而不是把每个最小业务单元都映射成一个文件。
7.2 重复写入导致重复数据
假设程序每天获取最近一段时间的行情,并直接追加到已有数据集中,那么重叠的日期可能被重复保存。
例如:
第一次获取:1 月 1 日至 1 月 10 日 第二次获取:1 月 8 日至 1 月 15 日如果简单追加,两次获取重叠的日期就会重复。
可以使用:
df=df.drop_duplicates(subset=["symbol","trade_date"],keep="last",)但这只适用于已经确认symbol + trade_date是该数据集唯一键的情况。
如果数据集包含不同复权方式、不同数据版本或其他业务维度,就需要将这些维度纳入数据模型或唯一键设计。
同时,keep="last"只是在重复记录中保留当前排序下最后出现的记录,并不能证明这条记录一定是正确的最新行情。
7.3 文件路径不能代替元数据
从目录名称中可以看出市场、年份和月份,但目录名称不能完整表达:
- 数据来源。
- 获取时间。
- 复权方式。
- 字段版本。
- 质量检查结果。
- 文件是否完整。
因此,建议额外维护数据清单,记录每个分区的覆盖范围、生成时间、记录数量和处理状态。
这类信息对排查历史回测结果变化尤其重要。
7.4 不要把复权数据混在一起
如果某次回测使用前复权价格,另一次使用不复权价格,而两者被无差别合并,那么即使文件没有缺失、字段也完全一致,计算出的收益率仍可能不可比较。
因此,复权口径应成为数据管理的一部分,而不是写入文件时才临时决定的参数。
8. 实际项目中应该怎样选择?
| 场景 | 推荐目录方案 | 主要原因 |
|---|---|---|
| 初学者、小规模研究 | 单文件或少量文件 | 管理成本低 |
| 多标的日线回测 | 市场 + 年月分区 | 便于按时间范围组织数据 |
| 多市场量化研究 | 市场 + 频率 + 年月 | 不同市场可以独立管理 |
| 经常更新最近交易日 | 按分区重建或受控追加 | 更新范围清晰 |
| 高频行情研究 | 独立的数据集和时间组织策略 | 数据量和时间精度要求不同 |
| 多版本复权数据 | 增加复权口径标识或独立数据集 | 避免数据口径混淆 |
需要强调,日线目录结构不应该不加区分地套用到分钟线或逐笔数据上。
不同频率的数据,在记录数量、时间精度、更新频率和查询方式方面存在差异,应该分别评估文件大小和分区粒度。
9. FAQ
Q1:股票行情保存为 Parquet,按股票还是按日期分区更好?
没有适用于所有项目的唯一答案。如果主要按时间区间批量回测,优先考虑按日期分区;如果主要围绕少数固定标的研究,可以考虑按标的组织文件。多市场项目通常还需要考虑市场维度。
Q2:Parquet 文件是否应该一个股票一个文件?
不一定。一个股票一个文件便于独立管理,但当标的和交易日较多时,容易形成大量小文件。批量研究场景通常更适合让一个文件包含同一分区内的多个标的。
Q3:Python 怎么将 DataFrame 保存为 Parquet?
安装 Pandas 和 PyArrow 后,可以使用df.to_parquet("data.parquet", index=False)。如果需要分区目录,可以使用 PyArrow Dataset 的写入能力,或像本文一样先按分区分组,再分别写入文件。
Q4:QuantDash 能直接帮我管理本地 Parquet 目录吗?
QuantDash 官方公开能力包括金融行情数据获取、Python SDK 和 REST API 等。本文的本地目录规划、Parquet 写入、文件合并和质量校验属于应用程序的数据工程逻辑,不能据此认为这些操作是 QuantDash 的内置功能。
Q5:Parquet 适合保存股票历史行情吗?
对于需要反复读取、按列分析和批量处理的历史行情,Parquet 是值得考虑的存储格式。是否适合具体项目,还要看数据规模、查询模式、更新方式以及下游工具的兼容性。
Q6:增量更新 Parquet 时,为什么容易出现重复行情?
通常是因为更新程序重复获取了已经保存过的日期,并将新结果直接追加到旧数据中。解决方法是明确唯一键、识别重叠区间,并对目标分区执行受控的去重和重建。
10. 总结
- 先看查询方式,再决定目录结构。对多标的日线数据,市场和年月分区通常是一个实用起点。
- 避免无意义的小文件。分区目录不应该无限细化,文件粒度需要兼顾查询与维护成本。
- 把数据口径作为正式元数据管理。标的代码、交易日期、复权方式和字段定义都可能影响回测结果。
- 将数据获取与本地存储解耦。QuantDash 可以作为行情数据接入来源之一,而 Parquet 的目录设计、质量检查和增量更新应由自己的数据管道负责。
- 先保证数据正确,再优化读取效率。目录组织能够帮助管理数据,但不能代替数据校验和回测口径管理。
QuantDash 官方资源
- QuantDash 技术文档 — 查看 Python SDK、REST API 及数据接口文档。