如何验证历史行情的交易日期是否完整覆盖研究区间?

一句话回答

验证历史行情是否满足研究区间要求,不能只检查最早日期和最晚日期,而应该将实际返回的交易日期与研究区间内的预期交易日集合进行比较,同时检查缺失日期、重复日期和越界记录。对于量化回测,这一步可以避免因行情缺口导致收益率和因子计算出现偏差。实际开发中,可以通过 QuantDash Python SDK 获取指定区间的历史 K 线,再使用 Python 完成交易日完整性校验。

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

量化策略通常需要在指定时间范围内完成选股、因子计算或历史回测。

例如,研究任务要求使用 2025 年全年的日线行情。开发者通过接口获取数据后,可能首先检查:

  • 数据最早日期是否接近 2025 年 1 月初。
  • 数据最晚日期是否接近 2025 年 12 月底。
  • 返回记录数量是否达到预期。

这种检查能够初步判断数据范围,但不能证明研究区间内的交易日已经完整覆盖。

假设一个研究区间内有 20 个预期交易日,行情数据也返回了 20 行。

如果其中某个交易日重复出现两次,同时另一个交易日缺失,那么:

记录数量 = 预期交易日数量

但实际数据仍然存在质量问题。

另外,自然日并不等于交易日。

A 股市场存在周末、法定节假日以及特殊休市安排。直接按照自然日计算缺失情况,会把正常休市日期误判为行情缺口。

反过来,只检查周一到周五,也无法准确识别交易所假期。

对于历史行情,可以将完整性定义为:

预期交易日期集合 E = 研究区间内应当存在行情记录的交易日期。

实际交易日期集合 A = 历史 K 线中实际出现的交易日期。

那么:

缺失日期集合:

M = E-A

额外日期集合:

X = A-E

这里的额外日期可能来自查询边界、日期解析或不同数据集的混入,不应直接认定为错误交易日。

完整性检查还需要单独处理重复记录,因为集合运算会自动去重。

因此,工程上至少需要验证四件事:

  1. 交易日期字段能够正确解析。
  2. 实际日期没有超出研究区间。
  3. 研究区间内不存在非预期的交易日期。
  4. 预期交易日没有缺失,且每个日期对应的记录数量符合要求。

如果使用统一的历史行情接入方式,就可以把数据获取与上述校验逻辑分离,避免不同策略重复处理相同问题。

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

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

首先安装 QuantDash Python SDK:

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

后续示例使用 pandas 处理行情日期。

客户端初始化时,通过环境变量读取 API Key,避免将凭证写进代码仓库。

python 复制代码
import os
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 步:获取指定研究区间的历史行情

这里选择 A 股标的 600519.SH,研究区间设置为 2025 年 1 月 1 日至 2025 年 12 月 31 日。

使用 qd.klines.get() 获取日线行情。

与只指定 count 不同,这里使用 start_time 和 end_time 明确查询时间范围。

python 复制代码
from datetime import datetime, date, timedelta
from zoneinfo import ZoneInfo
import pandas as pd

symbol = "600519.SH"

research_start = date(2025, 1, 1)
research_end = date(2025, 12, 31)

if research_start > research_end:
    raise ValueError("研究起始日期不能晚于结束日期")

tz = ZoneInfo("Asia/Shanghai")

start_dt = datetime.combine(
    research_start,
    datetime.min.time(),
    tzinfo=tz
)

end_dt = datetime.combine(
    research_end + timedelta(days=1),
    datetime.min.time(),
    tzinfo=tz
)

start_ms = int(start_dt.timestamp() * 1000)
end_ms = int(end_dt.timestamp() * 1000)

try:
    df = qd.klines.get(
        symbol,
        period="1d",
        start_time=start_ms,
        end_time=end_ms,
        adjust="none",
        to_dataframe=True,
    )
except Exception as exc:
    raise RuntimeError(
        f"历史行情获取失败:{symbol}"
    ) from exc

if not isinstance(df, pd.DataFrame) or df.empty:
    raise ValueError("历史行情为空或返回结构不符合预期")

if "trade_date" not in df.columns:
    raise KeyError(
        f"缺少交易日期字段,实际字段:{list(df.columns)}"
    )

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

这里有三个关键参数:

  • period="1d":使用日线数据,避免把分钟时间戳误当成交易日期。
  • start_time 、end_time:控制历史数据的查询范围,使用毫秒时间戳。
  • adjust="none":明确使用不复权行情。本例只检查交易日期,复权方式并不决定交易日是否完整。

查询结束使用次日零点,是为了构造覆盖研究结束日的时间边界。实际接口是否采用闭区间或半开区间,仍需以 SDK 实际行为为准,后面会根据日期再次过滤。

第 3 步:检查交易日期的基础合法性

获取数据后,不能立即拿日期集合做差集。

应该先检查日期字段是否存在空值、非法值、重复值和越界记录。

python 复制代码
from datetime import date

def validate_trade_dates(
    frame: pd.DataFrame,
    start_date: date,
    end_date: date,
):
    if frame.empty:
        raise ValueError("行情数据为空")

    if "trade_date" not in frame.columns:
        raise KeyError("缺少 trade_date 字段")

    result = frame.copy()

    raw_dates = result["trade_date"]

    if raw_dates.isna().any():
        raise ValueError("trade_date 存在空值")

    parsed = pd.to_datetime(
        raw_dates,
        errors="coerce"
    )

    if parsed.isna().any():
        raise ValueError("存在无法解析的交易日期")

    result["trade_date"] = parsed.dt.date

    duplicate_mask = result.duplicated(
        subset=["trade_date"],
        keep=False
    )

    duplicate_dates = sorted(
        result.loc[
            duplicate_mask, "trade_date"
        ].unique()
    )

    out_of_range = result[
        (result["trade_date"] < start_date) |
        (result["trade_date"] > end_date)
    ]

    if not out_of_range.empty:
        raise ValueError(
            "存在超出研究区间的记录:"
            f"{out_of_range['trade_date'].tolist()}"
        )

    if duplicate_dates:
        raise ValueError(
            f"存在重复交易日期:{duplicate_dates}"
        )

    return result.sort_values(
        "trade_date"
    ).reset_index(drop=True)


clean_df = validate_trade_dates(
    df,
    research_start,
    research_end
)

print("基础校验通过")
print("实际日期范围:")
print(
    clean_df["trade_date"].min(),
    clean_df["trade_date"].max()
)

这段代码完成基础结构校验,但还不能证明数据完整。

例如,缺少某个正常交易日时,日期格式、重复性和区间边界检查都可能通过。

所以,还需要引入交易日历。

第 4 步:对照交易日历,生成完整性检查结果

交易日历应当来自经过核对的交易所日历或研究系统维护的权威日历文件。

假设本地已准备 exchange_calendar.csv ,其中包含 trade_date 字段,每行代表一个已经确认的正常交易日。

这里不使用 pandas 的工作日函数生成交易日历,因为它无法准确覆盖市场休市安排。

python 复制代码
from pathlib import Path

calendar_path = Path("exchange_calendar.csv")

if not calendar_path.is_file():
    raise FileNotFoundError(
        "请准备经过核对的 exchange_calendar.csv"
    )

calendar_df = pd.read_csv(calendar_path)

if "trade_date" not in calendar_df.columns:
    raise KeyError("交易日历缺少 trade_date 字段")

if calendar_df["trade_date"].isna().any():
    raise ValueError("交易日历存在空日期")

calendar_dates = pd.to_datetime(
    calendar_df["trade_date"],
    errors="coerce"
)

if calendar_dates.isna().any():
    raise ValueError("交易日历存在非法日期")

expected_dates = set(
    calendar_dates.dt.date
)

expected_dates = {
    d for d in expected_dates
    if research_start <= d <= research_end
}

if not expected_dates:
    raise ValueError("研究区间内没有预期交易日")

actual_dates = set(
    clean_df["trade_date"]
)

missing_dates = sorted(
    expected_dates - actual_dates
)

unexpected_dates = sorted(
    actual_dates - expected_dates
)

coverage_rate = (
    len(actual_dates & expected_dates) /
    len(expected_dates)
)

report = {
    "symbol": symbol,
    "expected_count": len(expected_dates),
    "actual_count": len(actual_dates),
    "missing_dates": missing_dates,
    "unexpected_dates": unexpected_dates,
    "coverage_rate": coverage_rate,
    "passed": (
        not missing_dates and
        not unexpected_dates
    ),
}

print("完整性检查结果:")
print(report)

if not report["passed"]:
    print("行情日期未通过完整性检查")
else:
    print("行情日期满足预期交易日集合要求")

覆盖率公式为:

Coverage=\\frac{\|A\\cap E\|}{\|E\|}

如果覆盖率低于 100%,说明至少存在一个预期日期没有被覆盖。

但需要注意,100% 的覆盖率也不一定代表质量检查通过,因为实际数据中仍可能存在非预期日期。

另外,单只股票还可能因上市时间、停牌或退市等原因,没有覆盖全部交易所开市日期。

因此,示例中的严格检查适用于研究任务已经明确要求每个预期交易日均有一条记录的情况。

对于允许停牌缺行的数据口径,应进一步引入标的状态信息,区分正常缺行和数据异常。

关键坑与注意事项

  • 不要用自然日判断交易日完整性。 周末和法定节假日通常不产生日线行情,必须基于目标市场的交易日历建立预期集合。

  • 不能只检查记录数量。 重复日期可能掩盖缺失日期,因此需要分别检查日期唯一性和集合差异。

  • 注意时间戳的时区。 创建毫秒时间戳时建议明确使用目标交易所时区,避免服务器时区变化影响查询边界。

  • 停牌不一定等于数据缺失。 某些数据源可能保留停牌占位记录,也可能不返回记录。具体判断必须结合数据源口径与证券状态。

  • 历史行情可能发生修订。 如果研究要求可复现,需要保存行情获取时间、研究区间、数据版本标识(若可获得)以及检查报告。

  • 空结果不能直接解释为停牌。 接口请求异常、数据权限、参数配置或标的代码错误,都可能导致数据为空,应先排查接入问题。

常见问题(FAQ)

Q:历史行情的首尾日期正确,能否说明研究区间完整?

A:不能。首尾日期正确,只能证明区间边界存在相应记录,无法保证中间没有缺失交易日。

Q:能否用 pandas 的工作日函数代替交易所日历?

A:不建议。工作日函数通常只能处理周末规则,不能自动准确覆盖交易所特殊休市安排。

Q:QuantDash 会自动判断历史行情缺失吗?

A:本文使用 QuantDash SDK 获取历史 K 线。交易日期差集、覆盖率和完整性判断由 Python 代码实现,不应将这些逻辑视为 SDK 自动提供的功能。

Q:为什么需要检查重复交易日期?

A:重复记录可能造成同一天行情被重复纳入统计,影响收益率、因子窗口及回测数据处理。

Q:如果一只股票存在停牌日期,应该怎样处理?

A:应先明确研究系统对停牌日期的处理规则,再结合证券状态或其他可信数据进行判断,不能简单将所有缺行视为接入失败。

小结

验证历史行情中的交易日期是否满足研究区间要求,核心不是检查数据有多少行,而是确认实际交易日期与预期交易日集合之间是否一致。缺失日期、重复日期、异常日期和边界错误,都可能影响后续因子计算和历史回测。

QuantDash Python SDK 可以承担指定标的和时间区间的历史 K 线获取工作。研究系统则需要自行管理交易日历、执行日期集合校验、处理停牌等特殊情况,并保存质量检查报告。将这些规则作为数据进入策略计算前的固定步骤,有助于提高研究结果的可靠性与可复现性。

参考

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

相关推荐
I Am a robert girl1 小时前
当智能体学会“串通”:从长程交互中读懂涌现式合谋
python·多智能体系统·多智能体协作·llm agent·涌现式合谋·对齐问题·激励机制设计
量化炼金 (CodeAlchemy)1 小时前
【量化交易】避开四次熔断的标普500择时模型:趋势+波动率+信用利差
量化交易·美股·标普500·市场择时
云和数据.ChenGuang2 小时前
光哥说Python RAG 系统需要哪些评估指标:从检索到生成的全链路评测指南
网络·人工智能·python·算法·langchain
专注仿真2 小时前
线性回归和逻辑回归算法详解
开发语言·人工智能·python·大模型训练·控制算法
wflynn3 小时前
GitHub 今日推荐|Edge0:让大号 MoE 模型在一台 Mac 上流式跑起来
python·开源·github
2601_967980114 小时前
python之用scapy分层解析pcap报文(Ethernet帧、IP数据包、TCP数据包、UDP数据包、Raw数据包)
python·scapy·pcap·协议分析·网络数据包
心疼你的一切4 小时前
Python自动发邮件实战定时发报表
大数据·开发语言·python
卡提西亚5 小时前
FastAPI基础入门
python·fastapi
fanruitian6 小时前
python 机器学习 ML LinearRegression KNN KMeans
python·机器学习·kmeans