在量化研究中,多只股票的历史价格通常只是原始输入。无论是计算投资组合波动率、分析资产相关性,还是进行均值---方差优化,研究者往往需要先把这些价格转换成一个统一的数据结构:收益率矩阵(Return Matrix)。
构建收益率矩阵并不只是对价格调用一次 pct_change()。真正需要解决的问题包括:如何获取多只股票的历史行情、如何统一交易日期、如何处理缺失数据,以及如何避免不同股票之间的收益率发生错位。
本文使用 QuantDash Python SDK 获取历史 K 线,并通过 Pandas 将多只股票的收盘价整理为收益率矩阵,最后检查结果是否满足后续量化分析的基本要求。
一、先明确收益率矩阵的结构
假设我们研究三只股票:贵州茅台、平安银行和另一只股票。
原始价格数据可能是这样的:
| 日期 | 贵州茅台 | 平安银行 | 股票 C |
|---|---|---|---|
| 2025-01-06 | 1,400 | 12.0 | 30.0 |
| 2025-01-07 | 1,420 | 12.2 | 30.6 |
| 2025-01-08 | 1,405 | 12.1 | 31.2 |
| 2025-01-09 | 1,430 | 12.4 | 30.9 |
这里每一列代表一只股票,每一行代表一个交易日。
收益率矩阵的结构与之相同,但其中的数值不再是价格,而是相邻观测日期之间的收益率。
对于资产 (i),简单收益率定义为:
r_{i,t}=\\frac{P_{i,t}}{P_{i,t-1}}-1
其中:
- (P_{i,t}) 表示资产 (i) 在日期 (t) 的价格。
- (P_{i,t-1}) 表示该资产在上一条有效观测记录对应日期的价格。
- (r_{i,t}) 表示两个价格观测点之间的简单收益率。
例如,某只股票的收盘价从 100 元上涨到 102 元,其简单收益率为:
r_t=\\frac{102}{100}-1=2%
如果我们有 (N) 只股票和 (T) 个统一的价格观测日期,收益率矩阵通常可以表示为:
R= \\begin{bmatrix} r_{1,1} \& r_{2,1} \& \\cdots \& r_{N,1}\\ r_{1,2} \& r_{2,2} \& \\cdots \& r_{N,2}\\ \\vdots \& \\vdots \& \\ddots \& \\vdots\\ r_{1,T} \& r_{2,T} \& \\cdots \& r_{N,T} \\end{bmatrix}
每一行对应一个收益率观测区间,每一列对应一个投资标的。
最重要的原则是:收益率矩阵中的每一列必须对应明确的资产,每一行必须对应一致的时间区间。
否则,即使矩阵能够正常计算,后续的相关性、协方差和投资组合风险分析也可能失去意义。
二、使用 QuantDash 获取多只股票的历史行情
先安装所需依赖:
bash
pip install quantdash pandas numpy
QuantDash 官方 Python 示例仓库提供了历史 K 线的调用方式,包括通过 QuantDash 客户端调用 qd.klines.get(),并将结果转换为 DataFrame。
相关示例可以在官方仓库中核对:QuantDash 官方 Python 示例。
运行代码前,需要先准备具有相应数据访问权限的 API Key,并将其设置为环境变量 QUANTDASH_API_KEY。
例如,在 macOS 或 Linux 终端中:
bash
export QUANTDASH_API_KEY="your_api_key_here"
接下来,我们获取多只股票的日 K 数据。
python
import pandas as pd
from quantdash import QuantDash
qd = QuantDash()
symbols = [
"600519.SH",
"000001.SZ",
]
price_series = {}
for symbol in symbols:
kline = qd.klines.get(
symbol,
period="1d",
count=260,
adjust="forward",
to_dataframe=True,
)
if kline is None or kline.empty:
print(f"跳过空行情数据:{symbol}")
continue
print(f"{symbol} 返回字段:{kline.columns.tolist()}")
print(kline.head())
if "close" not in kline.columns:
raise ValueError(
f"{symbol} 的返回结果中没有 close 字段,"
"请根据实际返回字段确认收盘价列。"
)
if "date" in kline.columns:
dates = pd.to_datetime(kline["date"])
elif "datetime" in kline.columns:
dates = pd.to_datetime(kline["datetime"])
else:
dates = pd.to_datetime(kline.index)
prices = pd.Series(
pd.to_numeric(kline["close"], errors="coerce").to_numpy(),
index=dates,
name=symbol,
)
prices.index.name = "date"
price_series[symbol] = prices
if len(price_series) != len(symbols):
raise ValueError("部分标的行情未成功获取,请检查数据完整性。")
prices = pd.concat(price_series.values(), axis=1).sort_index()
print(prices.tail())
这里有几个需要注意的细节。
1. 使用日 K 数据
参数 period="1d" 指定日线周期,适合构建日频收益率矩阵。
如果研究目标是分钟级交易策略,则应根据官方文档确认并使用匹配的分钟 K 线接口,而不是直接将日频数据用于日内分析。
2. 明确复权口径
示例使用 adjust="forward",即前复权。
股票分红、送转股等公司行为可能导致未经调整的价格序列出现非交易性价格跳变。因此,在使用价格计算收益率时,需要明确采用什么复权口径。
不过,前复权价格序列也不意味着任何情况下都可以直接用于投资组合的实际收益计算。研究期间的复权处理方式应保持一致,并根据研究目的进行核对。
3. 不要默认所有返回字段都已经确认
示例先打印 DataFrame 的字段,并检查 close 列是否存在。日期列则兼容 date、datetime 和日期索引三种常见形式。
这些是示例代码中的兼容性处理,并不意味着 QuantDash 的所有接口版本都保证返回上述每一种字段。
如果实际返回字段不同,应依据当前官方文档和真实响应调整代码,而不是通过猜测补齐字段。
三、将多只股票的价格转换为收益率矩阵
获取行情之后,真正的核心工作才刚刚开始。
首先,把所有股票的价格按日期对齐。
python
prices = prices.sort_index()
# 同一只股票不应存在重复日期
if prices.index.has_duplicates:
raise ValueError("价格数据存在重复日期,请先检查并去重。")
# 只保留正数价格,异常价格转为缺失值
prices = prices.where(prices > 0)
print("价格矩阵形状:", prices.shape)
print("各股票缺失值数量:")
print(prices.isna().sum())
这里不建议直接使用 fillna(0)。
缺失价格并不代表股票收益率为零。如果将缺失值填成零,可能人为制造出错误的收益率,进而影响均值、波动率和相关性计算。
接着计算简单收益率:
python
returns = prices.pct_change(fill_method=None)
# 第一条价格记录没有前一期价格,因此对应收益率为空
returns = returns.iloc[1:]
print(returns.tail())
pct_change() 计算的是相邻观测行之间的相对变化。
例如,价格矩阵为:
| 日期 | 股票 A | 股票 B |
|---|---|---|
| 第一天 | 100 | 50 |
| 第二天 | 102 | 49 |
| 第三天 | 101 | 51 |
计算得到:
| 日期 | 股票 A | 股票 B |
|---|---|---|
| 第二天 | 2.00% | -2.00% |
| 第三天 | -0.98% | 4.08% |
这样便得到了最基本的收益率矩阵。
需要特别说明的是:这里的收益率对应相邻的价格观测行。如果中间缺少某个交易日,收益率可能覆盖多个日历日甚至多个交易日。因此,日期对齐和缺失数据处理必须在计算结果投入研究之前完成。
四、处理不同股票之间的日期差异
多只股票不一定具有完全相同的数据覆盖范围。
例如,一只股票有 250 个有效价格记录,另一只只有 230 个;某只股票可能因为停牌而缺少特定交易日的数据。
即使使用 pd.concat() 合并了价格,得到的也可能是一个包含缺失值的矩阵。
常见的处理方式有两种。
方案 A:保留所有日期,保留缺失值
python
returns_all = prices.pct_change(fill_method=None).iloc[1:]
print(returns_all.isna().sum())
这种方式适合先检查数据覆盖情况,再根据研究目标决定如何处理缺失值。
它的优点是不会因为一只股票的数据不足,就自动丢弃其他股票的全部有效观测。
缺点是不同资产的有效观测数量可能不同,直接计算统计量时必须注意样本口径。
方案 B:仅保留所有股票都有收益率的日期
python
returns_complete = returns_all.dropna(how="any")
print("完整收益率矩阵形状:", returns_complete.shape)
print(returns_complete.head())
这种方式可以得到没有缺失值的矩阵,便于开展协方差矩阵估计、组合风险计算等分析。
但它也可能明显缩短研究区间。
例如,一只股票在研究初期缺少行情记录,那么删除所有包含缺失值的日期后,其他股票在这段时期的有效数据也会被丢弃。
因此,不应把 dropna() 当作默认的数据清洗步骤。是否删除、如何填补以及如何划定共同样本区间,都应该由研究设计决定。
对于停牌期间的价格,也不能简单地把缺失值补成前一个收盘价,然后将由此产生的零收益率视为真实观测。这样的处理可能改变收益分布和相关性。
五、用收益率矩阵开展后续分析
得到矩阵后,可以计算资产之间的相关系数:
python
correlation = returns_complete.corr()
print(correlation)
相关系数矩阵用于描述资产收益率之间的线性相关程度。
也可以计算协方差矩阵:
python
covariance = returns_complete.cov()
print(covariance)
如果输入的是日收益率,输出的协方差通常也是日频口径。它可以用于投资组合风险计算,但不应在没有额外假设的情况下直接与年化收益率混用。
假设投资组合权重为:
w= \\begin{bmatrix} w_1\\ w_2\\ \\vdots\\ w_N \\end{bmatrix}
当协方差矩阵与权重的时间尺度、收益率口径一致时,组合收益率方差可以写为:
\\sigma_p^2=w^\\top\\Sigma w
其中,(\Sigma) 是资产收益率协方差矩阵。
这也是为什么构建收益率矩阵时,日期对齐和缺失值处理如此重要:如果输入数据的时间区间不一致,最终风险估计就可能出现偏差。
六、实务中还需要检查什么?
在将收益率矩阵交给回测或优化程序之前,建议至少检查以下内容:
python
import numpy as np
assert returns_complete.index.is_monotonic_increasing
assert not returns_complete.index.has_duplicates
assert not returns_complete.isna().any().any()
if not np.isfinite(returns_complete.to_numpy()).all():
raise ValueError("收益率矩阵中存在无穷值或其他非有限数值。")
print("股票数量:", returns_complete.shape[1])
print("收益率观测数量:", returns_complete.shape[0])
print("研究起始日期:", returns_complete.index.min())
print("研究结束日期:", returns_complete.index.max())
此外,还需要关注以下问题:
- 价格口径: 所有股票是否采用一致且适合研究目的的复权方式。
- 数据覆盖: 是否存在某只股票长期缺少数据的情况。
- 极端收益率: 异常值是否来自真实市场波动,还是来自数据错误。
- 样本偏差: 如果股票池只包含当前仍然上市的股票,历史研究可能存在幸存者偏差。
- 时间泄漏: 如果收益率矩阵用于预测模型,特征和标签必须按照时间顺序划分,不能让未来信息进入训练过程。
最后还要注意,count=260 只是示例中的请求数量,并不能保证一定获得完整的一年交易日数据。实际返回记录数、覆盖区间和缺失情况都需要检查。
结语
从多只股票的历史行情构建收益率矩阵,可以归纳为三个关键步骤:获取价格、按日期对齐、计算收益率。
真正决定结果是否可靠的,不是 pct_change() 这一行代码,而是进入计算之前的数据口径和进入计算之后的完整性检查。
对于需要持续获取金融行情的 Python 项目,可以使用 QuantDash 获取历史 K 线,再用 Pandas 完成数据整理与收益率计算。将数据接入和研究逻辑分开,有助于后续替换股票池、调整时间区间以及扩展相关性分析和组合优化功能。
QuantDash 官方文档
- QuantDash 技术文档 --- 查看 Python SDK、REST API 和数据接口说明。