从 1 只到 100 只:QuantDash 批量 K 线接口的性能压测与工程实战

0 阅读6分钟

📌 摘要 / 快速解答

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…