如何从多只股票的历史行情构建收益率矩阵?Python 与 QuantDash 实战教程

在量化研究中,多只股票的历史价格通常只是原始输入。无论是计算投资组合波动率、分析资产相关性,还是进行均值---方差优化,研究者往往需要先把这些价格转换成一个统一的数据结构:收益率矩阵(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 官方文档

相关推荐
专注仿真1 小时前
多智能体强化学习实战:从 Dec-POMDP 到 QMIX/MADDPG/MAPPO 的可运行 PyTorch 实现
人工智能·pytorch·python
不是万能的豆学长5 小时前
filetidy 文件整理助手:一键归类 + 规范重命名(Python 零依赖单文件工具)
大数据·开发语言·python·技术美术
gCode Teacher 格码致知6 小时前
Pandas教学-18:number与numeric的区别
python·pandas
梦想不只是梦与想9 小时前
Python 中subprocess 模块:执行外部命令
python·subprocess·调用外部命令
懂点Tech9 小时前
ctfshow MISC入门图片篇(信息附加):misc41解题思路
开发语言·python
catchadmin10 小时前
PHP 8.7 协程新提案 让跨协程代码共享请求身份
开发语言·php
念何架构之路10 小时前
go-grpc设计思想
开发语言·后端·golang
酷炫码神11 小时前
Day 10|Pinia 状态管理:把散落在各组件的数据,收进一个全局仓库
android·开发语言·javascript
Elastic 中国社区官方博客12 小时前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es