【魔码量化工程实战进阶 #08】历史K线全量回放与对齐:交易日历缺口与复权前对齐
【魔码量化工程实战进阶 #08】历史 K 线全量回放与对齐:交易日历、缺口与复权前对齐
入门系列第 05 篇讲了"历史 K 线入门",拉一只看一只。本篇解决多资产回测的隐形炸弹:不同股票的交易日并不对齐——新股上市晚、老股停牌、退市股截断。直接按行号对齐做回测,信号会整体错位。我们用"交易日历为轴"把多只 K 线对齐成面板(panel),并讲清对齐前必须做的复权处理。
本文你将得到什么
- 为什么"按列表下标"对齐多只股票必错(新股/停牌造成的日期缺口)
- 以交易日历为轴 pivot 成面板的标准做法(pandas
pivot) - 对齐的两种策略:交集(inner) vs 前向填充(forward-fill)
- 一个反直觉前提:对齐前必须先复权(否则历史价本身是错的,见 #09)
一、痛点:日期不对齐,回测全错位
假设你同时回测 3 只股票,代码里这么写:
for i in range(len(dates)):
s1 = close1[i]; s2 = close2[i]; s3 = close3[i] # 按行号取"同一天"
但 close1、close2、close3 的"第 i 行"未必是同一个交易日:
- 股票 A 是 2020 年新股,前面 200 行是空的;
- 股票 B 中间停牌 10 天,那 10 天没数据;
- 股票 C 去年退市了,尾部没数据。
结果:你拿 A 的"第 100 行(上市首日)"和 B 的"第 100 行(已上市半年)"当同一天算相关系数——整个信号错位。
二、工程方案:交易日历为轴
正确做法:不要用行号,要用日期 t 当索引。把多只股票的收盘价 pivot 成一个 DataFrame,行是交易日、列是股票:
import pandas as pd
# 每只股票的历史 K 线(真实字段: t,o,h,l,c,v,a,pc,sf)
# 演示证书返回演示数据,需替换为正式证书
def load_close(code):
rows = requests.get(f"{BASE}/hsstock/history/{code}/d/n/{TOKEN}?st=20200101&et=20260828").json()
return pd.Series({r["t"]: r["c"] for r in rows}, name=code)
panel = pd.DataFrame({c: load_close(c) for c in ["600519","000001.SZ","300750"]})
panel 的行索引是"所有出现过的交易日并集"。此时 panel.loc["2023-06-01"] 才是三只股票真正同一天的收盘价。
三、对齐策略:交集 vs 前向填充
拿到面板后,空值(NaN)怎么处理,决定回测口径:
| 策略 | 做法 | 适合 | 风险 |
|---|---|---|---|
| 交集(inner) | 只保留三只都有数据的交易日 | 严格同屏比较、配对交易 | 样本量骤减(任一只停牌就丢掉那天) |
| 前向填充(ffill) | 停牌日用最近一个有效价填充 | 持仓市值估算、宽基回测 | 停牌期间"假波动为 0",需配合停牌标记 |
panel_inner = panel.dropna() # 交集:最严
panel_ffill = panel.ffill() # 前向填充:最宽(但需标记停牌)
经验:因子/横截面计算(#16)常用交集,保证"同一天可比";组合净值估算常用前向填充,但必须叠加"停牌标记"避免把停牌当"持平"。
四、本机实测(真实字段 + 对齐逻辑可跑;缺口演示用示意)
真实历史 K 线字段(演示证书拉 600519 实测):
历史K线 字段: ['a', 'c', 'h', 'l', 'o', 'pc', 'sf', 't', 'v']
样本首行: t=2025-09-15, o=11.70, h=11.72, l=11.63, c=11.65, v=840387, a=980347616, pc=11.72(前收), sf=0
下面用一段合成但结构真实的示例演示"日期缺口导致错位、pivot 后对齐"(演示证书对各股返回相同窗口,无法体现缺口,故用合成数据说明机制;字段语义与真实一致):
import pandas as pd, numpy as np
# 示意:A 上市晚(从 d3 起)、B 中间停牌(d2 缺失)、C 完整
A = pd.Series([np.nan,np.nan,10,11,12], index=[f"d{i}" for i in range(1,6)], name="A")
B = pd.Series([5,np.nan,7,8,9], index=[f"d{i}" for i in range(1,6)], name="B")
C = pd.Series([20,21,22,23,24], index=[f"d{i}" for i in range(1,6)], name="C")
panel = pd.DataFrame({"A":A,"B":B,"C":C})
print("按行号错位的'同日' d3:", panel.iloc[2].to_dict()) # A=10,B=7,C=22 才是真同日
print("交集后天数:", len(panel.dropna())) # 仅 d3,d4,d5 三只齐全
print("前向填充 B 停牌日 d2:", panel["B"].ffill().iloc[1]) # 用 d1 的 5 填充
运行后你会看到:
按行号错位的'同日' d3: {'A': 10, 'B': 7, 'C': 22} # pivot 后以日期为轴,才是真同日
交集后天数: 3 # 任一只缺失就丢那天
前向填充 B 停牌日 d2: 5.0 # 停牌日用最近有效价
五、原理深挖:对齐前必须先复权
一个容易被忽略的前提:对齐用的价格,必须是同一复权口径。如果 A 用前复权、B 用不复权,它们的"同日收盘价"量纲不同,算出来的收益率、相关性全是错的。所以标准流程是:
拉历史 → 统一复权口径(通常后复权做回测) → 再按日期 pivot 对齐 → 再算指标
复权为什么必须统一、三种口径差在哪,见 #09。
另外注意:历史 K 线末条的 t 恒为该周期结束日标签(周线=周日、月线=自然月末,含非交易日),这是接口约定而非 bug——对齐时按字符串日期处理即可,不要"修正"成日线末日(会让历史数据抖动)。
六、小结
多股票回测的第一条铁律:永远用交易日历当索引,绝不用行号。pivot 成面板后,交集保证严格同屏、前向填充保证宽覆盖(但叠加停牌标记)。对齐之前,先统一复权口径——否则你比对的是两套量纲的价格。
把"全市场 5000+ 只 × 多年日 K"对齐成面板,存储与回拉成本不小;魔码量化 Pro 包(完整历史分钟/日 K + 更高配额)能让这一步直接从"手忙脚乱"变"一键面板"。详见文末。
免责声明:本文所有示例数据仅用于接口演示,不构成任何投资建议;市场有风险,投资需谨慎。
系列持续更新中。 想要亲手跑通上面的代码?前往 魔码证书申请页 免费领取你的专属证书,复制即用、按次计费、稳定可用。
