滚动收益率计算出现异常时,一个容易被忽略的原因是:可用数据点不足以支撑指定的计算窗口,但代码没有明确区分数据不足、窗口未填满和计算结果缺失这几种情况。
例如,研究人员希望计算过去 20 个交易日的累计收益率。如果直接使用 rolling(20),却没有确认输入数据究竟包含多少个有效价格、多少个有效收益率,以及这些数据是否连续,就可能得到大量 NaN,甚至误把部分窗口的结果当成完整的 20 日收益率。
解决这个问题,需要从收益率的数学定义出发,明确窗口的含义,再通过 Pandas 对数据长度和计算结果进行检查。
一、首先明确:20 日收益率需要多少个价格数据?
假设某只股票的收盘价为:
| 交易日 | 收盘价 |
|---|---|
| D1 | 100 |
| D2 | 102 |
| D3 | 101 |
| D4 | 105 |
| D5 | 110 |
单日简单收益率定义为:
r_t=\\frac{P_t}{P_{t-1}}-1
其中,(P_t) 是当前交易日的价格,(P_{t-1}) 是前一个交易日的价格。
如果需要计算过去 (N) 个交易日的累计收益率,则可以使用:
R_{t,N}=\\frac{P_t}{P_{t-N}}-1
这里有一个关键细节:
计算 N 日收益率,需要 N+1 个有效价格端点,而不是 N 个价格。
例如:
- 5 日收益率需要 6 个价格。
- 20 日收益率需要 21 个价格。
- 60 日收益率需要 61 个价格。
如果研究对象是过去 20 个单日收益率的算术平均值,则需要 20 个有效的单日收益率。由于第一个价格无法计算单日收益率,这通常意味着至少需要 21 个价格观测值。
这两种计算的含义不同:
- 累计收益率衡量区间起点到终点的整体价格变化。
- 单日收益率的滚动均值衡量窗口内每日收益率的平均水平。
在编写代码之前,应先明确研究指标属于哪一种。
二、用 QuantDash 获取行情,再检查数据是否满足窗口要求
如果滚动收益率用于股票量化研究,建议直接从行情数据开始建立检查流程,而不是仅用人工构造的价格序列验证整个程序。
QuantDash 官方 Python 示例提供了以下历史 K 线调用方式:
python
from quantdash import QuantDash
qd = QuantDash()
prices = qd.klines.get(
"600519.SH",
period="1d",
count=100,
adjust="forward",
to_dataframe=True,
)
上述示例使用前复权日 K 数据,并请求 100 条记录。SDK 的公开示例说明了 QuantDash 客户端、klines.get() 方法及相关参数的使用方式。(GitHub)
运行前,需要安装 quantdash 和 pandas,并在环境变量 QUANTDASH_API_KEY 中配置有效的 API Key。
不过,请求 100 条记录不代表一定能得到 100 条有效价格数据。返回数据仍可能受到标的代码、数据权限、实际可用历史行情以及查询条件的影响。
因此,获取数据后应先验证返回类型和字段。
python
import pandas as pd
if not isinstance(prices, pd.DataFrame):
raise TypeError("行情结果不是 pandas.DataFrame")
if prices.empty:
raise ValueError("行情数据为空")
print("返回行数:", len(prices))
print("字段列表:", prices.columns.tolist())
print(prices.head())
这里有意不直接假设收盘价字段的具体名称。应先根据当前官方文档或实际返回结果确认字段,再将其用于计算。
假设已经确认收盘价字段为 close,且数据中的每一行对应一个交易日,可以继续执行后面的检查。
三、最直接的检查:有效价格数量是否达到最低要求?
对于 N 日累计收益率,至少需要 N+1 个有效价格。
python
import numpy as np
import pandas as pd
window = 20
# 前提:close 已经从实际行情数据中正确提取
close = pd.to_numeric(
prices["close"],
errors="coerce",
)
# 将正无穷、负无穷统一视为无效价格
close = close.replace([np.inf, -np.inf], np.nan)
# 股票价格应为正数
close = close.where(close > 0)
valid_price_count = int(close.notna().sum())
required_prices = window + 1
if valid_price_count < required_prices:
raise ValueError(
f"无法可靠计算 {window} 日收益率:"
f"当前只有 {valid_price_count} 个有效价格,"
f"至少需要 {required_prices} 个有效价格。"
)
这段代码能够检查整个数据集的有效价格数量是否达到最低要求。
但它仍然不能证明所有滚动窗口都完整。
例如,数据中虽然存在 50 个有效价格,却可能有一段连续的缺失记录。此时,全局有效价格数量足够,不代表缺失位置附近的窗口也满足计算条件。
因此,还需要对每一个窗口进行检查。
四、逐窗口识别数据不足
方法一:直接计算 N 日累计收益率
如果需要的是 N 日累计收益率,可以直接比较当前价格与 N 个观测位置之前的价格。
python
window = 20
returns_n = close.div(close.shift(window)).sub(1)
# 标记每个计算位置是否具备 N+1 个连续有效价格
complete_window = (
close.notna()
.rolling(window=window + 1, min_periods=window + 1)
.sum()
.eq(window + 1)
)
# 只有完整窗口才保留结果
returns_n = returns_n.where(complete_window)
print("有效收益率数量:", returns_n.notna().sum())
print(returns_n.tail())
这里有两个关键点。
第一,shift(window) 将价格与前 N 个观测位置之前的价格对齐。
第二,rolling(window + 1) 检查的是包含当前价格在内的 N+1 个观测位置。
如果其中任何一个位置的价格缺失,该窗口就不会被视为完整窗口。
需要注意,这里的"连续"指 DataFrame 中连续的观测位置。要让它同时代表连续的交易日,还需要确保数据已经按交易日期排序,并且没有因数据采集不完整而遗漏交易日。
方法二:先计算单日收益率,再检查滚动窗口
如果研究任务是计算过去 N 个单日收益率的平均值,可以采用另一套逻辑。
python
window = 20
daily_returns = close.pct_change(fill_method=None)
rolling_mean = daily_returns.rolling(
window=window,
min_periods=window,
).mean()
print(rolling_mean.tail())
这里将 fill_method=None 显式写出,是为了避免在计算单日收益率之前自动填充缺失价格。
当某个窗口内存在缺失收益率时,严格的 min_periods=window 会使该窗口的均值保持缺失,而不是使用较少的数据计算一个看似完整的结果。
两种实现方式不能互相替代:
| 研究目标 | 推荐计算方式 | 最低有效输入要求 |
|---|---|---|
| N 日累计收益率 | close / close.shift(N) - 1 |
N+1 个有效价格 |
| N 日单日收益率均值 | pct_change() 后使用 rolling(N) |
N 个有效单日收益率 |
| N 日收益率波动率 | 单日收益率后使用 rolling(N).std() |
N 个有效单日收益率 |
最后一项还涉及标准差的定义。Pandas 默认使用样本标准差,即 ddof=1。如果研究方案要求总体标准差,应明确设置 ddof=0。
五、为什么 min_periods 不能解决所有问题?
min_periods 用于指定一个滚动窗口至少需要多少个非缺失观测值,才能产生计算结果。
例如:
python
rolling_mean = daily_returns.rolling(
window=20,
min_periods=10,
).mean()
这意味着只要窗口中有 10 个有效收益率,就可能生成一个均值。
从 Pandas 的计算规则来看,这没有问题。但从研究口径来看,它可能不符合要求。
假设策略规定必须使用完整的 20 个交易日收益率,那么 10 个有效观测值算出的均值就不能被当成标准的 20 日均值。
因此,应区分两个概念:
- 计算最低要求:Pandas 允许多少个有效值参与计算。
- 研究完整性要求:策略规定必须具备多少个有效值。
两者只有在研究定义允许的情况下才能一致。
如果研究要求固定长度窗口,通常应明确设置 min_periods=window,并对不完整窗口进行标记或过滤。
六、把窗口完整性检查加入量化研究流程
对于单只股票,前面的代码已经可以完成基础检查。对于多股票研究,更重要的是把检查规则变成可重复使用的数据质量标准。
至少应关注以下三个维度:
- 数量检查:每个标的是否拥有足够的历史价格。
- 窗口检查:计算结果是否来自满足要求的有效观测窗口。
- 时间检查:数据是否按交易日期正确排序,是否存在重复日期或意外缺失的交易日。
其中,第三项容易被忽略。
如果数据源漏掉了某个交易日,shift(20) 计算的是前 20 行记录之前的价格,而不是保证严格意义上的前 20 个交易日之前的价格。
因此,数据质量检查不应仅依赖行数。
对于多标的研究,应先按标的代码分组,再分别计算滚动指标,避免将一只股票的历史价格与另一只股票的数据放进同一个滚动窗口。
结语
检查滚动收益率是否存在窗口长度不足的问题,不能只看 NaN 的数量,也不能只检查 DataFrame 总行数。
更可靠的做法是先明确收益率的数学定义,再确认所需的价格或收益率数量,最后逐窗口验证有效观测数与时间连续性。
对于 N 日累计收益率,记住最重要的一点:需要 N+1 个有效价格端点;对于由单日收益率构成的 N 日滚动统计,则需要根据指标定义检查 N 个有效收益率。
这一区分能够帮助研究人员减少指标计算口径错误,避免在回测中将数据不足产生的结果误当成有效信号。
QuantDash 官方文档
- QuantDash 技术文档 --- 查看 Python SDK、REST API 和数据接口说明。