匡醍量化|大富翁量化 数据是量化研究的地基:选什么源、存成什么格式、用什么引擎查。这个专题回答选型级问题——A 股数据源怎么选(Tushare / AkShare / QMT / BaoStock)、存储格式怎么定(HDF5 / Parquet / CSV)、数据库用什么(DuckDB / SQLite / ClickHouse),并给出实测对比与迁移路径。
这是「数据与存储」专题的选型长文:把散落在十几篇旧文里的经验串成一条决策路径。读完你应该能回答两个问题——我的数据从哪来;我的数据存到哪、用什么查。
| 你的场景 | 数据源 | 存储 | 查询 |
|---|---|---|---|
| 入门 / A股日线研究 | AkShare + BaoStock | Parquet(按年分区) | DuckDB |
| 稳定生产 / 多品种研究 | Tushare Pro 或券商源 | Parquet + 复权因子表 | DuckDB |
| QMT 实盘 | XtQuant(行情 + 交易) | 本地增量缓存(Parquet) | DuckDB |
| 多人协作 / 服务化 | 上述任一 | Parquet | ClickHouse |
Tushare Pro——老牌、字段规范、社区大。日线、财务、指数、期货覆盖全,付费档位的稳定性和文档明显更好。适合作为研究数据的"稳定底座"。缺点:积分/付费门槛、部分接口限频,实时性一般(适合研究,不适合盘中)。
AkShare——免费开源、接口极多(股票、基金、期货、宏观、另类数据都有)。零成本、更新快,是很多人的第一站。缺点也很明确:它本质上是爬虫集合,上游页面改版时接口会间歇性失效;字段命名不统一,生产使用必须自己包一层清洗、重试与 schema 校验。
BaoStock——免费、接口少而稳,历史日线和财务数据干净够用。适合"不想维护爬虫、只要干净日线"的场景;代价是品种和维度有限。
XtQuant / QMT——券商侧行情与交易接口,全推行情、Level-1/Level-2(视券商开通情况),并且能下单。实盘链路它基本是唯一正解:行情质量高、交易可控。缺点:需要券商权限、依赖客户端在线、历史数据要自己落库;另外券商服务端与官网文档的版本不一致是常态,遇到问题以实际返回为准。
结论:研究起步用 AkShare / BaoStock,重要数据用 Tushare Pro 兜底;实盘必须走 QMT / XtQuant。多源并存时,用统一 schema 落库(见第四节),不要让下游脚本知道数据从哪来。
三个候选:CSV、HDF5、Parquet。
实践建议:日线、财务这类"批量写、批量读"的数据用 Parquet;研究脚本的中间产物随意,但仓库层统一 Parquet。
目录布局比格式更重要,推荐两种:
daily/year=2026.parquet # 全市场按年一个文件:适合全市场扫描
daily/symbol=600519/year=2026.parquet # 按标的分区:适合单票长历史与增量更新
日线量级(每年几百万行)用前者,文件少、全市场查询快;Tick 级以上或需要频繁增量更新的用后者,改一只票只碰一个文件。
read_parquet() 查文件,零部署、零导入。单机做全市场扫描非常快,是名副其实的"研究员的数据库"。结论:元数据放 SQLite;研究分析用 DuckDB 直接查 Parquet;规模化、服务化再考虑 ClickHouse——不要一上来就上服务。
下面是 Moonshot 系列里一直在用的两个类(完整实现见《Moonshot 实战》系列配套 store.py,544 行,生产验证过),精简了注释,逻辑一字未改。
先是交易日历——数据仓库的"表头",所有增量更新都靠它判断缺哪天:
class CalendarModel:
"""日历模型:is_open / prev 两列,按日期索引"""
def get_trade_dates(self, start, end):
mask = (df.index >= start) & (df.index <= end) & (df.is_open == 1)
return df[mask].index.tolist()
# 还有 is_trade_date / prev_trade_day / get_next_trade_day /
# floor / ceil / shift / delta,这里只列最常用的
然后是统一存储:以 (date, asset) 为唯一键,polars 读写 Parquet,输入输出仍是 pandas。最核心的是 get_and_fetch——本地缺哪段,就调你给的 fetch_data_func 补哪段,应用层永远不用操心数据全不全:
store = ParquetUnifiedStorage(store_path, calendar, fetch_data_func=fetch_bars)
barss = store.get_and_fetch(start, end) # 没有就拉、拉完就存、存完就读
dv_store = ParquetUnifiedStorage(store_path, calendar, fetch_data_func=fetch_dv_ttm)
dv_ttm = dv_store.get_and_fetch(start, end)
写入侧三条硬规则(都在 append_data 里):
# 1. (date, asset) 去重,保留最新 → 增量更新幂等,重复跑不产生脏数据
deduped = combined.unique(subset=["date", "asset"], keep="last").sort(["date", "asset"])
# 2. 按 date+asset 排序写入 → 范围查询天然连续
# 3. lz4 压缩落盘 → 日线量级读写都在毫秒级
sorted_df.collect().write_parquet(self._file_path, compression="lz4")
查询侧直接用 polars lazy scan(零导入、零服务):
lazy_df = pl.scan_parquet(self._file_path)
df = (lazy_df.filter((pl.col("asset").is_in(["600519", "000001"]))
& (pl.col("date") >= start))
.collect().to_pandas())
三个工程细节值得照抄:
CalendarModel,而不是文件存在性——停牌日、节假日不会被误判为缺失;fetch_data_func 是一个可插拔参数,换 Tushare / AkShare / XtQuant 只需换这一个函数,get_and_fetch 的语义不变——这就是第一节"多源并存,统一落库"的实现;文中的对比结论来自我们在 A 股日线与分钟线数据上的日常使用经验;具体性能数字与数据规模、字段宽度、磁盘和查询模式强相关——建议用本文的代码骨架在自己的数据上实测一遍,再定选型。
共 24 篇 · 按难度分组