股票池代码为什么总有问题?从常见误区到可解释的校验报告

一个量化策略连续运行了几个月,某天研究员发现,回测中实际参与计算的股票比预期少了十几只。

检查策略逻辑没有发现明显问题,再看数据获取日志,才发现部分代码使用了错误的市场后缀,另一些代码则包含不可见的空白字符。程序没有在入口处拦截这些问题,异常一直传递到行情查询和后续的数据处理环节。

这类问题并不罕见。股票池通常来自多个渠道:研究员维护的 CSV、Excel 自选表、数据库中的证券列表,以及程序自动生成的候选标的。不同来源的数据格式未必一致。

与其在每次查询失败时临时修改代码,不如建立一套可解释的校验机制。

本文关注的重点不是如何写一个复杂的验证框架,而是如何避免三种容易混淆的判断:格式是否正确、标的是否匹配、行情是否可获取。

先看几个容易误判的代码

假设研究股票池中出现以下内容:

python 复制代码
symbols = [
    "600519.SH",
    "600519.SH ",
    "600519",
    "600519.SZ",
    "AAPL.US",
    "00700.HK",
]

这些字符串看起来相似,但它们需要不同的处理方式。

  • 600519.SH:符合常见的 A 股代码表示形式。
  • 600519.SH :末尾存在空格,可能来自人工编辑或数据导入。
  • 600519:缺少市场标识,无法仅凭字符串确定系统预期的规范格式。
  • 600519.SZ:形式上符合常见的 A 股代码模式,但与贵州茅台的市场标识不匹配。
  • AAPL.US:采用美股代码的常见表示形式。
  • 00700.HK:采用港股代码的常见表示形式,前导零需要保留。

这里有一个容易忽略的细节:600519.SZ 并不一定会被基础正则表达式判定为格式错误。

它符合六位数字加 .SZ 的形式。真正的问题是,这个代码是否对应预期的证券。

因此,校验程序不能只输出一个 True 或 False,还应告诉研究员程序发现了什么,以及哪些问题需要进一步核实。

误区一:先把代码清洗干净,再说

很多脚本会在检查之前统一执行:

python 复制代码
symbol = symbol.strip().upper()

这段代码可以去除首尾空白,并统一英文字母大小写。但它也可能掩盖数据来源的问题。

假设原始数据是:

text 复制代码
600519.SH 

清洗后得到:

text 复制代码
600519.SH

如果程序只保存清洗后的值,研究员就很难知道原始数据曾经存在格式异常。

这在手工维护的研究股票池中尤其值得注意。格式问题可能不是偶发的输入错误,而是某个数据生成流程持续存在的缺陷。

更稳妥的设计是将规范化作为一个独立步骤,并保留原始值。

python 复制代码
def normalize_symbol(value):
    if not isinstance(value, str):
        return None

    normalized = value.strip()

    if not normalized:
        return None

    return normalized

这里没有直接将所有字母转换成大写,也没有自动补全市场后缀。

为什么?

因为不同数据源可能有自己的代码规范。未经确认就自动修改字符串,可能会把需要人工确认的异常变成看似正常的数据。

在一些研究环境中,自动修复可以作为便利功能,但应该同时记录原始值、修复后的值和修复规则。

误区二:代码符合格式,就认为它有效

正则表达式擅长检查字符串结构,不擅长确认证券身份。

例如:

python 复制代码
import re

pattern = re.compile(r"^\d{6}\.(SH|SZ|BJ)$")

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

for symbol in symbols:
    print(symbol, bool(pattern.fullmatch(symbol)))

这段代码检查的是六位数字和指定后缀的组合形式。

它无法独立判断:

  • 该代码是否真实对应某只证券。
  • 证券是否属于预期市场。
  • 证券是否符合研究策略的纳入条件。
  • 当前数据服务是否提供该标的的行情。

因此,格式规则应该被理解为第一道筛选条件,而不是完整的证券有效性验证。

对于需要跨市场运行的研究系统,可以为每个市场维护独立的格式规则,避免把所有代码塞进一条不断扩张的正则表达式。

此外,格式合法也不意味着标的符合策略要求。例如,一个代码即使能够查询,也可能不属于研究员希望纳入的 ETF 或股票范围。

用 QuantDash 的真实行情查询补足本地检查

如果研究任务依赖金融行情,格式校验之后可以继续检查代码能否用于实际数据查询。

QuantDash 官方 Python 示例提供了以下调用方式:

python 复制代码
from quantdash import QuantDash

qd = QuantDash()

df = qd.klines.get(
    "600519.SH",
    period="1d",
    count=5,
    adjust="forward",
    to_dataframe=True,
)

运行前,需要安装 quantdash,并通过环境变量配置有效的 QUANTDASH_API_KEY。

这个示例请求指定标的的日 K 数据,并将结果转换为 Pandas DataFrame。它可以作为研究股票池校验流程中的数据查询环节。

需要强调的是,这里并没有把行情接口当成通用的代码存在性数据库。

如果查询返回空 DataFrame,合理的结论是当前请求没有得到可用记录,而不是直接断言该代码不存在。如果请求抛出异常,也需要先判断异常来自哪里。

一个可维护的校验流程应该把这些结果分别记录。

例如,可以使用下面的状态设计:

状态 含义 后续动作
format_invalid 未通过本地格式规则 检查原始输入
returned_data 查询返回非空数据 继续检查数据质量
empty_result 查询没有返回记录 检查数据范围和查询条件
request_error 请求过程中出现异常 根据异常类型进一步排查
not_checked 尚未执行行情查询 等待后续处理

这种设计比将所有问题统一标记为 invalid 更有价值,因为不同状态对应不同的修复方法。

建立可解释的检查报告

下面用 Pandas 构建一个简化的股票池报告。

为了避免将格式规则和标的身份验证混为一谈,示例将它们分开处理。

python 复制代码
import re
import pandas as pd

PATTERNS = {
    "CN": re.compile(r"^\d{6}\.(SH|SZ|BJ)$"),
    "HK": re.compile(r"^\d{5}\.HK$"),
    "US": re.compile(r"^[A-Z][A-Z0-9-]*\.US$"),
}


def validate_format(value):
    if not isinstance(value, str):
        return "format_invalid", "输入不是字符串"

    if not value:
        return "format_invalid", "代码为空"

    if value != value.strip():
        return "format_invalid", "存在首尾空白字符"

    if not value.isascii():
        return "format_invalid", "包含非 ASCII 字符"

    if value.endswith((".SH", ".SZ", ".BJ")):
        market = "CN"
    elif value.endswith(".HK"):
        market = "HK"
    elif value.endswith(".US"):
        market = "US"
    else:
        return "format_invalid", "市场后缀不符合当前规则"

    if not PATTERNS[market].fullmatch(value):
        return "format_invalid", "代码结构不符合当前规则"

    return "format_valid", "通过格式检查"


universe = pd.DataFrame({
    "symbol": [
        "600519.SH",
        "600519.SH ",
        "600519",
        "600519.SZ",
        "AAPL.US",
        "00700.HK",
    ]
})

results = universe["symbol"].apply(validate_format)

universe[["format_status", "format_reason"]] = pd.DataFrame(
    results.tolist(),
    index=universe.index,
)

universe["identity_status"] = "not_checked"
universe["query_status"] = "not_checked"

print(universe)

这段代码不会因为一个代码格式正确,就擅自将 identity_status 改成有效。

原因很简单:格式检查和证券身份核验是两个不同的任务。

在后续流程中,可以将需要确认身份的代码与可信的证券主数据进行匹配;对于需要获取行情的代码,再执行 QuantDash 查询,并记录实际查询状态。

正式使用时,还应考虑空字符串、None、Pandas 的 NA 值以及重复代码等输入情况。对于包含大量数据的股票池,也应避免在每一条记录中重复执行相同的主数据查询。

重复代码也是股票池质量问题

代码格式检查完成后,还有一个很容易被忽略的现象:同一只股票可能在研究股票池中出现多次。

例如:

python 复制代码
symbols = [
    "600519.SH",
    "000001.SZ",
    "600519.SH",
]

重复记录可能导致后续程序重复请求行情、重复生成研究结果,或者在组合构建时产生非预期的权重分配。

可以通过 Pandas 检查:

python 复制代码
duplicate_mask = universe["symbol"].duplicated(
    keep=False
)

duplicates = universe.loc[
    duplicate_mask
].sort_values("symbol")

print(duplicates)

不过,重复检查应该基于明确的规范化策略。

如果同一个标的分别以 600519.SH 和 600519 出现,简单的字符串去重不会发现它们可能代表同一证券。反过来,也不能仅因为两个代码数字相同,就认定它们是同一个标的。

这再次说明,字符串规范化、标的身份匹配和重复检查应当有清晰的职责边界。

让校验结果成为研究流程的一部分

如果股票池只在研究开始时检查一次,之后由人工随意修改,那么这套校验机制的作用会很有限。

更实用的做法是将其放到数据入口处:

  1. 读取股票池,并将代码作为字符串处理。
  2. 保留原始输入,生成规范化候选值。
  3. 执行市场格式检查。
  4. 检查重复项和必要的标的身份信息。
  5. 对通过筛选的代码执行行情查询。
  6. 记录查询状态,并对返回的数据执行必要的完整性检查。

其中,格式规则和重复检查可以完全在本地完成。标的身份验证需要可信的主数据或相应的查询能力。行情获取则需要根据研究任务选择合适的数据接口。

这套流程不需要一开始就做得很复杂。对于个人研究项目,一个包含原始代码、格式状态、异常原因和查询状态的 CSV 报告,就足以显著改善问题定位效率。

对于持续运行的研究系统,则可以进一步把这些结果保存到数据库,并对格式异常率、查询失败率和标的覆盖变化进行跟踪。

最终,真正有价值的并不是让所有代码都变成 True,而是让研究员清楚地知道:哪些代码可以继续处理,哪些代码需要确认,以及问题具体发生在哪一个环节。

QuantDash 官方文档

相关推荐
考虑考虑8 小时前
nginx路由重写
运维·后端·自动化运维
阿里嘎多学长8 小时前
2026-10-20 GitHub 热点项目精选
开发语言·程序员·github·代码托管
JaguarJack9 小时前
PHP 紧急修复两项严重 RCE 漏洞,8.2 至 8.5 用户应立即升级
后端·php·服务端
BingoGo9 小时前
PHP 紧急修复两项严重 RCE 漏洞,8.2 至 8.5 用户应立即升级
后端·php
金銀銅鐵9 小时前
[Java] 展示添加元素时 ArrayList 的内部变化
java·后端·ai编程
斌糖雪梨10 小时前
spring auth 核心流程详解
java·后端·spring
高频因子挖掘机11 小时前
本地已有一年历史数据,第二天更新别只拉“昨天一天”:量化 K 线增量同步实践
后端·github·api
高频因子挖掘机11 小时前
只补缺失日期,不重拉全部历史:增量更新 K 线应该怎么设计?
后端·github·api
Gopher_HBo12 小时前
Go reflect 包动态调用与方法反射 —Call、MakeFunc 与性能
后端