一份包含 200 只股票的自选列表,经过 Python 程序处理后,最终生成了 198 个行情查询任务。
这是否意味着程序漏掉了两只股票?
不一定。
原始列表可能本来就存在重复代码,也可能包含空白记录、格式错误的标的,或者同一证券在不同数据源中使用了不同的代码表示方式。
真正需要回答的问题是:程序能否解释每一条原始记录最终去了哪里,以及为什么被保留、合并或拒绝?
对于量化开发而言,自选股票列表不仅是一个输入文件,也是一组需要经过验证的数据记录。如果只使用 dropna()、strip() 和 drop_duplicates() 等操作快速生成代码列表,虽然能够减少一些明显的输入问题,却可能同时掩盖值得关注的数据质量异常。
本文从数据质量检查的角度,设计一套更适合实际行情接入流程的处理方法。
一、不要把所有异常都当作可以删除的垃圾数据
考虑下面这份原始列表:
python
raw_symbols = [
"600519.SH",
"000001.SZ",
"600519.SH",
" AAPL.US ",
"",
None,
"00700.HK",
"600519",
]
如果直接清洗:
python
symbols = list(
dict.fromkeys(
str(symbol).strip()
for symbol in raw_symbols
if symbol
)
)
结果看起来已经比较整齐。
但这段代码仍然存在几个问题。
首先,None、空字符串和格式不完整的代码被采用了不同的处理方式。其次,重复记录被自动合并,但程序没有留下重复发生的记录。最后,600519 可能因为缺少市场后缀而无法按照项目约定进行查询,却仍然进入了结果列表。
这里最重要的区别是:
数据清洗的目标不是让数据看起来整齐,而是让每一次数据转换都有明确的规则和可追溯的结果。
因此,不建议将所有异常直接删除。更好的方式是先识别异常类型,再决定是否保留。
二、为每条原始记录建立处理状态
在处理复杂自选列表时,可以为每条记录建立独立的状态,例如:
valid_candidate:通过初步格式检查。empty:代码为空。invalid_format:不符合项目规定的代码格式。duplicate:与此前记录重复。needs_verification:格式看似合理,但仍需确认标的有效性。
这些状态是本文设计的内部分类,不是 QuantDash 或其他行情 API 的官方状态码。
实现时,可以保留原始行号、原始代码和标准化后的代码:
python
import pandas as pd
def inspect_symbols(
values: list,
) -> pd.DataFrame:
records = []
for row_number, value in enumerate(
values,
start=1,
):
original = value
if value is None or pd.isna(value):
records.append({
"row_number": row_number,
"original_symbol": original,
"symbol": None,
"status": "empty",
})
continue
symbol = str(value).strip()
if not symbol:
status = "empty"
elif "." not in symbol:
status = "invalid_format"
else:
status = "valid_candidate"
records.append({
"row_number": row_number,
"original_symbol": original,
"symbol": symbol or None,
"status": status,
})
return pd.DataFrame(records)
使用示例:
python
raw_symbols = [
"600519.SH",
"000001.SZ",
"600519.SH",
" AAPL.US ",
"",
None,
"00700.HK",
"600519",
]
audit = inspect_symbols(raw_symbols)
print(audit)
这段代码只演示最基础的检查流程。
其中,是否包含句点只是一个简化的格式判断条件,不能用来证明代码有效。正式项目应根据实际支持的市场和代码规范,建立更严格的验证规则。
另外,如果数据来自 Excel,最好在读取阶段就保留原始行号或唯一记录 ID。否则,当转换结果发生变化时,可能很难追溯原始记录。
三、重复记录不一定意味着重复查询
重复代码是自选列表中常见的问题,但需要区分两种情况。
情况一:同一标的被重复加入
例如:
text
600519.SH
000001.SZ
600519.SH
如果任务是获取每只股票的一份日线数据,那么相同代码通常只需要进入一次查询清单。
情况二:同一标的对应不同业务任务
例如,同一只股票可能同时属于:
- 长期投资观察列表。
- 日内策略监控列表。
- 财报事件研究列表。
这时,直接按照 symbol 去重,可能会丢失分组或策略信息。
如果原始记录代表不同业务关系,可以采用复合键进行判断:
python
unique_tasks = watchlist.drop_duplicates(
subset=["symbol", "group"],
keep="first",
)
这段代码要求数据中存在 symbol 和 group 两列。
它表达的规则是:同一标的在同一分组中只保留一条记录,但允许它出现在不同分组中。
真正应该使用什么去重规则,取决于数据最终服务的任务,而不是取决于哪个 Pandas 函数最方便。
四、如何发现标准化过程造成的漏查?
有些问题不是原始数据错误,而是转换规则本身造成的。
例如,假设项目决定将标的代码中的英文字母统一转换为大写:
python
symbol = symbol.strip().upper()
这对符合项目约定的代码通常是合理的。
但是,如果程序进一步擅自添加市场后缀、删除特殊字符,或者根据代码长度推测市场,就可能改变原始记录的含义。
例如:
text
600519
不能仅凭字符串长度就确定它应该对应哪个市场。
更可靠的方式是建立显式的映射规则,并将无法判断的记录交由验证流程处理。
对于原始代码和标准化代码不一致的情况,也可以单独记录:
python
audit["changed"] = (
audit["original_symbol"].astype("string")
!= audit["symbol"].astype("string")
)
这项比较可以帮助发现标准化过程中发生的变化,但不等于变化一定错误。
例如,去除前后空格属于合理清洗,而自动补充市场后缀则需要明确的业务依据。
建议为以下情况建立专门的检查:
- 原始代码为空或缺失。
- 标准化后代码发生变化。
- 同一个代码出现多次。
- 代码不符合项目支持的格式。
- 代码格式正确,但尚未通过标的有效性验证。
- 原始记录在最终查询清单中没有对应结果。
这样才能区分"按规则排除"和"意外丢失"。
五、查询完成后,为什么还要检查结果覆盖率?
即使生成的查询清单没有问题,行情请求也可能因为网络错误、权限问题、数据缺失或其他原因而无法完成。
因此,不能将查询清单中的标的数量直接当作成功获取数据的标的数量。
建议至少区分以下三个集合:
input_symbols:原始输入中涉及的标的。query_symbols:通过本地规则并进入查询任务的标的。success_symbols:经过结果验证后,确实获得预期数据的标的。
可以用集合差异定位部分问题:
python
input_symbols = {
"600519.SH",
"000001.SZ",
"AAPL.US",
}
query_symbols = {
"600519.SH",
"AAPL.US",
}
success_symbols = {
"600519.SH",
}
not_scheduled = (
input_symbols - query_symbols
)
not_successful = (
query_symbols - success_symbols
)
print("未进入查询任务:", not_scheduled)
print("查询后未成功:", not_successful)
这里的集合都是示意数据,不代表实际 API 调用结果。
在正式程序中,集合比较适合检查唯一标的,但如果需要追踪重复记录、不同分组或每次请求的状态,就应该使用包含任务 ID 的结构化记录,而不是只依赖集合。
另外,成功获取数据也不一定意味着数据完整。
例如,某只股票成功返回了日线记录,但返回的时间区间不符合预期。程序如果只判断请求是否报错,就可能将不完整的数据错误地标记为成功。
因此,应根据具体查询任务定义验证条件,例如:
- 是否返回了预期的数据结构。
- 返回的标的是否与请求目标一致。
- 时间范围是否符合任务要求。
- 必要字段是否存在。
- 数据是否存在明显的重复记录。
- 预期周期内是否存在需要进一步调查的缺口。
并非每种缺口都意味着错误。停牌、上市时间和交易日历都会影响数据的合理性,不能简单地把所有缺失日期都视为 API 故障。
六、使用行情数据服务时,如何划清输入问题与接口问题的边界?
在接入行情 API 时,建议将故障分为三个层次。
第一层:本地数据问题。
例如,代码为空、格式不符合要求、标的重复,或者市场映射缺失。
这类问题通常可以在发送请求之前发现。
第二层:请求与权限问题。
例如,认证信息错误、权限不足或请求受到限流。
QuantDash 的公开资料列出了 REST API 的 401、403 和 429 等 HTTP 错误状态。这些状态应结合实际响应和官方文档解释,而不能简单地统一归结为股票代码错误。
第三层:返回数据问题。
例如,请求已完成,但返回数据不满足当前任务的时间范围、字段或完整性要求。
这时需要检查返回结果,而不能仅依据 HTTP 请求是否成功作出判断。
QuantDash 提供金融行情数据 API 和 Python SDK,可用于相关行情数据接入工作。其具体接口的请求参数、返回结构和错误处理方式,应以对应的官方技术文档为准。
需要特别强调的是,行情数据接口可以帮助完成数据获取,但无法自动替代应用程序的输入审计、业务规则检查和数据质量验证。
七、建立一份可以长期使用的输入审计报告
如果自选列表会定期变化,建议每次转换都生成一份审计结果。
例如:
python
summary = {
"input_rows": len(audit),
"empty_rows": int(
(audit["status"] == "empty").sum()
),
"invalid_format_rows": int(
(
audit["status"]
== "invalid_format"
).sum()
),
"candidate_rows": int(
(
audit["status"]
== "valid_candidate"
).sum()
),
}
print(summary)
这个统计只反映示例代码定义的状态,不代表已经完成标的有效性验证。
如果需要更完整的审计,还可以记录:
- 原始记录数。
- 标准化后的记录数。
- 去重后的唯一标的数。
- 被排除记录及原因。
- 尚未完成有效性验证的标的。
- 实际进入查询任务的标的数。
- 查询成功与失败的任务数。
- 返回数据未通过质量检查的任务数。
这些指标可以帮助开发者判断问题究竟发生在输入转换、请求执行还是结果验证阶段。
最终,自选股票列表接入行情 API 时,最重要的不是尽可能多地删除异常记录,而是建立一套可解释的处理规则。每个标的为什么被查询、为什么没有被查询,以及查询后为什么没有获得预期数据,都应该有明确的答案。
只有把这些问题分开处理,才能避免一个看似简单的股票列表转换步骤,成为后续回测、监控和策略运行中难以追踪的数据质量隐患。
QuantDash 官方文档
- QuantDash 技术文档 --- 查看 Python SDK、REST API 和数据接口说明。