← 返回目录
p1.4动手⏱ 约 14 分钟

交易日历与多频率对齐

你的月频标签,有三分之一不是交易日

🔎 最后验证 2026-07📚 来源:交易日历对比与重采样标签检查于 2026-07-31 在 ECS 实测(2015-01-05~2026-07-29,2811 个交易日),输出见 code/outputs/stdout.txt🧰 baostock、pandas 3.0.5、yfinance 1.5.2
为什么学这个
weekly = close.resample("W-FRI").last()
monthly = close.resample("ME").last()

这两行代码,你大概率写过。它们看起来毫无问题——取每周五、每月末的收盘价。

实测:在 2015 到 2026 这段里,月频重采样产生的 139 个标签中,48 个不是交易日——34.5%

值是对的,标签是错的。而当你把这张表和另一张表 merge 的时候,错的标签会安静地丢掉三分之一的行。

💡 打个比方

快递单上写"周一送达",但那天是元旦,快递站根本没开门。包裹实际是周二到的。

单子上的日期和实际发生的日期不一致——单独看没问题,一旦要和别的记录对账,就全乱了。

一、别用 bdate_range 生成交易日

fake = pd.bdate_range("2015-01-01", "2026-07-29")   # 周一到周五

实测对比真实交易日历(交易所口径,经 baostock 获取):

真实交易日      2811 天
bdate_range 给的 3020 天
❌ 多算(以为开市其实休市) 209 天 = 7.4% 的交易日被凭空造出来
❌ 漏算(实际开市却被跳过) 0

每年被凭空造出 13–20 天:

年份201520182020202220242025
多算天数171819182018

最近的几个例子:2026-02-23(春节)、2026-05-01 / 05-04 / 05-05(五一)、2026-06-19(端午)。

后果:用假日历做 reindex,长假里会插入一串"价格不变"的假交易日。它们会稀释波动率、拉平均线、让换手统计失真——和 p1.3 的停牌坑同源:假的 0 收益,一律往下压你的风险指标

⚠️ 避坑

「调休补班的周六,A股开不开市?」

本次实测区间(2015–2026)里,落在周末的真实交易日是 0 天

但请不要因此在代码里写死"周末一定不开市"。交易日历是交易所公告的结果,不是可以推算的规律——历史上出现过周末交易安排,规则也可能调整。

正确做法:每次从交易所口径取真实日历,不要自己推。这一条看起来啰嗦,但它是 p1.1「凡是能查的就不要猜」的延续。

二、重采样的标签会骗人

周频 W-FRI: 592 个周期,其中 37 个标签不是交易日 (6.2%)
月频 ME:    139 个周期,其中 48 个标签不是交易日 (34.5%)

月频超过三分之一。

原因不难理解:resample 的标签用的是日历日——每月最后一个自然日、每周的周五。而 5 月 31 日可能是周日,10 月 31 日可能在长假里。

值是对的(pandas 取的是区间内最后一个有数据的日子),标签是错的

危害在于对账:

  • 你以为拿到的是 5 月 31 日的收盘,实际是 5 月 30 日的
  • 和另一张按真实交易日索引的表 merge → 那一行直接消失
  • 如果你 merge 前后没检查行数,丢了三分之一的数据都不会有人告诉你

修法:把标签换成区间内真实的最后一个交易日

g = close.groupby(pd.Grouper(freq="ME"))
last_day = g.apply(lambda s: s.index[-1] if len(s) else pd.NaT).dropna()
aligned = pd.Series(g.last().dropna().to_numpy(), index=pd.DatetimeIndex(last_day))

实测:修正后 139 个月度点,非交易日标签 0 个

🔧 动手做:你的月频标签,三分之一不是交易日(5 分钟)

python calendar_align.py。它把 pandas 默认的工作日/重采样标签,和真实交易日历比。

你会看到(实测):pd.bdate_range 生成的"工作日"比真实交易日多算 209 天(7.4%)(它不知道国庆春节休市);月频重采样 ME 的 139 个标签里,48 个落在非交易日(34.5%)——你以为在每月末取值,其实三分之一取在了休市日。

想明白:pandas 的日历不懂 A 股休市。用它生成的标签去对齐行情,会静默地取错日子(取到最近可用的、或 NaN)。改用"区间内最后一个真实交易日"作索引,非交易日标签立刻归零。任何按日历重采样的操作,先问:这个日历是真实交易日历吗?

三、跨市场对齐:重叠率只有 93.4%

A股与美股(标普 500)在同一段区间:

A股交易日 2811 | 美股交易日 2908 | 共同开市 2715 
 A股开市 96 天(美股假期:感恩节/圣诞/独立日…)
仅美股开市 193 天(A股长假:春节/国庆/五一…)
重叠率 93.4%

近 300 个交易日,两个市场只开一边。跨市场策略必须先回答三个问题:

  1. 调仓日怎么定?只在共同开市日调,还是各自日历各自跑?
  2. 缺失日怎么填?直接 join 会产生 NaN;前值填充等于"拿昨天的美股当今天用"——这是 p1.2 讲过的对齐层泄漏
  3. 时差怎么算?美股当日收盘发生在北京时间次日凌晨。用美股收盘信号做 A 股决策,至少要再延一天,否则就是未来函数

第三条最容易漏。很多"中美联动"策略的超额收益,拆开看全部来自这一天的时差。

❓ 测验
你把日线 resample 成月频后,和一张按真实交易日索引的因子表做 inner merge,结果行数少了三分之一。最可能的原因?
✏️ 填空
pandas 的 resample 用的标签是 ___ 日(如每月最后一个自然日),而不是真实交易日。

📌 免责:本课为技术教学,不构成投资建议。交易日历以交易所公告为准,课文数据为 2026-07-31 实测快照。

✅ 小结

三个实测数字:

  • 209 天:bdate_range 在 11 年里凭空造出的假交易日(7.4%),每年 13–20 天
  • 34.5%:月频 resample 标签里不是交易日的比例(修正后 0)
  • 93.4%:A股与美股的交易日重叠率——近 300 天只开一边

一条纪律:交易日历必须从交易所口径取,不能自己推算。周末不开市、节假日不开市,这些都是"目前如此"而不是"必然如此"。

下一节把数据存下来:DuckDB / Parquet 怎么选、schema 怎么定、增量更新怎么做到幂等。从这一节起,你不用每次跑回测都重新联网取数——那既慢,又让你的结果依赖当天的网络状况。

下一节 → 数据落库与增量更新
🔎 来源与核验· 4 条,点开核对
本节每个关键论断都对应一个可追溯的来源 —— 这是本课程"靠谱、不过时"的底线。
「2015-01-05~2026-07-29 真实交易日 2811 天;pd.bdate_range 给 3020 天,多算 209 天(7.4%),漏算 0 天;每年多算 13–20 天」
📚 本节 code/calendar_align.py,2026-07-31 于 ECS 实测(交易日历来自 baostock query_trade_dates),输出见 code/outputs/stdout.txt✓ 已核验 2026-07
「周频 W-FRI 592 个标签中 37 个非交易日(6.2%);月频 ME 139 个标签中 48 个非交易日(34.5%);改用「区间内最后一个真实交易日」作索引后非交易日标签为 0」
📚 同上实测✓ 已核验 2026-07
「A股 2811 个交易日 vs 美股(^GSPC)2908 个,共同开市 2715 天,仅 A股 96 天、仅美股 193 天,重叠率 93.4%」
📚 同上实测(美股日历来自 yfinance)✓ 已核验 2026-07
「本区间内落在周末的 A股交易日为 0 天;但交易日历应从交易所口径获取而非推算」
📚 同上实测✓ 已核验 2026-07
智图软件的赞赏码
都看到这了,打个赏呗!
接下来 · p1.5
数据落库与增量更新
继续读下一节 →