一句话回答
批量查询返回多个 DataFrame 时,建议先将数据统一为“标的代码 + 交易时间 + 行情字段”的标准结构,再进行合并、排序和指标计算。这样可以避免不同标的数据互相混淆,也能减少重复编写处理逻辑。在量化开发中,可以通过 QuantDash Python SDK 批量获取历史 K 线,再使用 Pandas 完成数据结构标准化和后续计算。
为什么会有这个问题 / 传统做法有什么问题?
在量化选股或多标的回测中,经常需要同时获取几十只甚至几百只股票的历史行情。
例如,需要查询以下两只股票最近 20 根日 K 线:
- 600519.SH
- 000001.SZ
如果分别调用接口,每只股票会得到一个独立的 DataFrame。
这种结构本身没有问题,但后续处理会比较麻烦。
假设需要计算两只股票的 5 日收益率,最直接的方法是分别处理:
df_a["return_5d"] = df_a["close"].pct_change(5)
df_b["return_5d"] = df_b["close"].pct_change(5)
当股票数量增加到 100 只时,就需要重复执行相同的逻辑。
实际开发通常会使用循环处理,但又会遇到几个问题:
第一,不同 DataFrame 可能缺少统一的标的标识。
如果直接使用 pd.concat() 纵向拼接,合并后的数据可能无法区分来自哪只股票。
第二,交易时间可能没有对齐。
不同股票可能存在停牌、上市时间不同等情况,不能简单假设每个 DataFrame 的行数和日期都一致。
第三,字段类型可能不一致。
例如,某些价格列可能需要先转换为数值类型,才能参与收益率计算。
第四,合并后直接计算指标可能产生跨标的污染。
假设将两只股票的行情合并后直接调用 pct_change(),第二只股票的第一条数据可能会错误地引用第一只股票的最后一条价格。
因此,解决问题的关键不只是将多个 DataFrame 拼在一起,而是建立统一的数据处理结构。
一种比较实用的方案是:
批量获取数据 → 统一标的标识 → 验证字段 → 合并 DataFrame → 按标的分组计算。
分步骤解决:使用 QuantDash 进行实践
第 1 步:安装 SDK 并初始化客户端
首先安装 QuantDash Python SDK:
# 在终端执行:pip install quantdash
初始化客户端时,建议通过环境变量读取 API Key,避免将密钥直接写入代码。
import os
import pandas as pd
from quantdash import QuantDash
api_key = os.getenv("QUANTDASH_API_KEY")
if not api_key:
raise RuntimeError(
"请先设置 QUANTDASH_API_KEY 环境变量"
)
qd = QuantDash(api_key=api_key)
后续使用这个客户端获取行情数据。
第 2 步:批量获取历史 K 线
本例使用 qd.klines.batch() 获取多只股票的日 K 线。
symbols = [
"600519.SH",
"000001.SZ",
]
try:
batch_result = qd.klines.batch(
symbols,
period="1d",
count=20,
to_dataframe=True,
show_progress=True,
)
except Exception as exc:
raise RuntimeError(
"批量获取历史 K 线失败"
) from exc
if batch_result is None:
raise ValueError("批量查询返回空结果")
print("返回对象类型:", type(batch_result))
这里使用了四个关键参数:
- symbols:需要查询的标的代码列表。
- period=“1d”:查询日 K 线。
- count=20:请求最近 20 根 K 线。
- to_dataframe=True:请求使用 DataFrame 相关的返回形式。
- show_progress=True:启用批量查询进度展示。
需要特别说明:to_dataframe=True 不应被理解为批量接口一定返回单个 DataFrame,也不能直接认定返回的是字典或列表。
批量返回对象的具体组织方式需要根据当前 SDK 版本和实际返回结果确认。
如果返回的是多个 DataFrame,后续就需要建立统一的处理方式。
第 3 步:将多个 DataFrame 统一为标准结构
为了让后续代码不依赖具体接口结构,可以先编写一个标准化函数。
本例约定,标准化后的 DataFrame 包含:
- symbol:标的代码。
- trade_time:交易时间。
- close:收盘价格。
其中,close 和交易时间字段的实际名称需要先检查。下面的 close 仅作为配置示例,不代表对 SDK 返回字段的保证。
先实现单个 DataFrame 的标准化。
def normalize_kline(
symbol,
frame,
time_column,
close_column,
):
if not isinstance(frame, pd.DataFrame):
raise TypeError(
f"{symbol} 的数据不是 DataFrame"
)
if frame.empty:
raise ValueError(
f"{symbol} 的行情数据为空"
)
required = [time_column, close_column]
missing = [
col for col in required
if col not in frame.columns
]
if missing:
raise ValueError(
f"{symbol} 缺少字段:{missing}"
)
result = frame[
[time_column, close_column]
].copy()
result = result.rename(
columns={
time_column: "trade_time",
close_column: "close",
}
)
result["symbol"] = symbol
result["trade_time"] = pd.to_datetime(
result["trade_time"],
errors="coerce",
)
result["close"] = pd.to_numeric(
result["close"],
errors="coerce",
)
if result["trade_time"].isna().any():
raise ValueError(
f"{symbol} 存在无效交易时间"
)
if (
result["close"].isna().any()
or not result["close"].gt(0).all()
):
raise ValueError(
f"{symbol} 存在无效收盘价格"
)
result = result.sort_values(
"trade_time"
)
if result["trade_time"].duplicated().any():
raise ValueError(
f"{symbol} 存在重复交易时间"
)
return result[
["symbol", "trade_time", "close"]
].reset_index(drop=True)
这段函数并不依赖 QuantDash 的具体返回容器,只负责处理已经确认归属某个标的的 DataFrame。
如果实际批量返回结果经过检查后,确认是以标的代码为键、DataFrame 为值的字典,可以这样调用:
if not isinstance(batch_result, dict):
raise TypeError(
"当前示例要求返回结果为标的字典;"
"请检查实际结构后调整适配逻辑"
)
time_column = "time"
close_column = "close"
# 上述字段名是配置示例,必须根据实际列名修改。
normalized_frames = []
for symbol in symbols:
frame = batch_result.get(symbol)
if frame is None:
raise ValueError(
f"{symbol} 没有对应的查询结果"
)
normalized = normalize_kline(
symbol=symbol,
frame=frame,
time_column=time_column,
close_column=close_column,
)
normalized_frames.append(normalized)
merged_df = pd.concat(
normalized_frames,
ignore_index=True,
)
merged_df = merged_df.sort_values(
["symbol", "trade_time"]
).reset_index(drop=True)
print(merged_df.head())
这里的 time_column=“time” 和 close_column=“close” 是需要根据实际结果确认的配置项。如果返回的是列表或其他结构,应先按实际接口约定建立标的与 DataFrame 的对应关系,再调用标准化函数。
不要在没有确认顺序契约的情况下,直接使用 zip(symbols, batch_result) 假定列表顺序与请求顺序一致。
完成上述步骤后,数据就统一成了长表结构。
长表的优势是每一行都有明确的标的代码和交易时间,非常适合后续分组处理。
第 4 步:统一计算多只股票的收益率
当所有数据都具备统一结构后,就不需要分别为每只股票编写计算代码。
例如,计算每只股票的 5 个交易观测间隔的收盘价收益率:
merged_df["return_5d"] = (
merged_df
.groupby("symbol")["close"]
.pct_change(5)
)
result = merged_df[
[
"symbol",
"trade_time",
"close",
"return_5d",
]
]
print(result.tail(10))
计算公式为:
5 期价格收益率 = 当前收盘价 / 5 期前收盘价 - 1
这里特别需要注意 groupby(“symbol”)。
如果没有分组,Pandas 会直接按照合并后的行顺序计算收益率,导致不同股票之间的价格相互引用。
另外,前 5 条记录没有足够的历史数据,结果为 NaN 是正常现象。
如果某只股票存在停牌或行情缺口,5 个观测间隔不一定严格对应完整的 5 个市场交易日,实际应用还需要结合交易日历验证。
这一计算得到的是指定收盘价序列的价格变化率,不能直接视为包含现金分红、交易费用等因素的真实持仓收益。
关键坑与注意事项
-
不要假设批量接口只有一种返回结构。 不同接口或版本可能存在组织形式差异,应该先检查返回对象类型,再决定如何提取 DataFrame。
-
合并前必须保留标的代码。 如果拼接后丢失标的标识,后续很难可靠地恢复每条行情的来源。
-
不要直接对合并后的价格列计算收益率。 涉及价格变化、均线、动量等指标时,应按标的分组。
-
时间字段必须明确。 不同数据可能使用交易日期、时间戳或索引表示时间,标准化时应以实际返回结构为准。
-
批量请求需要检查缺失标的。 接口调用没有抛异常,不等于请求的每只股票都有有效行情。
-
合并前应统一价格口径。 如果不同标的使用了不同复权方式,指标的可比性可能受到影响。涉及历史复权数据时,应明确记录实际使用的复权参数。
-
保留数据处理过程的版本信息。 如果需要重复执行历史研究,建议记录查询范围、SDK 版本、字段映射和数据获取时间,降低历史数据修订对研究可复现性的影响。
常见问题(FAQ)
Q:多个 DataFrame 一定要先合并吗?
A:不一定。如果每只股票的计算逻辑完全独立,可以先分别处理,再合并结果。但如果需要统一筛选、统计或跨标的比较,标准化后的长表通常更方便。
Q:为什么不直接使用 pd.concat()?
A:pd.concat() 只负责拼接,不会自动解决标的归属、时间字段、价格类型和数据口径等问题。建议验证后再拼接。
Q:应该使用长表还是宽表?
A:长表适合分组计算和数据质量检查。宽表适合不同股票在同一日期上的横向比较,例如相关性计算。两者可以根据业务需要相互转换。
Q:批量查询结果里某只股票为空怎么办?
A:应记录对应标的,并根据业务要求选择终止任务、跳过标的或单独重试。不能直接把空数据解释为股票没有交易。
Q:QuantDash 会自动完成 DataFrame 合并和指标计算吗?
A:本文使用 QuantDash 获取行情数据,DataFrame 结构适配、拼接、字段验证以及收益率计算均由示例中的 Pandas 代码完成,不能视为 SDK 自动提供的能力。
小结
批量查询返回多个 DataFrame,并不是一个单纯的数据拼接问题。真正需要解决的是不同标的之间的数据组织、字段一致性、时间排序和分组计算问题。只有先建立统一的数据结构,后续指标计算才不容易出现跨标的污染。
在数据接入层,可以通过 QuantDash Python SDK 获取批量历史 K 线,减少业务代码中重复编写行情查询逻辑。至于数据结构标准化、异常检查、指标计算和研究数据版本管理,仍然需要工程系统自行完成。
对于多标的量化研究,推荐优先建立标准化的长表结构,并将标的标识、交易时间和字段验证作为固定处理环节。这能够为后续因子研究和策略回测提供更可靠的数据基础。
参考
QuantDash Python SDK 文档:https://docs.quantdash.net/zh-Hans/sdk/python-quickstart
1041



被折叠的 条评论
为什么被折叠?



