首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >300只股票两年日K,如何避免逐只请求失控

300只股票两年日K,如何避免逐只请求失控

原创
作者头像
用户9138916
发布于 2026-10-04 16:49:08
发布于 2026-10-04 16:49:08
550
举报

问题不在循环,而在循环承担了太多职责

获取300只股票两年的日K,直觉上是遍历代码、逐只请求,再把结果拼起来。标的少、偶尔运行时,这样做能工作;一旦加入超时处理、重试、复权口径、数据校验和断点恢复,循环很快就会变成一团难维护的流程。

更稳妥的做法是把任务拆成几层:按批次请求、按标的独立校验、按标的保存,并明确记录失败对象。批量接口可以减少调用组织上的重复,但不要在没有实测的情况下假定它一定更快,也不要假定服务端支持任意大的单批标的数。

本文聚焦A股两年日K,不讨论分钟线。示例使用 QuantDash Python SDK 的批量 K 线接口;分块大小是客户端可调参数,不代表服务端的单批上限。

先固定数据口径

批量抓取前,先把四个约定写进任务配置:

  • 标的代码:使用带交易所后缀的代码,例如 600519.SH、000001.SZ。
  • 时间范围:起止时间统一采用毫秒时间戳;示例按上海时区生成边界。接口文档将起止边界描述为包含边界,取回后仍应按交易日期过滤确认。
  • 复权方式:示例采用前复权 forward。比例复权常用于收益率计算,但前复权、后复权和不复权的价格序列并不相同;研究结果应记录口径,不能把一种选择当作所有任务的标准答案。
  • 字段与粒度:这是日K,核心字段是交易日期和 OHLCV。分钟K线有不同的时间字段与数据边界,不能直接沿用本例的日线校验逻辑。

两年自然日通常覆盖的交易日少于600个,因此示例留出一定余量,并使用起止时间限定范围。返回条数不是精确交易日数;节假日、上市时间和停牌都会影响每只股票的记录数。

用批量接口,但把批次控制权留在客户端

安装并固定 SDK 版本,避免环境升级改变接口行为:

代码语言:bash
复制
pip install quantdash==0.1.0 pandas pyarrow

在 macOS/Linux 中通过环境变量提供密钥:

代码语言:bash
复制
export QUANTDASH_API_KEY="your_api_key_here"

Windows PowerShell 使用:

代码语言:powershell
复制
$env:QUANTDASH_API_KEY = "your_api_key_here"

下面的脚本按每批50只组织请求。50只是便于控制单次失败影响范围的客户端示例值,不是接口限制或性能结论。实际使用时,可以根据接口响应、失败情况和运行环境调整。

代码语言:python
复制
from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo
import os

import pandas as pd
from quantdash import QuantDash

# 示例日期范围;运行任务时按研究区间修改。
START_DATE = "2023-01-01"
END_DATE = "2024-12-31"
CHUNK_SIZE = 50
OUT_DIR = Path("data/daily_forward")

symbols = [
    "600519.SH",
    "000001.SZ",
    # 在这里放入完整的300只标的代码
]

if not os.getenv("QUANTDASH_API_KEY"):
    raise RuntimeError("请先设置 QUANTDASH_API_KEY")

if len(symbols) != len(set(symbols)):
    raise ValueError("symbols 中存在重复代码")

# 转为上海时区的毫秒时间戳;结束边界包含当天最后一毫秒。
shanghai = ZoneInfo("Asia/Shanghai")
start_time = int(
    datetime.strptime(START_DATE, "%Y-%m-%d")
    .replace(tzinfo=shanghai)
    .timestamp() * 1000
)
end_time = int(
    datetime.strptime(END_DATE, "%Y-%m-%d")
    .replace(tzinfo=shanghai)
    .replace(hour=23, minute=59, second=59, microsecond=999000)
    .timestamp() * 1000
)

qd = QuantDash()
OUT_DIR.mkdir(parents=True, exist_ok=True)

required = {"trade_date", "open", "high", "low", "close", "volume"}
failed_chunks = []

for offset in range(0, len(symbols), CHUNK_SIZE):
    chunk = symbols[offset : offset + CHUNK_SIZE]
    try:
        frames = qd.klines.batch(
            chunk,
            period="1d",
            start_time=start_time,
            end_time=end_time,
            adjust="forward",
            to_dataframe=True,
            show_progress=True,
        )
    except Exception as exc:
        # 记录整批失败;不要把失败静默转换为空数据。
        failed_chunks.append({"symbols": chunk, "error": repr(exc)})
        continue

    if not isinstance(frames, dict):
        failed_chunks.append({
            "symbols": chunk,
            "error": f"预期 dict[str, DataFrame],实际为 {type(frames).__name__}",
        })
        continue

    for symbol in chunk:
        df = frames.get(symbol)
        if df is None or df.empty:
            failed_chunks.append({
                "symbols": [symbol],
                "error": "结果缺失或为空,请区分无数据与请求失败",
            })
            continue

        missing = required - set(df.columns)
        if missing:
            failed_chunks.append({
                "symbols": [symbol],
                "error": f"缺少字段:{sorted(missing)}",
            })
            continue

        clean = df.copy()
        clean["trade_date"] = pd.to_datetime(clean["trade_date"], errors="coerce")
        clean = clean.dropna(subset=["trade_date"])
        clean = clean.sort_values("trade_date")
        clean = clean.drop_duplicates(subset=["trade_date"], keep="last")

        # 再按日期过滤,显式确认数据落在请求窗口内。
        start = pd.Timestamp(START_DATE)
        end = pd.Timestamp(END_DATE)
        clean = clean.loc[
            clean["trade_date"].dt.normalize().between(start, end)
        ].reset_index(drop=True)

        if clean.empty:
            failed_chunks.append({
                "symbols": [symbol],
                "error": "过滤日期范围后没有有效记录",
            })
            continue

        # 每只股票单独落盘,便于定位和重跑;同名文件会被本次结果覆盖。
        target = OUT_DIR / f"{symbol}.parquet"
        temp = OUT_DIR / f"{symbol}.parquet.tmp"
        clean.to_parquet(temp, index=False, engine="pyarrow")
        temp.replace(target)

if failed_chunks:
    failure_path = OUT_DIR / "failed_chunks.json"
    pd.Series(failed_chunks).to_json(
        failure_path,
        force_ascii=False,
        indent=2,
    )
    print(f"存在失败项,记录已写入 {failure_path}")
else:
    print("请求批次均已完成;请继续检查数据覆盖率和字段质量。")

代码没有假设SDK会自动重试,也没有虚构运行耗时或返回行数。遇到失败时,它将失败批次或缺失标的写入记录,同时保留已成功写入的单标的文件。排查后可以针对失败标的重新运行;若要实现自动断点续跑,应额外保存任务参数和每个标的的完成状态,避免把旧文件误当成当前口径的结果。

为什么按标的保存比直接拼成一个大表更好维护

300只股票的返回数据可以最终合并分析,但在采集阶段,按标的保存有几个直接好处:

  1. 失败范围清楚:能区分某只股票无数据、字段异常和整批请求失败。
  2. 重复运行更可控:单个文件对应一个标的;更换时间范围或复权口径时,也可以用不同目录隔离数据集。
  3. 便于局部检查:研究者可以先抽查少量 Parquet 文件,再进入全量计算。
  4. 减少内存压力:不必把所有批次长期保存在一个大对象里。若后续需要合并,可在分析步骤中读取文件并添加标的标识。

注意:目录名只是人为标记,不能替代元数据。正式任务还应保存起止日期、复权方式、SDK版本、标的列表版本和任务运行时间。否则,即使文件完整,也未必能回答“这份数据是按什么口径生成的”。

质量检查不能只看接口是否返回

接口返回了 DataFrame,不代表这份数据可以直接用于回测。至少需要检查以下内容:

  • 覆盖率:逐只统计记录数和首末交易日期。上市时间晚于区间起点的股票,本来就不应被要求覆盖完整两年。
  • 日期顺序和重复:检查交易日期是否单调递增、是否存在重复日期。重复处理策略应明确;示例保留排序后的最后一条,但这只是工程处理,不等同于确认重复记录的业务含义。
  • 价格关系:在排除空值后检查 high >= low,以及开盘价、收盘价是否落在最高价与最低价之间。异常值应记录并调查,不要直接静默修正。
  • 缺失值和数值类型:检查 OHLCV 是否为空、是否为可用于计算的数值类型;成交量的具体单位应以数据定义为准,不要自行假定。
  • 复权一致性:同一批研究数据应使用一致的复权设置。若把不同口径的数据拼在一起,收益率和价格比较会失去一致性。

在回测中,还要注意历史股票池的定义。若只用当前仍上市的300只股票回看两年,可能产生幸存者偏差;这不是批量接口或DataFrame清洗可以自动消除的问题。

常见维护陷阱

把“批量”误解成“无限大单次请求”

批量调用减少了逐只编写请求的工作,但不意味着任何规模都适合一次提交。采用客户端分块,失败时影响范围更小;批次大小应通过实际任务的响应和失败记录调整,不要把示例值写成服务端承诺。

请求失败后写入空表

空表可能代表标的在区间内无记录,也可能是请求失败、权限不足或返回结构异常。将两者混为一谈,会让数据缺口悄悄进入后续分析。应分别记录请求状态、返回状态和数据质量结果。

把复权当成清洗步骤

复权不是简单的“修正错误价格”,而是研究口径选择。需要在数据文件、任务配置或实验记录中明确标注。不要把前复权价格与不复权价格直接拼接,也不要在不说明口径的情况下比较价格水平。

重跑时覆盖了不同参数的数据

如果前复权和不复权文件都写入同一目录,或者新旧日期范围使用同一文件名,后续很难辨认数据来源。将关键参数编码到数据集目录或伴随元数据保存,比依赖人工记忆可靠。

结论

300只股票的两年日K,核心不是把 for 循环改成一个批量调用,而是把采集任务设计成可诊断、可校验、可重跑的数据流程:按客户端可控的批次请求,逐标的验证字段和日期,按稳定口径落盘,并记录失败和任务参数。

批量接口可以简化请求组织;数据质量、复权一致性、失败恢复和回测偏差仍需要由研究流程负责。把这些边界分清,才不会让一段看似简单的下载代码变成长期维护负担。

FAQ

两年日K可以直接用 count=500 吗?

可以把条数作为一种取数方式,但它不等于精确的两年日期区间。若研究范围按日历日期定义,使用起止时间更清晰,并在返回后按交易日期检查边界和覆盖情况。

300只股票必须分批请求吗?

不应把“必须”或某个批次上限写死。分块是客户端的可靠性设计:可以缩小单次失败影响范围。批次大小需要结合实际请求表现和接口约束确认。

复权后的日K能直接用于所有回测吗?

不能。价格复权口径应与策略计算目标一致,并在数据集和回测配置中记录。还需单独处理股票池历史变化、停牌和其他会影响回测有效性的因素。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 问题不在循环,而在循环承担了太多职责
  • 先固定数据口径
  • 用批量接口,但把批次控制权留在客户端
  • 为什么按标的保存比直接拼成一个大表更好维护
  • 质量检查不能只看接口是否返回
  • 常见维护陷阱
    • 把“批量”误解成“无限大单次请求”
    • 请求失败后写入空表
    • 把复权当成清洗步骤
    • 重跑时覆盖了不同参数的数据
  • 结论
  • FAQ
    • 两年日K可以直接用 count=500 吗?
    • 300只股票必须分批请求吗?
    • 复权后的日K能直接用于所有回测吗?
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档