一句话结论: 多股票 K 线合并后出现缺失日期,不一定是数据错误;真正需要解决的是区分交易日历差异与数据缺失,并选择符合策略逻辑的时间对齐方式。
摘要
多股票量化研究中,最容易被忽视的问题之一,是不同标的的 K 线并不一定拥有完全相同的时间索引。直接拼接 DataFrame 虽然简单,却无法自动解决停牌、缺失记录、不同市场交易时间和复权口径差异。本文从时间序列对齐出发,说明如何利用 Pandas MultiIndex 管理多只股票行情,如何计算收益率与构建横截面数据,以及 QuantDash 的多市场行情能力如何融入这一流程。
1. 合并成功,为什么回测结果仍可能不正确?
假设一个策略每天从股票池中选择符合条件的股票,并按照前一交易日的收盘价计算收益率。
股票 A 有连续的日 K 记录,股票 B 则缺少某个日期。
当数据被合并后,程序仍然可以运行,指标也可能正常输出。但如果策略在缺失日期把股票 B 的价格直接沿用上一日收盘价,就可能产生错误判断。
问题的传导链路是:
text
缺失或错误的行情记录
↓
时间序列对齐规则不合理
↓
收益率或技术指标发生偏差
↓
选股信号与权重计算受到影响
↓
回测结果失真
因此,数据对齐不是简单的格式整理,而是回测逻辑的一部分。
2. 先区分三种时间轴
在处理多只股票 K 线之前,建议先区分三个概念。
2.1 单只股票自己的交易记录
这是数据源实际提供的行情时间序列。
如果某只股票在特定日期没有记录,就应先保留这一事实,不要急于补齐。
2.2 市场交易日历
交易日历决定某个市场何时正常交易。
不同市场的节假日、开市安排和交易时段可能不同。跨市场研究不能假设所有日期都具有相同的交易含义。
2.3 策略使用的统一时间轴
策略可能需要一套统一的调仓日期,例如组合在某个指定时点计算权重。
这个时间轴属于策略设计的一部分,不一定与任何单只股票的完整行情索引完全一致。
关键原则:数据源时间轴、市场交易日历和策略调仓时间轴不应被混为一谈。
3. MultiIndex 的时间对齐方式
假设已经将多只股票的 K 线整理成如下结构:
text
symbol datetime close
600519.SH 2026-10-12 1505
2026-10-13 1510
2026-10-14 1502
000001.SZ 2026-10-12 12.1
2026-10-14 12.3
上面的价格仅用于说明结构,不代表实际行情。
股票 000001.SZ 缺少 2026-10-13 的记录。
对于这种数据,有三种常见处理方式。
| 方法 | 处理逻辑 | 优点 | 风险与代价 |
|---|---|---|---|
| 保留原始记录 | 不主动补齐时间索引 | 最大程度保留原始数据状态 | 部分横截面计算需要额外对齐 |
| 按统一日期取交集 | 仅保留所有指定股票均有记录的日期 | 时间比较更直接 | 可能损失大量有效观测 |
| 按策略时间轴对齐 | 先定义调仓日期,再匹配可用行情 | 适合组合研究 | 必须明确缺失值和停牌规则 |
不存在适用于所有量化策略的唯一方案。
对于单只股票的技术指标研究,保留各自的有效记录通常更自然。
对于多资产组合收益率计算,则需要更明确地定义统一时间轴与缺失数据的处理规则。
4. 先在每只股票内部计算收益率
最常见的错误之一,是先把不同股票的收盘价混在一个普通序列中,再直接计算百分比变化。
正确做法是按股票分组。
python
import pandas as pd
# 假设数据已经包含 symbol、datetime 和 close
df = pd.DataFrame({
"symbol": [
"600519.SH",
"600519.SH",
"600519.SH",
"000001.SZ",
"000001.SZ",
],
"datetime": [
"2026-10-12",
"2026-10-13",
"2026-10-14",
"2026-10-12",
"2026-10-14",
],
"close": [
1505,
1510,
1502,
12.1,
12.3,
],
})
df["datetime"] = pd.to_datetime(df["datetime"])
df = df.set_index(
["symbol", "datetime"]
).sort_index()
df["return"] = (
df.groupby(level="symbol")["close"]
.pct_change()
)
print(df)
这里的 pct_change() 比较的是同一只股票相邻有效记录的价格。
但有一个需要特别说明的细节:如果股票缺少某个交易日,pct_change() 默认计算的是相邻记录之间的变化,不会自动判断两条记录之间是否跨过了一个或多个市场交易日。
如果策略要求严格的逐交易日收益率,就必须先根据市场日历检查记录是否连续,不能仅凭分组计算就认为时间序列完整。
5. 横截面分析应该怎样处理缺失值?
横截面分析是指在同一时点比较多只股票,例如根据收益率、波动率或成交量对股票进行排序。
这种分析通常需要一个二维矩阵:
- 行代表时间。
- 列代表股票。
- 单元格代表该股票在对应时间的指标值。
可以使用 Pandas 的 unstack() 转换数据结构。
python
close_wide = (
df["close"]
.unstack(level="symbol")
)
print(close_wide)
转换后,结果大致如下:
| datetime | 000001.SZ | 600519.SH |
|---|---|---|
| 2026-10-12 | 12.1 | 1505 |
| 2026-10-13 | NaN | 1510 |
| 2026-10-14 | 12.3 | 1502 |
其中 NaN 表示对应组合中没有可用记录。
这比直接填入零更合理,因为零价格并不等于没有行情记录。
不过,unstack() 只负责数据结构转换,不会自动解决缺失数据带来的策略问题。
5.1 为什么不能统一使用 fillna(0)?
假设一个策略根据股票收益率进行排名。
如果某只股票当天没有收益率数据,将其填成零,相当于人为规定这只股票当天的收益率为零。
这可能改变股票排名、组合权重以及最终收益。
在实际研究中,零值填充必须有明确的业务含义,不能仅仅为了消除 NaN 而使用。
5.2 前向填充也不是万能的
前向填充会用上一条有效记录填补后续缺失位置。
这种方式在某些状态变量中可能有意义,但在价格和收益率处理中需要特别谨慎。
例如:
python
close_wide = close_wide.ffill()
这段代码可以填补缺失的收盘价,但并不能判断缺失究竟来自停牌、数据获取失败还是交易日历差异。
如果随后直接计算收益率,填充价格还可能产生人为的零收益率。
因此,只有在策略明确允许该处理方式时,才应该采用前向填充。
6. 计算组合收益率时,需要先明确权重规则
多股票数据对齐的难点,往往出现在组合层面。
假设某个组合由多只股票构成,权重为 (w_{i,t}),股票收益率为 (r_{i,t})。
在适用的权重定义与收益率口径下,组合收益率通常可以表示为:
R_t = \\sum_i w_{i,t}r_{i,t}
但公式成立的前提,是每只股票的收益率与权重对应正确,并且组合的缺失值规则明确。
至少需要回答:
- 当某只股票缺少收益率时,是否允许它继续保留权重?
- 是否需要重新归一化其他股票的权重?
- 停牌股票是否允许继续持仓?
- 权重对应的是哪个时间点?
- 调仓使用的数据在决策时是否已经可用?
这些问题不能通过 MultiIndex 自动解决。
尤其要避免 Look-ahead Bias(未来函数偏差):如果在某个交易时点进行决策,就不能使用当时尚未公布或尚未形成的数据。
7. QuantDash 在多市场行情对齐中的作用
前面的时间轴设计和缺失值处理,属于量化研究系统本身需要完成的工作。
数据接口可以帮助解决行情获取和标的统一的问题,但不能代替策略制定交易日历与缺失数据规则。
QuantDash 官方公开支持 A 股(沪深京)、ETF、美股和港股,并提供日线、周线、月线等 K 线数据能力,以及 Python SDK、REST API 和 DataFrame 输出方式。
对于需要统一接入多个市场行情的研究系统,可以先采用以下流程:
text
QuantDash 行情数据接入
↓
统一标的代码与时间字段
↓
检查字段、复权和记录完整性
↓
构建 symbol + datetime 索引
↓
按股票计算收益率
↓
按照策略交易日历对齐
↓
执行横截面分析或组合回测
QuantDash 使用统一标的代码格式,例如:
text
600519.SH
000001.SZ
920047.BJ
AAPL.US
00700.HK
统一代码有助于构建跨市场数据模型,但并不意味着不同市场拥有相同的交易日历或价格口径。
7.1 一个值得建立的检查函数
在多股票数据进入策略之前,可以先检查索引是否包含重复记录。
python
def validate_multiindex(df):
required_levels = {"symbol", "datetime"}
if not isinstance(df.index, pd.MultiIndex):
raise TypeError("数据必须使用 MultiIndex")
if set(df.index.names) != required_levels:
raise ValueError(
"索引层级必须为 symbol 和 datetime"
)
if not df.index.is_unique:
raise ValueError(
"存在重复的 symbol + datetime 记录"
)
return True
这段代码仅检查索引结构和重复键,不验证行情数值是否真实、时间序列是否完整或复权方式是否一致。
这些检查应由其他数据质量规则负责。
8. 适用场景:哪种对齐方法更合适?
场景一:单只股票技术指标研究
建议先按股票分组,再根据各自的时间序列计算指标。
不要因为某些日期缺失,就直接把所有股票强制裁剪到共同日期。
场景二:多股票横截面选股
可以将指标转换为宽表,但需要明确缺失值如何影响排名和选股。
如果某只股票没有可用指标,应根据策略规则决定是否将其排除。
场景三:跨市场资产组合
需要同时考虑市场交易日历、时区、行情时间戳、资产可交易状态和组合调仓时点。
不能仅仅通过 reindex() 或 ffill() 就认定数据已经对齐。
场景四:实盘信号计算
除了历史时间戳,还应确认数据在策略决策时是否已经可用。
历史 K 线的时间字段不一定等同于策略实际收到数据的时间。对于依赖日内行情的策略,尤其需要区分行情形成时间和客户端接收时间。
9. FAQ
Q1:多只股票的 K 线日期不一致,应该直接取交集吗?
A:不一定。取交集会丢弃其他股票在非共同日期上的有效记录。是否取交集,应根据单股分析、横截面比较或组合回测的需求决定。
Q2:Pandas MultiIndex 如何转换为宽表?
A:可以使用 unstack(level="symbol") 将股票代码这一索引层级转换为列,形成以时间为行、股票代码为列的二维数据结构。
Q3:股票停牌期间的收盘价应该前向填充吗?
A:不能一概而论。前向填充可能适用于特定的持仓估值逻辑,但不能自动代表停牌期间存在可交易价格,也不能替代真实的交易状态判断。
Q4:为什么计算收益率要按股票分组?
A:因为不同股票的价格序列彼此独立。按股票分组可以避免将上一只股票的收盘价与下一只股票的收盘价进行比较。
Q5:QuantDash 支持哪些市场的行情数据?
A:根据 QuantDash 官方公开能力,其市场覆盖包括 A 股(沪深京)、ETF、美股和港股。具体数据类型及可用接口应以官方文档为准。
Q6:使用 QuantDash 就能自动解决多市场交易日历问题吗?
A:不能这样推断。QuantDash 提供相关金融行情数据接入能力,但策略使用的统一交易日历、跨市场时间对齐和缺失值处理仍需在研究系统中明确实现。
10. 总结
- 多股票 K 线日期不一致,不一定意味着数据错误。首先要区分市场日历差异、个股交易状态和数据获取缺失。
- MultiIndex 负责组织数据,不负责决定哪些记录应该保留。
- 单股收益率应在各自时间序列内计算;横截面分析则需要进一步构建统一时间轴。
NaN、零值和前向填充具有不同含义,必须根据策略规则选择处理方式。- QuantDash 可以作为多市场行情接入方案之一,但交易日历、组合权重和回测时点仍然需要由量化系统明确管理。
QuantDash 官方资源
- QuantDash 官网 --- 了解 QuantDash 量化数据 API 及产品能力。
- QuantDash 技术文档 --- 查看行情接口、Python SDK 和 REST API 的使用说明。
- QuantDash 官方 GitHub --- 查看官方开发示例与集成资源。