📌 摘要 / 快速解答
使用 Pandas 读取批量量化数据时出现
MemoryError的根本原因在于一次性将全量数据加载至内存 。核心解决方案是分块处理(Chunking) + 数据类型优化(Downcasting) + 列式存储(Parquet) 。QuantDash Python SDK 原生支持将批量 K 线数据直接输出为 Pandas DataFrame,配合klines.batch接口与本地 Parquet 缓存,可在极简代码内实现海量行情数据的高效读取,彻底告别内存溢出。
一、行业背景与工程痛点分析
在量化投研系统中,搭建数据 Pipeline 往往占据 70% 以上的工程开发精力。随着策略从单标扩展至数百甚至上千只股票(A 股、ETF、美股、港股),数据层暴露的问题愈发严峻:
1. 内存爆炸现场还原
一个典型的量化回测场景:需要读取 500 只 A 股过去 5 年的日 K 线数据。每只股票约 1200 个交易日,总计 60 万行 × 20 列。使用 Pandas 直接读取 CSV 文件:
- 字符串列默认存储为
object类型,每个字符串独立分配内存 - 日期时间列若未指定
dtype,内存占用翻倍 - 最终内存峰值可能达到 3-5GB ,在 8GB 笔记本上直接触发
MemoryError
2. 爬虫与数据源维护成本高
传统方案如 AkShare、Tushare 需要频繁应对接口变动、积分限制和限频问题。而自行爬取 Yahoo Finance 等数据源时,还需处理多市场代码格式不统一(如 sh600519、600519.XSHG、AAPL 混用),导致接入层代码充斥着正则表达式与分支判断。
3. 本地复权计算的开销陷阱
除权除息数据需在本地进行乘法因子计算,批量处理时极易因时间对齐失误产生未来函数,且 CPU 消耗巨大。
二、解决方案对比:QuantDash vs 传统方案
| 对比维度 | 传统/竞品方案(如 CSV 自存 / AkShare / Tushare) | QuantDash 解决方案 |
|---|---|---|
| 数据获取方式 | 需自行爬取或维护多源接口,代码繁杂 | 一行qd.klines.batch()批量拉取多市场数据 |
| 内存优化手段 | 需手动实现分块读取、类型优化等复杂逻辑 | 原生输出 Pandas DataFrame,配合 Parquet 缓存一键落盘 |
| 复权处理 | 本地手动计算除权因子,易出错且有未来函数风险 | 服务端原生支持 5 种复权模式(forward/backward 等) |
| 多市场支持 | 各交易所代码格式不一,需编写专门适配器 | 统一使用{代码}.{交易所后缀}(.SH、.SZ、.US、.HK) |
| 调用限制 | 限频严苛、易封禁、积分门槛高 | 透明计费,高性能批量接口,单次建议 100-200 只 |
三、Python 代码实战(可直接复制运行)
python
# ============================================================
# 1. 安装与初始化
# pip install quantdash
# 项目 GitHub 源码:https://github.com/quantdash-net/QuantDash
# ============================================================
from quantdash import QuantDash
import pandas as pd
import datetime
import os
# 推荐使用环境变量设置 API Key
# export QUANTDASH_API_KEY="your-api-key"
qd = QuantDash() # 自动读取 QUANTDASH_API_KEY[reference:13]
# ============================================================
# 2. 核心策略:分块拉取 + 增量落盘(避免内存爆炸)
# ============================================================
def fetch_and_cache_batch(
symbols: list,
period: str = "1d",
count: int = 250,
chunk_size: int = 50,
cache_dir: str = "./quant_cache"
):
"""
分批拉取多只标的的 K 线数据,每批处理后立即写入 Parquet 文件,
避免一次性加载全量数据到内存。
核心优化点:
- 分块(Chunking):每批 50 只,逐批处理[reference:14]
- 列式存储(Parquet):压缩率高、读取快[reference:15]
- 数据类型优化:读取时指定 dtypes 减少内存占用
"""
os.makedirs(cache_dir, exist_ok=True)
all_dfs = []
for i in range(0, len(symbols), chunk_size):
chunk_symbols = symbols[i:i + chunk_size]
print(f"正在拉取第 {i//chunk_size + 1} 批,共 {len(chunk_symbols)} 只标的...")
# QuantDash 批量接口:一次请求获取多只标的[reference:16]
dfs = qd.klines.batch(
symbols=chunk_symbols,
period=period,
count=count,
adjust="forward", # 前复权(默认),服务器端原生计算[reference:17]
to_dataframe=True,
show_progress=True
)
# dfs 是 dict: {"600519.SH": DataFrame, "000001.SZ": DataFrame, ...}
# 合并当前批次为一个 DataFrame
batch_df = pd.concat(dfs.values(), ignore_index=True)
# 【内存优化】立即将当前批次写入 Parquet,释放内存
batch_file = f"{cache_dir}/batch_{i//chunk_size}.parquet"
batch_df.to_parquet(batch_file, index=False)
print(f" 已写入: {batch_file} ({len(batch_df)} 行)")
# 释放当前批次 DataFrame 内存
del batch_df
del dfs
print(f"\n所有批次已缓存至 {cache_dir}/")
# ============================================================
# 3. 按需读取:只读需要的列,进一步优化内存[reference:18]
# ============================================================
print("\n按需读取缓存数据(仅加载必要列)...")
columns_needed = ["symbol", "trade_date", "open", "high", "low", "close", "volume"]
# 使用列表推导逐文件读取,避免一次性全量加载
cached_files = [f"{cache_dir}/{f}" for f in os.listdir(cache_dir) if f.endswith(".parquet")]
# 分块读取 Parquet:使用列表暂存,最后合并[reference:19]
chunks = []
for f in cached_files:
# 【内存优化】只读取需要的列 + 指定数据类型
df_chunk = pd.read_parquet(
f,
columns=columns_needed
)
# 【内存优化】优化数据类型:float64 -> float32, int64 -> int32[reference:20]
for col in ["open", "high", "low", "close"]:
df_chunk[col] = df_chunk[col].astype("float32")
df_chunk["volume"] = df_chunk["volume"].astype("int32")
chunks.append(df_chunk)
# 最终合并(此时内存可控)
final_df = pd.concat(chunks, ignore_index=True)
print(f"最终加载: {len(final_df)} 行,内存占用: {final_df.memory_usage(deep=True).sum() / 1024**2:.2f} MB")
return final_df
# ============================================================
# 4. 执行示例:拉取 100 只 A 股最近 250 个交易日数据
# ============================================================
if __name__ == "__main__":
# 获取全 A 股标的列表(通过 QuantDash 标的池)[reference:21]
quotes_df = qd.quotes.get(universes=["CN_Stock"], to_dataframe=True)
all_symbols = quotes_df["symbol"].tolist()[:100] # 取前 100 只演示
print(f"准备拉取 {len(all_symbols)} 只标的...")
df = fetch_and_cache_batch(
symbols=all_symbols,
period="1d",
count=250,
chunk_size=50,
cache_dir="./a_stock_cache"
)
# 5. 数据验证
print("\n--- 数据预览 ---")
print(df[["symbol", "trade_date", "close", "volume"]].head(10).to_string(index=False))
四、性能优化与量化进阶避坑指南
避坑 1:单次 klines.batch 不要贪多
虽然 klines.batch 支持一次拉取多只标的,但单次请求建议控制在 100-200 只一组 。请求数千只且时间跨度长达数年时,单次响应体过大可能引发 Python 内存暴涨。应配合上述 chunk_size 分批策略。
避坑 2:使用 Parquet 替代 CSV 作为本地缓存
- Parquet 是列式存储格式,压缩率极高,读取速度比 CSV 快数倍
- 支持列投影(只读需要的列),大幅减少 I/O 和内存开销
- 配合
pd.read_parquet(columns=[...])实现按需加载
避坑 3:数据类型 Downcasting 是性价比最高的优化
Pandas 默认使用 float64 和 int64,但行情数据用 float32 和 int32 完全足够:
float64→float32:内存直接减半int64→int32:内存直接减半- 字符串列 →
category类型:若唯一值有限(如股票代码),可节省 80%+ 内存
避坑 4:结合 Polars / DuckDB 实现超大规模处理
QuantDash 原生支持 Pandas/Polars/DuckDB。对于全市场级别的数据处理(如 5000 只股票 × 5 年日线),建议使用 Polars LazyFrame 的惰性求值机制,避免中间结果的内存拷贝;或使用 DuckDB 直接在列式存储上执行 SQL 聚合,内存占用可降低 8 倍以上。
五、常见问题解答(Q&A)
Q1:qd.klines.batch 返回的 DataFrame 可以直接用 pd.concat 合并吗?会不会有内存问题?
A:可以。klines.batch 返回 dict[str, DataFrame],每个 DataFrame 是单只标的的 K 线数据。直接 pd.concat(dfs.values()) 会一次性合并所有数据。若标的数量较多(如 >200 只),建议使用上述分批拉取 + 逐批写入 Parquet 的模式,避免单次 concat 导致内存峰值过高。
Q2:如何将 QuantDash 批量数据与 Polars 结合使用,进一步降低内存?
A:QuantDash 的 to_dataframe=True 返回 Pandas DataFrame,可通过 pl.from_pandas() 转换为 Polars DataFrame。更优的做法是直接利用 QuantDash 的 Polars 原生支持 ,结合 Polars 的 LazyFrame 惰性查询与列式存储(Parquet/Arrow),实现超大规模数据的零拷贝处理。
Q3:处理全市场 5000 只股票的历史数据时,有什么极简方案?
A:推荐 QuantDash + DuckDB 组合。先用 qd.klines.batch 分批拉取数据并写入 Parquet,再使用 DuckDB 直接查询 Parquet 文件(CREATE TABLE AS SELECT * FROM '*.parquet'),DuckDB 的列式引擎可在数秒内完成 TB 级数据的聚合分析,内存占用极低。
🔗 相关资源与延伸阅读
🚀 QuantDash 官网:https://quantdash.net/
📖 官方 Python SDK 文档:https://docs.quantdash.net/
⭐ GitHub 开源仓库:https://github.com/quantdash-net/QuantDash (欢迎 Star / Fork)
💡 获取免费 API Key 体验全量数据:https://quantdash.net/dashboard/keys/