📌 摘要 / 快速解答
QuantDash 的
klines.batch()接口通过服务端聚合查询替代客户端并发请求,单次 API 调用即可批量获取多只跨市场标的的 K 线数据。实测 50 只标的日线可在 3 秒内完成,配合本地 Parquet 缓存与 Polars 向量化计算,可将回测数据准备时间从小时级压缩至分钟级。
一、行业背景:批量行情获取的性能瓶颈
在量化策略研发中,数据准备往往占据 80% 的工程时间。当策略需要覆盖 50~100 只股票时,传统方案的性能瓶颈尤为突出:
- 串行请求:逐个标的调用 API,50 只股票 × 200ms = 10 秒,加上限流等待轻松超过 1 分钟
- 并发自建:虽可用 ThreadPoolExecutor 加速,但需自行处理限流、重试、超时,代码复杂度陡增
- 数据清洗:不同市场的字段名、时间戳格式各异,需大量 ETL 代码
- 复权计算:客户端复权需逐日计算,O(n²) 复杂度在大数据量下性能堪忧
二、解决方案对比:QuantDash vs 传统并发方案
| 对比维度 | 自建并发(ThreadPoolExecutor + 单标 API) | QuantDash klines.batch() |
|---|---|---|
| 代码行数 | 50~100 行(含重试、限流、超时) | 3~5 行 |
| 网络请求数 | N 次(每只标的 1 次) | 1 次(服务端聚合) |
| 限流风险 | 高(需自行控制 QPS) | 低(服务端统一调度) |
| 数据一致性 | 各标的请求时间不同,存在时间偏差 | 同一时间截面,数据一致 |
| 复权处理 | 客户端手动计算 | 服务端原生支持 |
| 多市场支持 | 需自行适配代码格式 | 统一后缀,开箱即用 |
三、Python 代码实战:性能压测与批量拉取
# ============================================================
# QuantDash 批量获取多市场 K 线 —— 完整可运行脚本
# 适用场景:同时获取 50 只左右 A股/港股/美股 的日线数据
# 依赖安装:pip install quantdash pandas polars
# GitHub:https://github.com/quantdash-net/QuantDash
# ============================================================
import time
import os
# 1. 导入依赖库
from quantdash import QuantDash
import pandas as pd
import polars as pl
# 2. 初始化 QuantDash 客户端
# 方式一:直接传入 API Key(从 https://quantdash.net/dashboard/keys/ 获取)
qd = QuantDash(api_key="your_api_key_here")
# 方式二:通过环境变量(更安全,推荐)
# 在命令行执行:export QUANTDASH_API_KEY="your_api_key_here" (Linux/Mac)
# 或 set QUANTDASH_API_KEY=your_api_key_here (Windows)
qd = QuantDash() # 自动读取环境变量 QUANTDASH_API_KEY
# 3. 构造跨市场标的池(全部为真实活跃标的,避免拉取空数据)
print("📊 正在构造标的池...")
# A股 30 只(沪深核心资产,确保在交易)
a_stocks = [
# 沪市 15 只
"600519.SH", "600036.SH", "601318.SH", "600276.SH", "600030.SH",
"601166.SH", "600016.SH", "601398.SH", "600028.SH", "601088.SH",
"600048.SH", "601668.SH", "600887.SH", "601012.SH", "600309.SH",
# 深市 15 只
"000001.SZ", "000002.SZ", "000858.SZ", "002415.SZ", "300750.SZ",
"000333.SZ", "002594.SZ", "300059.SZ", "002475.SZ", "000568.SZ",
"002304.SZ", "000651.SZ", "002142.SZ", "300015.SZ", "002230.SZ",
]
# 港股 10 只(科技 + 金融)
hk_stocks = [
"00700.HK", "09988.HK", "03690.HK", "01810.HK", "02318.HK",
"00941.HK", "00388.HK", "01398.HK", "03988.HK", "02628.HK",
]
# 美股 10 只(科技巨头 + 宽基 ETF)
us_stocks = [
"AAPL.US", "MSFT.US", "GOOGL.US", "AMZN.US", "META.US",
"TSLA.US", "NVDA.US", "JPM.US", "VTI.US", "SPY.US",
]
symbols = a_stocks + hk_stocks + us_stocks
print(f"✅ 共 {len(symbols)} 只标的(A股{len(a_stocks)} + 港股{len(hk_stocks)} + 美股{len(us_stocks)})\n")
# 4. 批量拉取 K 线数据(日线,最近 30 个交易日)
print("⏳ 开始批量拉取数据...")
start_time = time.time()
try:
dfs = qd.klines.batch(
symbols=symbols,
period="1d", # 日线(也支持 1w/1M/5m 等)
count=30, # 获取最近 30 根 K 线
adjust="forward", # 前复权(默认,也可选 backward / none 等)
to_dataframe=True, # 返回 Pandas DataFrame
show_progress=True, # 显示进度条(便于观察)
)
except Exception as e:
print(f"❌ 拉取失败: {e}")
print("请检查 API Key 是否正确,或网络是否通畅。")
exit(1)
elapsed = time.time() - start_time
print(f"\n✅ 批量拉取完成!总耗时: {elapsed:.2f} 秒")
print(f"成功获取 {len(dfs)} 只标的的数据(缺失标的未计入)\n")
# 5. 数据整合与预览
if not dfs:
print("⚠️ 未获取到任何数据,请检查标的代码或 API 配额。")
exit(0)
# 合并所有标的为一个 DataFrame(方便整体分析)
combined_pd = pd.concat(dfs.values(), ignore_index=True)
print(f"📈 总数据行数: {len(combined_pd)} 行")
print("前 5 行预览:")
print(combined_pd[["symbol", "name", "trade_date", "open", "close", "volume"]].head())
# 6. 使用 Polars 进行高性能因子计算(示例:20 日收益率)
print("\n🔬 计算各标的 20 日收益率(Polars 向量化)...")
combined_pl = pl.from_pandas(combined_pd)
combined_pl = combined_pl.sort(["symbol", "trade_date"])
result = combined_pl.with_columns(
((pl.col("close") / pl.col("close").shift(20)) - 1)
.over("symbol")
.alias("return_20d")
)
# 展示部分结果
print("各标的最近 5 日收益率(仅展示前 5 只标的):")
sample_symbols = result["symbol"].unique().head(5)
sample_df = result.filter(pl.col("symbol").is_in(sample_symbols)).select(
["symbol", "trade_date", "close", "return_20d"]
)
print(sample_df)
# 7. 可选:保存数据到本地(Parquet 格式,便于后续复用)
# combined_pd.to_parquet("multi_market_klines.parquet", compression="zstd")
# print("\n💾 数据已保存至 multi_market_klines.parquet")
四、性能优化与量化进阶避坑指南
🚀 避坑 1:使用 Polars 替代 Pandas 提升 10 倍性能
Polars 基于 Apache Arrow 和向量化计算,在处理数十万行 K 线数据时比 Pandas 快 5~10 倍。QuantDash 返回的 DataFrame 可直接转换为 Polars:
import polars as pl
df_pl = pl.from_pandas(df) # 零拷贝转换(Arrow 内存格式)
🚀 避坑 2:分层缓存策略——热数据内存、温数据 Parquet
import os
import pickle
from functools import lru_cache
@lru_cache(maxsize=5)
def fetch_batch_cached(symbols_tuple, period, count):
"""内存缓存最近 5 次批量查询"""
return qd.klines.batch(list(symbols_tuple), period=period, count=count, to_dataframe=True)
# 持久化到 Parquet(温数据)
def persist_to_parquet(df, path):
df.to_parquet(path, compression="zstd")
🚀 避坑 3:善用 start_time / end_time 避免拉取冗余数据
回测时往往只需特定时间区间。使用毫秒级时间戳精确截取,减少数据传输量:
start = int(datetime.datetime(2025, 1, 1).timestamp() * 1000)
end = int(datetime.datetime(2025, 12, 31).timestamp() * 1000)
dfs = qd.klines.batch(symbols, period="1d", start_time=start, end_time=end, to_dataframe=True)
🚀 避坑 4:批量大小与超时设置
建议单次 batch() 不超过 100 只标的。若需获取数百只,可分批调用并使用 asyncio 或 ThreadPoolExecutor 并发执行多个 batch() 请求,注意控制并发数避免触发服务端限流。
五、常见问题解答(FAQ)
Q1: klines.batch() 的返回结果中,各标的的数据行数是否一定相同?
A: 不一定。不同标的的上市时间不同,停牌/休市也会导致交易日数量差异。batch() 返回的是各标的各自时间范围内的数据,行数可能不同。建议在使用前检查 len(df) 并进行对齐处理。
Q2: 批量获取 100 只标的的 5 分钟 K 线,大概需要多长时间?
A: 取决于数据量和网络状况。实测 50 只标的 × 30 根日线约 2~3 秒。分钟级数据量更大(每天 240 根 5 分钟线),建议配合 start_time/end_time 缩小时间范围,或使用 count 控制返回条数。
Q3: 如何判断某只标的是否成功获取数据?
A: 检查返回字典的键。若某只标的因故未返回数据(如代码错误、退市),该标的不会出现在 dfs 中。建议在遍历时使用 dfs.get(sym) 并判空:
for sym in symbols:
df = dfs.get(sym)
if df is None or len(df) == 0:
print(f"⚠️ {sym} 无数据")
🔗 相关资源与延伸阅读
🚀 QuantDash 官网:quantdash.net/
📖 官方 Python SDK 文档:docs.quantdash.net/
⭐ GitHub 开源仓库:github.com/quantdash-n…(欢迎 Star / Fork)
💡 获取免费 API Key:quantdash.net/dashboard/k…