打开任何一个行情软件,切到日 K,副图里那个「两根线 + 一排红绿柱」的指标,十有八九就是 MACD。它被很多老股民叫作「指标之王」:不吹不黑,它未必最准,但一定最常见——你迟早会在新闻、研报和股评里遇到「MACD 金叉」「顶背离」这些词,与其到时一头雾水,不如今天就把它的底细看穿。
上一篇我们认识了均线系统:SMA 等权平均、EMA 越近越重要,还用 MA5 与 MA20 演练了金叉与死叉。本篇在这个基础上再进一步——两条均线的「距离」本身也是信息,MACD(Moving Average Convergence Divergence,指数平滑异同移动平均线)正是把这个想法做成指标的典范:用 12 期快 EMA 减 26 期慢 EMA 得到差离值 DIF,看两条均线的「距离」如何张合;再给 DIF 做一次 9 期 EMA 平滑得到 DEA;最后把两者的距离画成红绿柱。本篇我们不背口诀,从三行公式出发,手写一个完整 MACD,自动标记金叉死叉,讲清零轴与背离,最后让参数跑起来看取舍。
本篇目标
学完本篇,你能够做到:
- 用三行公式说清 DIF、DEA、MACD 柱的推导关系,不再把它们当成三条互不相干的线。
- 用 pandas 的
ewm从零手写 MACD,并封装成参数可调的函数。 - 用代码自动标记金叉(Golden Cross)与死叉(Dead Cross),并区分信号发生在零轴上方还是下方。
- 用文字加图示解释顶背离与底背离,说清它为什么是「预警」而不是「发令枪」。
- 说出 (12, 26, 9) 三个参数各自的作用,以及调大调小的取舍。
核心概念
三步推导:DIF、DEA 与柱
整个 MACD 只有三步,每一步都建立在上一步之上。第一步算两条 EMA——EMA12 与 EMA26,平滑系数就是第 10 篇讲的 2/(n+1),ewm(span=n, adjust=False) 一行搞定;第二步相减、再平滑:
DIF = EMA12 − EMA26 (差离值,Difference)
DEA = EMA9(DIF) (信号线,Signal,对 DIF 再做一次 EMA9)
MACD 柱 = 2 × (DIF − DEA) (柱线,Histogram,A 股软件口径)
白话版:DIF 衡量「短期均线比长期均线远了多少」——价格上涨时短期 EMA 跑在前面,DIF 为正且越大,代表上涨动能越足;DEA 是 DIF 的平滑版,相当于滤掉毛刺后的「动能均线」;柱是 DIF 与 DEA 的距离再放大两倍,把「动能与其自身均值的关系」变成一眼能看的红绿柱。柱为正表示 DIF 在 DEA 上方、动能强于近期平均;柱由长变短时,哪怕还在零轴之上,也说明动能在衰减。
划重点:柱线乘 2 是 A 股软件(同花顺、通达信等)的惯用口径,国际平台(如 TradingView)通常不乘。两种口径画出来的柱高低不同,但金叉死叉的位置完全一样。本篇统一用 A 股口径,和别人讨论指标前先对齐口径再吵架。
金叉、死叉与零轴:信号的坐标系
金叉指 DIF 自下而上穿过 DEA(柱由负转正),死叉相反。但「在哪儿交叉」比「交叉」本身更重要:零轴是 DIF 的正负分界,DIF > 0 等价于 EMA12 > EMA26,也就是短期均线在长期均线上方——正是第 10 篇里金叉之后、死叉之前的那段多头状态。所以零轴上方的金叉,是「上升趋势中的动能再启动」,顺势信号;零轴下方的金叉,是「下跌趋势中的反弹起步」,波动大、假信号多。这就是双均线思路的动能化改写:均线看方向,MACD 看方向里的油门深浅。
背离:价格与新高不同步的警报
背离(Divergence)是 MACD 最有名的用法。顶背离:价格创出阶段新高,DIF 却没能同步创新高——价格还在涨,但「油门」已经松了。底背离相反:价格创新低,DIF 的低点却抬高,下跌动能在衰竭。看个示意图:
顶背离(两个上涨波段,上:价格,下:DIF)
价格
峰1
/\ 峰2
/ \ /\
/ \ / \
/ \_______/ \____
↑ 价格峰 2 高于峰 1
DIF
峰1
/\ 峰2
/ \ /
/ \______/
↑ DIF 峰 2 低于峰 1 —— 顶背离
此时往往伴随红柱一波比一波矮。要强调的是:背离是动能衰减的预警,不是精确的买卖点——强趋势里背离可以连续出现三四次,每次都当见顶信号去操作,会被趋势反复打脸。它更像「减速带」:提醒你系好安全带,而不是替你决定下车。
划重点:背离之后可以再背离,趋势的韧性经常超出想象。实战里背离要结合位置(零轴上下)、量能和更大的时间框架一起看,单独使用很容易「预测了十次顶,对了半次」。
参数 (12, 26, 9):三个旋钮各管什么
12 与 26 是快慢 EMA 的周期,决定 DIF 对价格变化的反应速度;9 是 DEA 对 DIF 的平滑周期,决定信号的「迟钝程度」。三个数一起调小,指标变灵敏:信号多、来得早、假信号也多,适合观察短线节奏;一起调大则钝化:信号少、来得晚、噪声小,适合看中期趋势。没有「最优参数」这回事——第 25 篇讲参数优化时会看到,把参数调到历史回测最好看,往往只是过拟合(Overfitting)。
划重点:换参数就像换眼镜,看同一条路,清晰度和变形各有取舍。指标的价值在于给你一个一致的观察框架,而不是一个万能参数。
完整代码
依赖:uv pip install pandas numpy,可选 uv pip install akshare 拉真实行情;拉不到会自动降级为本地模拟数据。第一块造数据并手写 MACD 函数;三块代码连起来依次运行。
import numpy as np
import pandas as pd
def make_fake_close(n: int = 250, seed: int = 11) -> pd.Series:
"""本地造一段模拟收盘价:先涨、再震荡、后跌,方便观察指标形态。"""
rng = np.random.default_rng(seed) # 固定随机种子
drift = np.concatenate([
rng.normal(0.0015, 0.012, 100), # 前 100 天:缓慢上涨
rng.normal(0.0000, 0.010, 80), # 中间 80 天:横盘震荡
rng.normal(-0.0015, 0.014, 70), # 后 70 天:缓慢下跌
])
dates = pd.bdate_range("2024-01-02", periods=n) # 交易日历
close = pd.Series(20 * np.exp(np.cumsum(drift)), index=dates).round(2)
close.index.name = "date"
return close
def macd(close: pd.Series, fast: int = 12, slow: int = 26,
signal: int = 9) -> pd.DataFrame:
"""手写 MACD:输入收盘价,输出 DIF、DEA 与 MACD 柱。"""
ema_fast = close.ewm(span=fast, adjust=False).mean() # 快线 EMA12(口径同第 10 篇)
ema_slow = close.ewm(span=slow, adjust=False).mean() # 慢线 EMA26
dif = ema_fast - ema_slow # DIF = 快线 − 慢线
dea = dif.ewm(span=signal, adjust=False).mean() # DEA = DIF 的 EMA9
hist = 2 * (dif - dea) # MACD 柱(A 股软件口径 ×2)
return pd.DataFrame({"DIF": dif, "DEA": dea, "MACD": hist})
close = make_fake_close()
try:
import akshare as ak
raw = ak.stock_zh_a_hist(symbol="600519", period="daily",
start_date="20240101", end_date="20241231")
close = pd.Series(raw["收盘"].values, index=pd.to_datetime(raw["日期"]),
name="close")
print("已使用 akshare 真实行情。")
except Exception:
print("真实数据不可用,改用本地模拟行情。")
ind = macd(close)
print(ind.tail(3))
第二块把金叉死叉自动标出来,并区分零轴位置。
import pandas as pd
ind = macd(close) # 接上文:close 为收盘价,macd 为上文手写函数
# 1) 金叉 / 死叉:今天穿越 + 昨天还没穿越,两个条件相与(shift 防未来数据)
cross_up = (ind["DIF"] > ind["DEA"]) & (ind["DIF"].shift(1) <= ind["DEA"].shift(1))
cross_dn = (ind["DIF"] < ind["DEA"]) & (ind["DIF"].shift(1) >= ind["DEA"].shift(1))
ind["signal"] = 0
ind.loc[cross_up, "signal"] = 1 # 金叉记 +1
ind.loc[cross_dn, "signal"] = -1 # 死叉记 -1
# 2) 零轴位置:同一个交叉,零轴上还是零轴下,含义大不相同
sig_days = ind[ind["signal"] != 0]
print(f"全样本共 {len(sig_days)} 个信号:")
for day, row in sig_days.iterrows():
zone = "零轴上" if row["DIF"] > 0 else "零轴下"
action = "金叉" if row["signal"] == 1 else "死叉"
print(f"{day.date()} {action}({zone})")
# 3) 柱线为正的天数占比:DIF 在 DEA 上方的时间,趋势强弱的粗略体温计
up_ratio = (ind["MACD"] > 0).mean()
print(f"柱线为正的天数占比:{up_ratio:.0%}")
# 4) 分段统计:按造数据时的三段行情,分别看柱线为正的占比
seg = pd.Series("震荡段", index=ind.index) # 默认全部标为震荡段
seg.iloc[:100] = "上涨段" # 前 100 天(与造数据的三段设定一致)
seg.iloc[180:] = "下跌段" # 第 181 天起
print(ind.assign(seg=seg).groupby("seg")["MACD"].apply(lambda s: (s > 0).mean()).round(2))
第三块让参数跑起来,对比三组参数的信号数量与 DIF 波动幅度。
import pandas as pd
# 4) 参数可调:同一份行情,三组参数跑一遍,看灵敏度与噪声的取舍
def golden_cross_count(close: pd.Series, fast: int, slow: int, signal: int) -> int:
"""返回给定参数下的金叉次数(DIF 上穿 DEA 的天数)。"""
m = macd(close, fast, slow, signal) # 复用上文的手写 macd
up = (m["DIF"] > m["DEA"]) & (m["DIF"].shift(1) <= m["DEA"].shift(1))
return int(up.sum())
for fast, slow, sig in [(6, 13, 5), (12, 26, 9), (24, 52, 18)]:
m = macd(close, fast, slow, sig)
n = golden_cross_count(close, fast, slow, sig)
print(f"参数({fast:2d},{slow:2d},{sig:2d}):金叉 {n} 次,"
f"DIF 波动幅度(标准差){m['DIF'].std():.3f}")
print("结论:参数越小越灵敏,信号越多、噪声越大;参数越大越钝,信号越少、越稳。")
代码逐段解释
第 1 块:数据与手写 MACD。 make_fake_close 用三段不同斜率的随机游走拼出「先涨、再震荡、后跌」的行情:np.concatenate 把三段日收益拼在一起,np.exp(np.cumsum(...)) 累乘成价格曲线(起点 20 元)——这种带趋势转折的形状比纯随机走势更适合观察指标形态。macd 函数是全篇核心:两个 ewm(span=..., adjust=False) 算快慢 EMA(口径与第 10 篇完全一致),相减得 DIF,DIF 再做一次 ewm(span=signal) 得 DEA,2 * (dif - dea) 得柱。打印尾部(示意,模拟数据):
真实数据不可用,改用本地模拟行情。
DIF DEA MACD
date
2024-12-13 -0.281 -0.236 -0.090
2024-12-16 -0.302 -0.249 -0.106
2024-12-17 -0.268 -0.253 -0.030
下跌段里 DIF 与 DEA 都在零轴下方、柱为负,数值形态与理论一致——柱恒等于 DIF 与 DEA 之差的两倍,你可以拿这三行验算:2 × (−0.281 − (−0.236)) = −0.090。
第 2 块:信号标记。 金叉的判定是「今天 DIF > DEA 且昨天 DIF ≤ DEA」:今天的比较用当天值,昨天的状态用 shift(1) 取,两个条件相与,一行锁定穿越发生的那一天;死叉镜像。signal 列用 +1/−1 记录信号,循环打印每个信号日与它的零轴位置(示意):
全样本共 7 个信号:
2024-01-16 金叉(零轴下)
2024-02-27 死叉(零轴上)
2024-03-18 金叉(零轴上)
2024-05-20 死叉(零轴上)
2024-07-15 金叉(零轴上)
2024-09-10 死叉(零轴上)
2024-11-22 死叉(零轴下)
读法:1 月的金叉发生在零轴下方,属于「下跌后的反弹起步」;3 月与 7 月的零轴上金叉才是「上升趋势中的动能再启动」;11 月的零轴下死叉则与下跌段相互印证。信号个数与行情的三段结构吻合:上涨段以零轴上金叉为主、下跌段死叉增多。柱线为正天数占比(示意:约五成)等于「DIF 在 DEA 上方的时间占比」;再按造数据时的三段切开来统计(示意,groupby 按标签编码排序):
seg
上涨段 0.72
下跌段 0.18
震荡段 0.45
上涨段动能为正的时间最长、下跌段最短,与三段设定吻合——这也是检查指标有没有「算对」的土办法:让数字讲得通行情的故事。
第 3 块:参数对比。 把 (6, 13, 5)、(12, 26, 9)、(24, 52, 18) 三组参数喂给同一个 macd 函数,分别数金叉次数、算 DIF 的标准差(示意):
参数( 6, 13, 5):金叉 9 次,DIF 波动幅度(标准差)0.164
参数(12, 26, 9):金叉 6 次,DIF 波动幅度(标准差)0.108
参数(24, 52, 18):金叉 3 次,DIF 波动幅度(标准差)0.071
规律清晰可见:参数越小,金叉越频繁、DIF 起伏越大——灵敏与噪声是一枚硬币的两面。把三组参数的手感记下来,第 18 篇写双均线回测、第 25 篇做参数优化时,今天搭好的 macd 函数和「数信号」的思路会直接派上用场。
常见坑与小练习
坑 1:口径打架。 同一天同一只票,你的 MACD 柱和朋友的差一倍,多半是「乘不乘 2」的口径差异,也可能是 adjust 参数不同。写代码时把口径写进注释,跨平台对比先统一口径。
坑 2:ewm 忘了 adjust=False。 默认的 adjust=True 用另一种加权方式,数值与行情软件对不上,整条 MACD 链都会偏差。第 10 篇定下的规矩:与软件对齐就用 adjust=False。
坑 3:拿 signal 列直接回测。 金叉那天收盘之后才确认信号,当天就用收盘价成交属于偷跑(未来函数)。回测时至少 shift(1) 到次日执行;这个问题的完整讨论在第 17、18 篇。
坑 4:数据有洞还硬算。 停牌、缺数会让 EMA 断档,DIF 出现莫名的跳变。先把缺失处理好(第 8 篇的数据清洗),再算指标。
坑 5:把背离当精确卖点。 背离是动能预警,可以连续出现;把它当发令枪,强趋势里会反复止损。背离只负责「提醒」,不负责「扣扳机」。
小练习 1: 给 macd 的返回值加一列 cross(金叉 1、死叉 −1、其余 0),统计全样本的金叉死叉次数,再只统计「零轴上方」的金叉次数。
思路提示:先
ind[ind["DIF"] > 0]筛出零轴上方的行,再对cross == 1计数。
小练习 2: 把参数改成 (5, 35, 5) 跑一遍,观察金叉次数和 DIF 标准差的变化方向,并用一句话解释为什么快慢线周期拉得越开,信号越少。
思路提示:快慢 EMA 的差异被拉平后,DIF 本身更「钝」,穿越 DEA 的次数自然减少。
小练习 3: 在模拟数据的下跌段里找一次底背离:打印 DIF < 0 期间的收盘价局部低点与对应的 DIF 值,比较两个相邻低点的 DIF 高低。
思路提示:可以用
rolling(10).min()找价格的局部低点,再看这些日期上 DIF 是否一个比一个高。
本文 MACD 相关内容仅用于教学,不构成投资建议,实盘前请充分测试。