第 2 章 · 03 MACD 趋势动量与因子验证(对应 QS013) 速查摘要:MACD(Moving Average Convergence Divergence,指数平滑异同移动平均线)是趋势策略最经典的指标之一,由 Gerald Appel 提出。本节手写 MACD 三件套(快慢 EMA 差 + 信号线 + 柱状图),用 对比识别金叉(买入信号)/死叉(卖出信号),并画出三子图专业图。最珍贵的彩蛋在末尾——用 算 5 日前瞻收益,再 算滚动相关性,直接验证"MACD 到底有没有预测力",这是从"算指标"升级到"做因子研究"的关键一步。 涉及脚本:原项目 QS013-macd/01macd.
速查摘要:MACD(Moving Average Convergence Divergence,指数平滑异同移动平均线)是趋势策略最经典的指标之一,由 Gerald Appel 提出。本节手写 MACD 三件套(快慢 EMA 差 + 信号线 + 柱状图),用
shift(1)对比识别金叉(买入信号)/死叉(卖出信号),并画出三子图专业图。最珍贵的彩蛋在末尾——用Close.shift(-5)算 5 日前瞻收益,再.rolling(30).corr(MACD)算滚动相关性,直接验证"MACD 到底有没有预测力",这是从"算指标"升级到"做因子研究"的关键一步。
涉及脚本:原项目 QS013-macd/01_macd.py(约 109 行 Python,是第 2 章教学价值最高的脚本——指标 + 信号 + 因子验证三合一)
⚠️ 注意:依赖 OpenBB 3(
from openbb_terminal.sdk import openbb),v4 不兼容。EMA 全部用ewm(span=..., adjust=False)(与 QS004 一致,金融指标必备)。前瞻收益shift(-5)含未来信息,只能用于事后因子验证,不能拿来做回测的"今日预测"——否则就是前视偏差(Look-Ahead Bias),回测结果会虚高。
MACD 的核心是比较两条 EMA 的"收敛与发散"。三件套定义:
MACD = EMA_short - EMA_long。正值表示快线在慢线上方(短期趋势向上),负值相反。Hist = MACD - Signal,衡量 MACD 相对其信号线的偏离。直观理解:MACD 是趋势的"加速度"——价格加速上涨时快线远离慢线,MACD 柱变大;趋势减弱时快慢线收敛,MACD 柱变小。信号线是 MACD 的滞后平滑,用来产生交叉信号。
数学表达——金叉在今天发生:
MACDₜ > Signalₜ 且 MACDₜ₋₁ ≤ Signalₜ₋₁
即"今天 MACD 已在信号线上方,但昨天还在下方(或相等)"——交叉发生在今天。shift(1) 取昨天的值来做这个判断。死叉对称。
任何指标都可以当因子(Factor),但能不能赚钱要看它对未来收益有没有预测力。验证模板:
forward_return = Close.shift(-5) / Close - 1——今天往后 5 天的收益率(用 -5 shift 取未来值)。forward_return.rolling(30).corr(MACD)——过去 30 天两者相关程度。这一步把"画指标图"升级为"定量验证因子有效性",是量化研究的核心方法论。注意:前瞻收益含未来信息,只能用于离线验证因子,绝不能进回测信号,否则就是前视偏差,回测会"看起来很美"但实盘必亏。
SYMBOL = "NVDA" START = "2021-09-30" END = "2024-03-01" df = openbb.stocks.load(SYMBOL, start_date=START, end_date=END)
选 NVDA(英伟达),2021-2024 横跨完整上涨周期,MACD 在这种强趋势股上信号丰富、视觉效果好。openbb.stocks.load 返回标准 OHLC DataFrame,列名是 Open/High/Low/Close/Volume。
def calculate_macd(df, short_period=12, long_period=26, signal_period=9): df['EMA_short'] = df['Close'].ewm(span=short_period, adjust=False).mean() df['EMA_long'] = df['Close'].ewm(span=long_period, adjust=False).mean() df['MACD'] = df['EMA_short'] - df['EMA_long'] df['Signal_Line'] = df['MACD'].ewm(span=signal_period, adjust=False).mean() df['MACD_Histogram'] = df['MACD'] - df['Signal_Line'] df['Bullish_Crossover'] = (df['MACD'] > df['Signal_Line']) & (df['MACD'].shift(1) <= df['Signal_Line'].shift(1)) df['Bearish_Crossover'] = (df['MACD'] < df['Signal_Line']) & (df['MACD'].shift(1) >= df['Signal_Line'].shift(1)) return df
逐行解读:
EMA_short/EMA_long 用 12/26 期 EMA(行业标准参数,Gerald Appel 原版),adjust=False 与 QS004 一致。
MACD = EMA_short - EMA_long 两线之差,捕捉快慢线收敛发散。
Signal_Line = MACD.ewm(span=9).mean() MACD 的 9 期 EMA,作为交叉基准。
MACD_Histogram = MACD - Signal_Line 柱状图,衡量 MACD 偏离信号线多少——柱状图变长说明动量加强,变短说明动量减弱(即使还没交叉)。
金叉/死叉判定(46-47 行)是本节的核心信号生成逻辑:
df['Bullish_Crossover'] = (df['MACD'] > df['Signal_Line']) & (df['MACD'].shift(1) <= df['Signal_Line'].shift(1))
拆开看:(今天 MACD > 今天 Signal) 且 (昨天 MACD ≤ 昨天 Signal)。第一项保证今天 MACD 已在上方,第二项保证昨天还在下方——交叉就发生在今天。两个条件用 & 组合,结果是布尔列,True 即金叉日。死叉对称,把 > 换 <、≤ 换 ≥。
注意 shift(1) 同时作用于 MACD 和 Signal——必须两侧都 shift,否则对比的是"今日 MACD"和"昨日 Signal",逻辑错乱。
💡 核心心法:识别交叉的标准模板是
(今A > 今B) & (昨A ≤ 昨B)——"今天穿越了,昨天还没穿越",交叉必发生在今天。shift(1)两侧都要 shift,缺一不可。这个模板适用于任何"穿越"信号(均线交叉、价格穿越均线等)。
plt.figure(figsize=(10, 12)) # 子图1:收盘价 plt.subplot(3, 1, 1) plt.plot(df.index, df['Close'], label='Close', color='blue') # 子图2:MACD + 信号线 + 金叉死叉散点 plt.subplot(3, 1, 2) plt.plot(df_with_macd.index, df_with_macd['MACD'], label='MACD', color='green') plt.plot(df_with_macd.index, df_with_macd['Signal_Line'], label='Signal Line', color='red') plt.scatter(df_with_macd[df_with_macd['Bullish_Crossover']].index, df_with_macd[df_with_macd['Bullish_Crossover']]['MACD'], color='blue', label='Bullish Crossover', marker='^', s=100) plt.scatter(df_with_macd[df_with_macd['Bearish_Crossover']].index, df_with_macd[df_with_macd['Bearish_Crossover']]['MACD'], color='red', label='Bearish Crossover', marker='v', s=100) # 子图3:柱状图 plt.subplot(3, 1, 3) plt.bar(df_with_macd.index, df_with_macd['MACD_Histogram'], label='MACD Histogram', color='purple') plt.tight_layout() plt.show()
三个子图自上而下:收盘价(背景)、MACD 与信号线(含金叉/死叉散点)、柱状图。重点看散点标记的写法——用布尔列 df[df['Bullish_Crossover']] 筛选 DataFrame 只取 True 行,再分别取 index(x 轴)和 MACD(y 轴)画散点。marker='^' 上三角(金叉看涨)、marker='v' 下三角(死叉看跌),s=100 控制点大小。这种"用布尔列选行画标记"是标注事件(交叉、突破、异常)的通用技巧。
# Make forward 5-day returns df_with_macd['5-day-forward-return'] = df_with_macd['Close'].shift(-5) / df_with_macd['Close'] - 1 # Calculate rolling correlation df_with_macd['rolling_corr'] = df_with_macd['5-day-forward-return'] \ .rolling(window=30) \ .corr(df_with_macd['MACD']) df_with_macd['rolling_corr'].describe()
这是全节最有价值的部分,逐行解读:
Close.shift(-5) / Close - 1 算"从今天起 5 天后的收益率"。
Close.shift(-5) 是"5 天后的收盘价"(-5 是负方向 shift,取未来值)。shift(-5) 含未来信息,这一列只能用于离线验证因子有效性,绝不能进实盘信号——否则就是前视偏差,回测会"上帝视角"作弊。.rolling(30).corr(MACD) 算"5 日前瞻收益"与"MACD"的 30 天滚动相关性。pandas 的 .rolling(w).corr(other) 语法很优雅:对每 30 天窗口,算当前 Series 与另一 Series 的皮尔逊相关系数。结果是一个时序——不同时段的相关性可能完全不同。.describe() 看相关性的统计分布(均值、分位数、最大最小)。如果均值显著为正(比如 0.1)且分布稳定,说明 MACD 对 5 日收益有正向预测力;如果均值接近 0 且方差大,说明预测力弱。这一步把研究从"画 MACD 看"升级为"量化 MACD 有没有用"——这正是量化研究员(factor researcher)日常工作。验证流程:定义因子 → 算前瞻收益 → 算相关性/IC → 决定要不要用。本节是个最小可运行版本,第 4 章会用 alphalens 做更专业的因子分析。
技巧一:识别交叉的标准模板 (今A > 今B) & (昨A ≤ 昨B)。shift(1) 两侧都要 shift,缺一不可。适用于金叉死叉、价格穿越均线、突破阈值等所有"穿越事件"。
技巧二:布尔列选行画散点标记事件。plt.scatter(df[df['事件列']].x, df[df['事件列']].y) 用布尔筛选只画事件发生的点,配 marker='^'/'v' 区分多空。这是标注信号的通用模式。
技巧三:Close.shift(-5)/Close - 1 算前瞻收益。shift(-5) 取 5 天后收盘(负方向 shift),算出未来 5 天收益率。仅用于离线因子验证,绝不可进回测信号(前视偏差)。
技巧四:series.rolling(w).corr(other) 滚动相关性。一行算两列在 w 天窗口里的皮尔逊相关,输出时序。因子验证的核心调用——看因子和未来收益的相关性是否稳定。
技巧五:三子图布局 subplot(3,1,k)。价格/指标/柱状图自上而下分层,tight_layout() 防重叠。配合散点标注,是专业金融图的基本骨架。
💡 速查要点:MACD 三件套(快慢 EMA 差 + 9 期信号线 + 柱状图),金叉死叉用
(今>今) & (昨≤昨)两条件 + 双侧 shift 识别。因子验证三步:前瞻收益shift(-5)、滚动相关rolling(30).corr、.describe()看显著性——这是从算指标到做因子研究的关键跃迁。
EMA_short(12) 减 EMA_long(26) 得 MACD,再 ewm(9) 得信号线,差值得柱状图,全部 adjust=False。(今 MACD > 今 Signal) & (昨 MACD ≤ 昨 Signal) 模板,shift(1) 两侧都要 shift。Close.shift(-5)/Close-1 算 5 日前瞻收益,.rolling(30).corr(MACD) 算滚动相关性,.describe() 看显著性——量化"指标有没有预测力"。shift(-5)),只能离线验证因子,绝不能进回测信号,否则前视偏差。