如何封装 qd.klines.batch(),让批量 K 线查询函数在多个策略中复用?

一句话回答

为 qd.klines.batch() 建立可复用的批量查询函数,核心是把标的列表、K 线周期、查询数量、客户端初始化和结果校验集中管理,避免不同策略重复编写行情获取代码。使用 QuantDash Python SDK 完成批量历史 K 线获取后,再通过 Python 函数统一处理输入参数、异常和返回数据,可以让选股、因子研究和策略回测使用更一致的数据接入方式。

为什么会有这个问题 / 传统做法有什么问题?

在量化研究中,批量获取多只股票的历史 K 线是一个非常常见的操作。

比如,一个简单的动量选股策略需要获取几十只股票最近一段时间的日线数据,计算各只股票的阶段收益率,然后根据动量大小进行排序。

如果只做一次研究,直接调用 qd.klines.batch() 就能完成数据获取。

但如果同时开发多个策略,问题就出现了。

假设目前有三个研究任务:

  • 动量策略需要最近 60 根日 K 线。
  • 波动率策略需要最近 120 根日 K 线。
  • 短周期研究需要最近 100 根 5 分钟 K 线。

最直接的方式,是在三个脚本中分别编写批量查询代码。

这种写法短期看起来没有什么问题,但随着项目规模扩大,会暴露几个局限。

首先,查询参数容易散落在不同脚本中。某个策略使用 period="1d" ,另一个策略使用 period="5m",如果缺少统一记录,后续分析结果时就容易混淆数据频率。

其次,每个脚本可能采用不同的异常处理方式。有的遇到接口异常就直接终止,有的捕获异常后返回空表,还有的没有检查返回数据是否为空。

更麻烦的是,返回数据的字段处理可能不一致。

例如,一个研究模块使用原始时间字段,另一个模块把时间转换为字符串。如果后面需要合并多个研究结果,就可能出现日期无法正确对齐的问题。

这些问题并不是批量查询接口本身造成的,而是缺少统一的数据访问层。

比较稳妥的做法是将批量查询操作封装为一个独立函数,统一完成参数检查、SDK 调用和结果验证。

分步骤解决:使用 QuantDash 进行实践

第 1 步:安装 SDK 并初始化客户端

首先安装 QuantDash Python SDK。

python 复制代码
# 在终端执行
# pip install quantdash

实际安装命令为 pip install quantdash。

建议通过环境变量保存 API Key,而不是直接写入代码。

python 复制代码
import os
from quantdash import QuantDash


def create_client():
    api_key = os.getenv("QUANTDASH_API_KEY")

    if not api_key:
        raise RuntimeError(
            "请先设置 QUANTDASH_API_KEY 环境变量"
        )

    return QuantDash(api_key=api_key)


qd = create_client()

将客户端初始化封装为 create_client(),后续其他模块就可以复用相同的初始化逻辑。

需要注意,这里只是统一创建客户端,并不代表 SDK 自动完成了请求限流、重试或者并发管理。

这些能力是否存在,需要以实际 SDK 文档为准。

第 2 步:获取与主题相关的数据

先使用已知的批量接口获取两只股票的历史日 K 线。

python 复制代码
import pandas as pd

symbols = ["600519.SH", "000001.SZ"]

try:
    df = qd.klines.batch(
        symbols,
        period="1d",
        count=20,
        to_dataframe=True,
        show_progress=True,
    )
except Exception as exc:
    raise RuntimeError(
        f"批量 K 线查询失败: {exc}"
    ) from exc

if not isinstance(df, pd.DataFrame):
    raise TypeError(
        "返回结果不是 DataFrame,请检查实际接口结构"
    )

if df.empty:
    raise ValueError("批量查询结果为空")

print("返回字段:", df.columns.tolist())
print("返回行数:", len(df))

这里几个参数需要重点关注。

symbols 是需要查询的标的列表,标的代码使用带交易所后缀的格式。

period="1d" 表示查询日 K 线。

count=20 表示请求指定数量的 K 线记录。每个标的实际取得的数据数量,需要结合接口返回和可用历史数据检查。

to_dataframe=True 表示请求 DataFrame 形式的结果。

show_progress=True 用于显示批量查询进度。

这里没有直接假设返回结果一定包含 symbol 、datetime 或 close 字段。

原因是批量数据可能采用不同的组织方式,例如将标的信息放在普通列、索引或者其他结构中。

具体字段名称和组织方式应当根据实际返回结果确认。

第 3 步:处理数据并展示核心逻辑

接下来实现一个可复用的批量查询函数。

第一版不急着计算因子,而是先统一输入检查和基础数据验证。

python 复制代码
import pandas as pd


def fetch_batch_klines(
    client,
    symbols,
    period="1d",
    count=100,
    show_progress=False,
):
    if not isinstance(symbols, (list, tuple)):
        raise TypeError(
            "symbols 必须是列表或元组"
        )

    clean_symbols = list(dict.fromkeys(symbols))

    if not clean_symbols:
        raise ValueError("标的列表不能为空")

    if not all(
        isinstance(symbol, str) and symbol.strip()
        for symbol in clean_symbols
    ):
        raise ValueError("标的代码必须是非空字符串")

    if not isinstance(period, str) or not period:
        raise ValueError("period 不能为空")

    if (
        isinstance(count, bool)
        or not isinstance(count, int)
        or count <= 0
    ):
        raise ValueError("count 必须是正整数")

    try:
        result = client.klines.batch(
            clean_symbols,
            period=period,
            count=count,
            to_dataframe=True,
            show_progress=show_progress,
        )
    except Exception as exc:
        raise RuntimeError(
            f"批量查询失败: {exc}"
        ) from exc

    if not isinstance(result, pd.DataFrame):
        raise TypeError(
            "返回类型不符合预期,请核对 SDK 返回结构"
        )

    if result.empty:
        raise ValueError(
            "批量查询返回空 DataFrame"
        )

    return result.copy()

这个封装主要完成四件事:

第一,对标的列表进行去重,避免同一标的重复出现在输入中。

第二,检查周期参数和查询数量,减少明显错误进入数据接口。

第三,集中捕获 SDK 调用阶段的异常。

第四,统一验证返回类型和空数据情况。

注意,当前函数仅保证数据通过了基础检查。

它并没有验证每只股票是否都返回了足够的 K 线,也没有判断时间序列是否连续。

这些属于更高一层的数据质量检查。

第 4 步:展示完整业务示例或进阶处理

基础函数完成后,可以在研究层增加一个收盘价动量计算函数。

这里先确认实际返回结构,再通过参数指定字段名称,避免把尚未核实的字段写死。

下面的 symbol_col 、time_col 和 close_col 是调用方根据实际数据指定的列名,不代表对 SDK 返回字段的承诺。

python 复制代码
import pandas as pd


def calculate_momentum(
    data,
    symbol_col,
    time_col,
    close_col,
):
    required = {
        symbol_col,
        time_col,
        close_col,
    }

    missing = required - set(data.columns)

    if missing:
        raise ValueError(
            f"缺少必要字段: {sorted(missing)}"
        )

    frame = data[
        [symbol_col, time_col, close_col]
    ].copy()

    frame[time_col] = pd.to_datetime(
        frame[time_col],
        errors="coerce",
        utc=True,
    )

    frame[close_col] = pd.to_numeric(
        frame[close_col],
        errors="coerce",
    )

    if frame[time_col].isna().any():
        raise ValueError("存在无法解析的时间字段")

    if (
        frame[close_col].isna().any()
        or (frame[close_col] <= 0).any()
    ):
        raise ValueError("存在无效收盘价")

    if frame.duplicated(
        [symbol_col, time_col]
    ).any():
        raise ValueError("存在重复标的时间记录")

    frame = frame.sort_values(
        [symbol_col, time_col]
    )

    results = []

    for symbol, group in frame.groupby(
        symbol_col,
        sort=False,
    ):
        if len(group) < 2:
            continue

        first_close = group[close_col].iloc[0]
        last_close = group[close_col].iloc[-1]

        momentum = last_close / first_close - 1

        results.append({
            "symbol": symbol,
            "momentum": momentum,
            "observations": len(group),
        })

    return pd.DataFrame(
        results,
        columns=[
            "symbol",
            "momentum",
            "observations",
        ],
    )


klines = fetch_batch_klines(
    qd,
    ["600519.SH", "000001.SZ"],
    period="1d",
    count=60,
)

print("可用字段:", klines.columns.tolist())

# 确认实际列名及标的、时间的组织方式后,
# 再调用 calculate_momentum()

动量计算使用的公式为:

阶段价格收益率 = 期末收盘价 / 期初收盘价 - 1

该公式适用于所选价格序列的区间价格变化计算,不等同于包含现金分红、交易费用和税费的完整投资收益。

需要特别说明,calculate_momentum() 只负责研究层的计算,而 qd.klines.batch() 负责获取行情数据。

两者职责不同,分开封装以后,后续替换因子算法不会影响行情查询函数。

另外,上面的计算要求时间列能够正确转换,并且每个标的的记录按时间排序。若实际批量返回使用索引或其他结构保存标的与时间信息,需要先进行结构适配。

关键坑与注意事项

  • 批量查询不代表每只股票数据都完整。 即使返回 DataFrame 非空,也应检查每个请求标的是否有对应记录,以及实际记录数量是否符合研究要求。
  • 不要默认批量结果包含固定字段。 特别是标的代码和时间信息,可能采用不同组织方式,应该先查看实际返回结构再进行字段映射。
  • 不同周期的数据不能直接混算。 日 K 线和分钟 K 线的频率不同,函数虽然可以统一获取,但因子计算需要分别处理。
  • 查询数量不等于自然日数量。 请求 60 根日 K 线不代表覆盖 60 个自然日,遇到节假日、停牌或上市时间较短的股票时尤其需要检查。
  • 重复标的应当在查询前处理。 去重可以保持输入语义清晰,但不会自动解决重复行情记录的问题,返回数据仍要单独检查。
  • 不要在异常发生时直接返回空表。 查询失败与真实无数据是两种不同状态,混在一起可能导致策略错误地跳过某些股票。
  • 基础封装不等于生产级数据服务。 超时控制、重试策略、访问频率管理、缓存和监控都需要根据实际运行环境补充,不能假定 SDK 已自动完成。

常见问题(FAQ)

Q:为什么不直接在每个策略里调用 qd.klines.batch()?

A:直接调用适合一次性研究。如果多个策略长期使用相同的数据接口,统一封装能够减少重复参数校验和异常处理,并让数据接入逻辑更容易维护。

Q:封装函数是否应该固定 period 和 count?

A:不建议完全固定。可以设置默认值,同时允许调用方覆盖。不同研究任务所需的数据频率和历史窗口并不相同。

Q:返回 DataFrame 以后,是否就可以直接计算因子?

A:不能直接假设可以。至少需要确认标的标识、时间字段、价格字段、数据类型以及记录顺序,再进入因子计算。

Q:为什么查询函数中没有直接加入动量计算?

A:行情获取和因子计算属于不同职责。把它们拆开,可以让同一批行情数据服务于动量、波动率和其他研究任务。

Q:生产环境是否应该让查询函数自动重试?

A:可以根据实际需求设计,但必须先确认接口的错误类型、访问限制和可重试条件。对于参数错误或权限错误,盲目重试通常没有意义。

小结

为 qd.klines.batch() 建立可复用函数,最重要的不是减少几行代码,而是让不同量化策略使用统一的数据查询入口。通过参数校验、标的去重、异常处理和结果验证,可以减少研究脚本之间的数据接入差异。

QuantDash Python SDK 提供了批量历史 K 线获取的接口,开发者可以在此基础上建立自己的数据访问层。至于字段适配、完整性检查、因子计算和缓存管理,仍然需要由业务系统明确实现。

当数据获取和业务计算形成清晰的分层以后,后续扩展更多策略时,就能够减少重复维护工作,也更容易追踪研究数据的来源和处理过程。

参考

QuantDash Python SDK 文档:https://docs.quantdash.net/zh-Hans/sdk/python-quickstart

相关推荐
神仙别闹3 小时前
基于Python实现英词语自动对齐系统
开发语言·python
akinggw3 小时前
libagentcore - 一个基于c++的AI多agent系统升级到2.0版本
开发语言·jvm·c++
老歌老听老掉牙3 小时前
圆柱端面四平面交线及其在yoz面投影的几何解析与 Python 实现
开发语言·python·平面
我要见SA姐14 小时前
Chrome 扩展安全攻防实录:从权限滥用到供应链投毒
运维·人工智能·python·机器学习·自动化
2601_962885724 小时前
给 backtrader 接 A 股数据:用 AlphaFeed 喂PandasData
python
旖旎夜光4 小时前
【C++ 基础入门】命名空间、输入输出、缺省参数与函数重载
开发语言·c++·笔记·学习
YYYing.4 小时前
【Agent系列 (十一) 】上下文工程
人工智能·python·ai·agent·上下文工程
俩个逗号。。4 小时前
Kotlin flow 详解
android·开发语言·kotlin
计算机毕业编程指导师4 小时前
基于深度学习的光伏组件红外图像缺陷识别系统设计与实现
人工智能·python·深度学习·目标检测·毕业设计·课程设计·光伏