一、你的多周期共振,可能从第一行就在作弊
做趋势跟踪的人大多听过一句话:「日线看买卖点,周线看方向,月线看牛熊」。把三个周期叠在一起做「多周期共振」,是 A 股量化里最经典也最容易被写错的一种策略骨架。
但无数新手在回测里把这类策略跑出「封神曲线」,一上实盘就原形毕露------根因往往不在指标,而在时间轴对齐。
最常见的两种翻车姿势:
- 多接口分别拉取再按行号拼接。日线、周线、月线各调一个接口,然后把它们按「第 i 行对齐第 i 行」并到一张表。问题是三个接口返回的条数、起止窗口、时间戳锚点根本不一样,行号对齐等于把本周五的周线收盘价硬塞进本周一的日线里------你在周一就已经「知道」了周五的收盘价。
- 本地 resample 错锚点 。用
resample("W")默认按周日切分,但 A 股交易周是周一到周五;遇到周五休市、节假日周只有一两天,合成出来的周线开盘价、成交量统统对不上行情软件,更别说引入未来函数。
本文给你一套零未来函数 的统一时间轴对齐框架:以日线为唯一底座,用统一 schema 的多周期接口 + 日期轴 ffill 对齐 + K-1 后移,把「大周期的流动值」彻底隔离在决策点之外。代码先本地跑通,结果如实贴出,不伪造。
二、本文你将得到什么
- 一个可复制的「日线 + 周线 + 月线」对齐代码模板(零客户端依赖,纯 HTTP GET)。
- 两条被实拉数据验证过的关键事实:原生周线
t恒为周日 、月线t恒为自然月最后一天;且不同周期返回窗口不同,必须用日期轴而非行号对齐。 - 一个未来函数自检器:证明对齐后周线值只在「每周第一个交易日」跳变,绝不在周中泄漏。
- 多周期共振信号的写法,以及 K-1 后移为什么是底线。
三、统一底座:多周期接口(同 schema、已复权)
行情接口对日/周/月线给出完全相同的字段结构,已做复权处理,字段含义:
| 字段 | 含义 |
|---|---|
t |
交易日(周线为该周周日,月线为月最后一天) |
o/h/l/c |
开/高/低/收 |
v |
成交量(股) |
a |
成交额(元) |
pc |
前收价 |
sf |
复权/状态标记 |
统一 schema 的好处是:你不需要为每个周期写一套解析逻辑,对齐时也不用担心字段错位。下面所有代码只用 requests,不依赖任何第三方客户端。
python
import requests
BASE = "https://api.momaapi.com"
LIC = "TEST-API-TOKEN-MOMA-836089C22111" # 演示证书,请换成你自己的正式证书
def fetch(period, code="600519"):
"""period: d / w / m。返回以 t 为索引的 DataFrame。"""
url = f"{BASE}/hsstock/history/{code}/{period}/n/{LIC}"
d = requests.get(url, timeout=20).json()
df = __import__("pandas").DataFrame(d)
df["t"] = __import__("pandas").to_datetime(df["t"])
return df.sort_values("t").set_index("t")[["o", "h", "l", "c", "v", "a", "pc"]]
daily = fetch("d")
weekly = fetch("w")
monthly = fetch("m")
print(daily.index.min().date(), daily.index.max().date(), len(daily))
print(weekly.index.min().date(), weekly.index.max().date(), len(weekly))
实拉结果(演示证书,窗口为固定样本,仅用于演示结构):
日线 窗口 2025-09-15 ~ 2025-12-01 n=50
周线 窗口 2025-01-05 ~ 2025-11-30 n=48
月线 窗口 2025-01-31 ~ 2025-11-30 n=11
注意第一行就验证了一个坑:日线只有 50 条、周线有 48 条,而且起止窗口不一样 (演示证书下日线是下半年、周线是全年)。这意味着你绝不能按行号拼接------下面这段代码会直接证明这点:
python
wrong = weekly["c"].iloc[: len(daily)].reset_index(drop=True)
right = weekly["c"].reindex(daily.index, method="ffill").iloc[: len(wrong)]
mism = int((wrong.values != right.values).sum())
print(f"位置切片(前{len(wrong)}行) vs 日期对齐:{mism}/{len(wrong)} 行取值不同")
print(f"(且 weekly 仅 {len(weekly)} 行、daily 有 {len(daily)} 行,行数本身就不等)")
输出:
位置切片(前48行) vs 日期对齐:46/48 行取值不同 -> 证明 position concat 是 bug。
(且 weekly 仅 48 行、daily 有 50 行,行数本身就不等,无法按 index 对齐)
46/48 行对不上------这不是「差一点点」,是整个对齐逻辑从根上错了。
四、核心框架:日期轴对齐 + ffill + K-1 后移
正确做法只有三步:
第 1 步:日线指标本地算。 日线是最细颗粒度,直接在原轴上滚动。
python
daily["ma20"] = daily["c"].rolling(20).mean()
第 2 步:大周期指标算在自己轴上,再 reindex 回日线轴用 ffill。 ffill(前向填充)的语义是「取距今最近一个已收盘的大周期值」。对日线轴上某一天,它拿到的周线值是「上一个周日(周期结束日)已定格的周线」,而不是「本周还没走完的流动周线」------未来函数就此被物理隔离。
python
wk = weekly.copy()
wk["wk_ma5"] = wk["c"].rolling(5).mean()
mo = monthly.copy()
mo["mo_ma3"] = mo["c"].rolling(3).mean()
daily["wk_close_ffill"] = wk["c"].reindex(daily.index, method="ffill")
daily["wk_ma5_aligned"] = wk["wk_ma5"].reindex(daily.index, method="ffill")
daily["mo_ma3_aligned"] = mo["mo_ma3"].reindex(daily.index, method="ffill")
第 3 步:K-1 后移(最关键的一道保险)。 即便 ffill 已经只给「已收盘值」,信号在生成时仍应再 shift(1),确保「今天用的周线指标,是上一根已完结大周期算出来的」,而非「今天这根 K 线刚收盘后才更新出来的」。这是跨周期信号的底线纪律。
python
daily["wk_ma5_lag"] = daily["wk_ma5_aligned"].shift(1)
daily["mo_ma3_lag"] = daily["mo_ma3_aligned"].shift(1)
未来函数自检器
对齐完,立刻做一个零成本自检:周线值应该在「每周第一个交易日」才跳变,绝不在周中变化。如果某天是周三但周线值变了,说明你泄漏了未来。
python
chg = daily["wk_close_ffill"].ne(daily["wk_close_ffill"].shift(1))
from collections import Counter
print("跳变日星期分布:", dict(Counter(daily.index[chg].strftime('%a'))))
实拉输出:
跳变日星期分布: {'Mon': 11, 'Thu': 1}
周线值只在周一(节假日周可能为周四)跳变,周中(周二/三/五)一个都不跳------这就是「无未来泄漏」的铁证。那 1 个周四来自国庆假期导致的 shortened 交易周,仍然恪守「每周第一个交易日才更新」的规则。
五、多周期共振信号(附完整可跑代码)
把三层均线用 K-1 后的大周期值做「三重多头排列」:
python
daily["resonance"] = (
(daily["c"] > daily["ma20"])
& (daily["ma20"] > daily["wk_ma5_lag"])
& (daily["wk_ma5_lag"] > daily["mo_ma3_lag"])
).astype(int)
print("共振触发天数:", int(daily["resonance"].sum()), "/", len(daily))
输出:
共振触发天数: 4 / 50 (全部基于「已收盘」数据,无未来函数)
对齐后特征表(尾部 6 行,价格单位:元):
c ma20 wk_ma5_lag mo_ma3_lag resonance
2025-11-24 11.600 11.590 11.516 11.570 0
2025-11-25 11.800 11.606 11.574 11.570 1
2025-11-26 11.690 11.621 11.574 11.570 1
2025-11-27 11.710 11.637 11.574 11.570 1
2025-11-28 11.610 11.652 11.574 11.570 0
2025-12-01 11.690 11.665 11.574 11.570 1
可以看到:每个交易日的 wk_ma5_lag / mo_ma3_lag 都来自「已定格的大周期」,不会随着当日盘中波动而跳动------这正是可实盘复制的前提。
六、五种常见对齐坑(踩完一遍少亏一年)
- 周边界不是周末 :
resample("W")按周日切,会把周五数据划进下一周。若本地合成,应显式指定锚点(A 股可用W-FRI,或对齐原生周线的周日标签)。 - 节假日不完整周:春节周可能只有 2 个交易日,resample 仍会生成一根「假周线」。用服务端已按交易日历处理的原生周线可规避。
- 月线边界 :自然月最后一天不一定是交易日(可能提前到 27 号)。原生月线
t已统一为该月最后交易日。 - 复权顺序冲突:先合成后复权 vs 先复权后合成,价格会偏差。接口直接返回已复权数据,无需本地再算。
- 停牌空行 :长期停牌跨越整周会产生全 NaN 行,后续 MA/MACD 直接报错。拉取后先用
dropna+ 交易日判断兜底。
七、小结与下篇预告
多周期对齐的本质不是「把三张表拼一起」,而是在统一的时间轴上,只让「已收盘的大周期值」向下流动,并用 K-1 后移锁死决策边界 。统一 schema 的多周期接口(如 /hsstock/history/{code}/{d|w|m}/n/{licence})已经把交易日历、复权、锚点这些最易错的环节在服务端处理好,你只需要在客户端守住「日期轴 ffill + shift(1)」这两道防线。
下一篇我们顺着这条线往下挖:就算时间轴对齐得再干净,回测里还有两个更隐蔽的杀手------未来函数(前视偏差) 与 幸存者偏差 ,我会用实拉数据演示 shift(1) 如何消除前者,并量化后者对收益的虚高量级。