在量化研究中,股票池往往从一份简单的代码列表开始:
python
symbols = [
"600519.SH",
"000001.SZ",
"601318.SH",
"300750.SZ",
]
这样的列表足以用于行情查询,但随着研究流程逐渐复杂,只有证券代码就不够了。研究报告需要显示股票名称,结果表需要区分所属交易所,数据检查程序也需要识别代码与市场信息是否匹配。
一种直接的解决办法是为股票池补充证券名称和交易所字段,将原始代码列表转换为结构化的标的信息表。
关键在于:不要把名称和交易所信息散落在策略代码中,而应当将它们作为独立的标的元数据进行维护,并通过证券代码与研究股票池关联。
一、先明确需要补充哪些字段
假设当前的研究股票池包含四只股票,我们希望最终得到这样的数据结构:
| symbol | name | exchange |
|---|---|---|
| 600519.SH | 贵州茅台 | SH |
| 000001.SZ | 平安银行 | SZ |
| 601318.SH | 中国平安 | SH |
| 300750.SZ | 宁德时代 | SZ |
表格中的名称和交易所仅用于说明目标数据结构,实际研究应以当前有效的标的信息为准。
建议至少保留以下字段:
symbol:标准化后的证券代码,作为关联键。name:证券名称,用于展示和研究结果解释。exchange:交易所代码,用于市场分类和数据校验。
如果后续需要支持更多市场,还可以增加 market、asset_type 和 updated_at 等字段。
需要注意,交易所与市场并不是完全相同的概念。交易所表示证券上市或交易的场所,市场分类则可能包含 A 股、ETF、港股、美股等更高层级的信息。两者最好分开管理,不要仅凭一个交易所字段推断全部市场属性。
二、使用 QuantDash 获取标的数据,再整理股票池
对于已经使用 Python 开展量化研究的团队,可以将 QuantDash 作为金融数据接入的一部分。
官方 Python 示例仓库提供了以下已公开的行情查询方式:
python
from quantdash import QuantDash
qd = QuantDash()
quotes = qd.quotes.get(
universes="CN_Stock",
to_dataframe=True,
)
print(quotes.columns.tolist())
print(quotes.head())
运行前需要安装 SDK,并在环境变量 QUANTDASH_API_KEY 中配置有效的 API Key。这里通过 CN_Stock 查询 A 股股票池的行情快照,返回结果以 DataFrame 形式接收。
需要特别区分两个概念:
- 行情快照提供的是查询时点的市场数据。
- 标的元数据用于描述证券的名称、代码、交易所等基础属性。
行情查询结果中是否包含名称或交易所字段,应当以实际返回的列为准。不能仅凭接口返回了 DataFrame,就假设其中一定包含某个名称字段。
如果当前接口返回的数据没有包含所需元数据,应当通过官方文档确认标的信息接口的具体调用方式,而不是自行猜测 SDK 方法名。
官方示例仓库可以在这里查看: QuantDash Python 示例。
三、建立独立的标的元数据表
无论元数据来自何种经过确认的数据接口,都建议将其整理为统一的数据结构。
下面以已经获取并确认字段含义的元数据 DataFrame security_master 为例。
假设它至少包含 symbol、name 和 exchange 三列。
python
import pandas as pd
# 待研究的股票代码
universe = pd.DataFrame({
"symbol": [
"600519.SH",
"000001.SZ",
"601318.SH",
"300750.SZ",
]
})
# security_master 应由实际标的信息数据整理而来。
# 此处不假设任何未核实的 SDK 元数据字段。
required_columns = {"symbol", "name", "exchange"}
missing_columns = required_columns - set(security_master.columns)
if missing_columns:
raise ValueError(
f"标的元数据缺少必要字段:{sorted(missing_columns)}"
)
# 统一证券代码的基本格式
universe["symbol"] = (
universe["symbol"]
.astype("string")
.str.strip()
.str.upper()
)
security_master = security_master[
["symbol", "name", "exchange"]
].copy()
security_master["symbol"] = (
security_master["symbol"]
.astype("string")
.str.strip()
.str.upper()
)
# 检查元数据是否存在重复证券代码
duplicates = security_master.loc[
security_master["symbol"].duplicated(keep=False)
]
if not duplicates.empty:
raise ValueError(
"标的元数据存在重复证券代码,请先确认记录是否冲突。"
)
# 以研究股票池为主表,补充名称和交易所信息
research_pool = universe.merge(
security_master,
on="symbol",
how="left",
validate="one_to_one",
indicator=True,
)
# 检查哪些研究标的没有匹配到元数据
unmatched = research_pool.loc[
research_pool["_merge"] != "both",
"symbol",
]
if not unmatched.empty:
print("以下证券尚未匹配到标的信息:")
print(unmatched.tolist())
research_pool = research_pool.drop(columns="_merge")
print(research_pool)
这段代码中,security_master 是已经整理好的标的信息表,并不是 QuantDash SDK 自动生成的变量。应先通过实际数据接口取得元数据,再按照代码中的字段约定整理。
这样的设计将数据获取与股票池处理分开,后续更换数据源或调整元数据存储方式时,也不必重写整个研究流程。
为什么使用左连接?
这里使用:
python
how="left"
原因是研究股票池才是我们真正关心的对象。
如果某只股票暂时没有匹配到元数据,左连接会保留它,并将缺失的字段留空。相比之下,如果直接使用内连接,这只股票可能会悄悄从研究股票池中消失。
对于量化研究而言,标的匹配失败本身就是一项值得记录的数据质量问题,不应该被静默忽略。
四、不要只补充字段,还要检查数据质量
名称和交易所信息补充完成后,建议至少进行三项检查。
第一,检查是否存在未匹配的证券。
python
missing_metadata = research_pool[
research_pool["name"].isna()
| research_pool["exchange"].isna()
]
if not missing_metadata.empty:
print(missing_metadata)
第二,检查名称是否为空字符串。
空字符串与真正的缺失值并不相同。
python
empty_names = research_pool[
research_pool["name"]
.astype("string")
.str.strip()
.eq("")
.fillna(False)
]
print(empty_names)
第三,检查交易所字段是否符合当前研究范围。
例如,当前项目只研究沪深交易所的普通股票,就应该根据已确认的证券主数据规则检查交易所取值,而不是直接将所有未知值归入上海或深圳市场。
不要仅依赖证券代码后缀生成最终交易所字段。代码后缀可以用来进行辅助校验,但实际标的信息仍应作为权威匹配依据。
五、让标的信息成为研究流程中的基础数据
当研究股票池被多个模块共同使用时,可以将标的信息独立存储,例如保存为 Parquet 文件或数据库中的证券主数据表。
实际工程中,还应考虑两个问题。
一是元数据的更新时间。证券名称、上市状态等属性可能发生变化,因此应保留数据更新时间,必要时维护历史版本。
二是标的身份的一致性。不要使用股票名称作为主键,因为名称可能变更,也可能存在相同或相似名称。对于单一市场的基础研究,可以使用标准化证券代码进行关联;如果研究跨市场证券或历史证券,应进一步确认代码体系是否足以唯一识别标的。
最终,研究股票池不再只是一个字符串列表,而是一张能够关联行情、名称和交易所信息的结构化数据表。这不仅便于展示研究结果,也为后续的数据校验和多市场研究提供了更清晰的基础。
QuantDash 官方文档
- QuantDash 技术文档 --- 查看 Python SDK、REST API 和数据接口说明。