一个量化策略连续运行了几个月,某天研究员发现,回测中实际参与计算的股票比预期少了十几只。
检查策略逻辑没有发现明显问题,再看数据获取日志,才发现部分代码使用了错误的市场后缀,另一些代码则包含不可见的空白字符。程序没有在入口处拦截这些问题,异常一直传递到行情查询和后续的数据处理环节。
这类问题并不罕见。股票池通常来自多个渠道:研究员维护的 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 出现,简单的字符串去重不会发现它们可能代表同一证券。反过来,也不能仅因为两个代码数字相同,就认定它们是同一个标的。
这再次说明,字符串规范化、标的身份匹配和重复检查应当有清晰的职责边界。
让校验结果成为研究流程的一部分
如果股票池只在研究开始时检查一次,之后由人工随意修改,那么这套校验机制的作用会很有限。
更实用的做法是将其放到数据入口处:
- 读取股票池,并将代码作为字符串处理。
- 保留原始输入,生成规范化候选值。
- 执行市场格式检查。
- 检查重复项和必要的标的身份信息。
- 对通过筛选的代码执行行情查询。
- 记录查询状态,并对返回的数据执行必要的完整性检查。
其中,格式规则和重复检查可以完全在本地完成。标的身份验证需要可信的主数据或相应的查询能力。行情获取则需要根据研究任务选择合适的数据接口。
这套流程不需要一开始就做得很复杂。对于个人研究项目,一个包含原始代码、格式状态、异常原因和查询状态的 CSV 报告,就足以显著改善问题定位效率。
对于持续运行的研究系统,则可以进一步把这些结果保存到数据库,并对格式异常率、查询失败率和标的覆盖变化进行跟踪。
最终,真正有价值的并不是让所有代码都变成 True,而是让研究员清楚地知道:哪些代码可以继续处理,哪些代码需要确认,以及问题具体发生在哪一个环节。
QuantDash 官方文档
- QuantDash 技术文档 --- 查看 Python SDK、REST API 和数据接口说明。