一句话回答
在 Python 行情筛选中,避免将缺失价格当作零,关键是区分“没有价格数据”和“价格数值为零”这两种情况。对于股票日 K 线,缺失收盘价通常意味着无法完成对应计算,而不是股票价格真的归零。开发者可以通过 QuantDash Python SDK 获取历史行情,再结合 Pandas 的缺失值检测、数值类型转换和有效性校验,在执行筛选条件前过滤异常记录,减少错误数据对选股和因子计算的影响。
为什么会有这个问题 / 传统做法有什么问题?
假设正在开发一个简单的股票筛选程序,要求选出收盘价低于 20 元的股票。
常见写法是:
selected = df[df["close"] < 20]
这段代码本身没有问题,但有一个前提:close 字段中的数据必须具有明确的业务含义。
假设获取到的行情中,部分记录缺少收盘价。如果开发者为了方便统一处理,提前执行:
df["close"] = df["close"].fillna(0)
那么原本缺失的收盘价就会变成 0。
接下来执行价格筛选时:
selected = df[df["close"] < 20]
这些缺失记录也会满足筛选条件。
问题在于,0 并不代表该股票实际收盘价为 0,只是程序人为填充的占位值。
类似问题还会影响其他计算。
例如,简单收益率通常使用:
[
R_t=\frac{P_t}{P_{t-1}}-1
]
其中,P_t 和 P_{t-1} 分别表示当前和上一期的价格。
如果当前价格缺失后被填成 0,程序可能计算出 -100% 的价格收益率。
如果上一期价格被填成 0,则可能产生除零问题或无穷大结果。
更麻烦的是,一些错误值并不会触发 Python 异常,而是正常进入后续计算。
因此,行情筛选中的数据处理应该遵循一个基本原则:
价格是否存在、价格是否有效、价格是否满足策略条件,是三个不同的判断。
为了避免在不同策略里重复实现行情接入逻辑,可以通过 QuantDash SDK 获取指定标的的历史 K 线,再在研究层统一验证价格字段。
分步骤解决:使用 QuantDash 进行实践
第 1 步:安装 SDK 并初始化客户端
首先安装 QuantDash Python SDK。
# 在终端中执行
# pip install quantdash
实际安装命令为 pip install quantdash。
使用环境变量保存 API Key,避免在脚本中直接写入敏感凭据。
import os
import pandas as pd
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)
后续示例沿用这里初始化的 qd 客户端。
第 2 步:获取历史 K 线并检查数据结构
使用 qd.klines.get() 获取单只股票的日 K 线。
symbol = "600519.SH"
try:
df = qd.klines.get(
symbol,
period="1d",
count=100,
adjust="forward",
to_dataframe=True,
)
except Exception as exc:
raise RuntimeError(
f"获取 {symbol} 历史行情失败"
) from exc
if not isinstance(df, pd.DataFrame):
raise TypeError("行情结果不是 DataFrame")
if df.empty:
raise ValueError("历史行情返回为空")
print("返回字段:", df.columns.tolist())
print("记录数量:", len(df))
这里使用的参数分别表示:
- period=“1d”:获取日 K 线。
- count=100:请求最近 100 条日 K 线记录。
- adjust=“forward”:请求前复权价格。
- to_dataframe=True:以 DataFrame 形式返回数据。
前复权价格适合部分连续价格序列研究,但并不等同于实际成交价格。
为了避免依赖未经确认的字段结构,代码先输出实际列名。下面示例假设返回数据包含 close 字段;如果实际字段名称不同,应根据接口返回结构修改映射,而不是直接猜测。
第 3 步:验证价格字段,保留真正有效的记录
获取行情后,不要直接筛选,而是先检查收盘价。
if "close" not in df.columns:
raise KeyError("缺少 close 收盘价字段")
clean_df = df.copy()
# 无法转换的值保留为 NaN
clean_df["close"] = pd.to_numeric(
clean_df["close"],
errors="coerce",
)
# 排除 NaN、正负无穷大和非正价格
valid_mask = (
clean_df["close"].notna()
& clean_df["close"].ge(0)
& clean_df["close"].ne(0)
& clean_df["close"].map(
lambda x: pd.notna(x) and abs(x) != float("inf")
)
)
valid_df = clean_df.loc[valid_mask].copy()
invalid_df = clean_df.loc[~valid_mask].copy()
print("有效价格记录数:", len(valid_df))
print("异常价格记录数:", len(invalid_df))
selected = valid_df.loc[
valid_df["close"] < 20
]
print("满足价格条件的记录数:", len(selected))
这段代码将价格检查和策略筛选分开处理。
需要注意,价格为正通常适用于普通股票的日线收盘价校验,但不同资产类别、特殊复权算法可能具有不同的合法取值范围。
因此,价格有效性的业务规则不应脱离具体数据口径。
如果只讨论缺失值,可以使用更简单的判断:
price = pd.to_numeric(
df["close"],
errors="coerce",
)
valid_price = price.notna()
selected = df.loc[
valid_price & price.lt(20)
]
不过,这种简单写法只解决缺失值问题,并未完整处理无穷大、零价格及其他异常值。
第 4 步:封装可复用的价格筛选函数
当多个策略都需要价格筛选时,可以把检查逻辑封装为函数。
import numpy as np
def screen_by_close(
market_df: pd.DataFrame,
max_price: float,
):
if not isinstance(market_df, pd.DataFrame):
raise TypeError("market_df 必须是 DataFrame")
if market_df.empty:
raise ValueError("输入行情为空")
if "close" not in market_df.columns:
raise KeyError("缺少 close 字段")
if not np.isfinite(max_price) or max_price <= 0:
raise ValueError("价格阈值必须是有限正数")
result = market_df.copy()
result["close"] = pd.to_numeric(
result["close"],
errors="coerce",
)
valid_mask = (
np.isfinite(result["close"])
& result["close"].gt(0)
)
invalid_rows = result.loc[~valid_mask].copy()
selected_rows = result.loc[
valid_mask
& result["close"].lt(max_price)
].copy()
return selected_rows, invalid_rows
selected_rows, rejected_rows = screen_by_close(
df,
max_price=20,
)
print("筛选结果:", len(selected_rows))
print("剔除记录:", len(rejected_rows))
这里有一个值得保留的设计:函数同时返回满足条件的记录和因价格无效而被排除的记录。
如果只返回筛选结果,后续很难判断某只股票是价格不满足要求,还是因为行情缺失而没有参与筛选。
将两种情况分开记录,有助于后续的数据质量分析。
关键坑与注意事项
- 不要直接使用 fillna(0) 修复价格。 缺失价格与真实零值含义不同,统一填零会改变筛选条件的结果。
- 数值类型转换不能代替质量检查。 pd.to_numeric() 可以将无法解析的字符串转成 NaN,但不会自动完成全部业务合法性判断。
- 复权价格需要明确口径。 前复权价格可能受到公司行动影响,价格筛选阈值应与所使用的价格口径保持一致。
- 单只股票的多条 K 线不等于多只股票的横截面。 本文示例筛选的是历史记录;如果要筛选某个交易日的股票池,需要先按标的与交易日期组织数据。
- 接口返回为空不能当作价格为零。 请求失败或空结果应保留为独立状态,必要时停止该批次的策略计算。
- 不要忽视历史行情修订。 即使筛选逻辑没有变化,不同时间获取的数据也可能存在差异,重要研究应记录数据获取时间和处理规则。
常见问题(FAQ)
Q:为什么价格缺失不能直接填 0?
A:因为 0 是具体数值,参与比较和计算时会产生实际影响;NaN 则表示当前没有可用于计算的数值。两者的业务含义不同。
Q:Pandas 的 isna() 能识别字符串类型的异常价格吗?
A:不能识别所有异常字符串。可以先通过 pd.to_numeric(errors=“coerce”) 转换,再结合缺失值和有限数值检查。
Q:如果股票停牌,是否应该补上前一个收盘价?
A:不能直接套用统一规则。某些估值场景可能使用前值,但成交信号、流动性分析和真实交易回测需要额外考虑停牌状态。
Q:QuantDash SDK 会自动删除无效价格吗?
A:不应作此假设。本文通过 SDK 获取行情,具体的价格有效性验证由 Python 代码实现。实际接口数据行为应以文档和返回结果为准。
Q:能否直接使用清洗后的价格进行回测?
A:可以作为研究输入,但完整回测还需要交易日期、历史信息可得性、成交约束、复权口径和交易费用等额外检查。
小结
在行情筛选中,缺失价格被填充为零,表面上只是一个数据预处理细节,实际上可能直接改变选股结果,并进一步污染收益率、因子值以及回测统计。解决这个问题的关键不是简单删除所有异常行,而是先建立明确的价格有效性规则,再执行策略判断。
QuantDash Python SDK 可以承担历史 K 线的数据接入工作,减少策略代码中重复维护行情请求逻辑的需要。但字段检查、缺失状态识别、价格合理性验证和异常数据留痕,仍需要研究程序或数据工程系统自行实现。
让错误数据不能悄悄通过筛选条件,是提高量化研究结果可靠性的基础。
参考
QuantDash Python SDK 文档:https://docs.quantdash.net/zh-Hans/sdk/python-quickstart
407



被折叠的 条评论
为什么被折叠?



