ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

MACD算法全解析:从EMA数学原理到Python量化实现

MACD算法全解析:从EMA数学原理到Python量化实现 MACD 大概是炒股软件里出镜率最高、解释得却最少的指标。每天盯着红柱绿柱进进出出的用户很多金叉死叉挂在嘴边的人也很多但真要问一句“股票软件里 MACD 的算法到底是啥样”能把整条计算链路讲清楚的人其实少得可怜。这篇文章我就把这个指标从数学定义到 Python 代码、再到行情终端里那张图是怎么画出来的一层层拆开给你看。适合两类人一类是想搞懂指标本质、不想再盲用金叉死叉的投资者另一类是要在自有量化系统里手写 MACD、或者要把回测结果和通达信对齐的工程师。看完你会发现它背后没有任何黑魔法核心就是两轮指数移动平均加一组差值放大。1. MACD 算的是哪三样东西DIF、DEA、柱状图1.1 所谓“指数平滑”到底是怎么平滑的MACD 全称 Moving Average Convergence Divergence译过来是指数平滑异同移动平均线。名字绕拆开就不绕了它先算两条移动平均线一条快一条慢然后用快线减慢线得到“异同”再把这个差值平滑一次最后画成柱子。整套算法最终输出三样东西DIF、DEA、MACD 柱。万事得先解决 EMA。EMA 是 Exponential Moving Average指数移动平均线。普通均线 SMA 的算法是把最近 N 根收盘价加起来除以 N每根K线权重完全一样EMA 的思路不同它认为离当前越近的K线越重要权重按指数速度递减而且只需要记住上一根 EMA 的值就能一路递推下去公式就一行EMA(今日) α × 今日收盘价 (1 - α) × EMA(昨日)其中 α 2 / (N 1)。12 日 EMA 对应 α 2/13 ≈ 0.153826 日 EMA 对应 α 2/27 ≈ 0.0741。你可以把 α 理解为“今天新价格占多大分量”α 越大新价格的影响越大曲线就越贴近价格α 越小曲线越平滑、滞后越明显。为什么说它是“指数平滑”因为把递推式层层展开之后历史每一天的权重是 (1-α) 的 k 次方k 越大权重越小呈指数衰减。所以理论上再老的数据都有影响只是小到可以忽略。递推需要一个起点。大多数软件的做法是拿第一根K线的收盘价当初始 EMA然后从第二根开始逐个迭代。我拿一个 5 日 EMA 举例假设连续五天收盘价是 1、2、3、4、5α 2/6 ≈ 0.3333。第二天 EMA 0.3333×2 0.6667×1 ≈ 1.3333第三天大约 1.8889第四天大约 2.5926第五天大约 3.3951。你看它一直在追最新价但永远不一步到位这就是 EMA 作为“动态平均”的特性。1.2 DIF、DEA、柱状图的先后顺序不能乱有了 EMA 基础MACD 的完整计算链路就是五步对收盘价序列计算 12 日 EMA得到 EMA12即快线。对同一份收盘价序列计算 26 日 EMA得到 EMA26即慢线。DIF EMA12 - EMA26。DEA DIF 序列的 9 日 EMA也叫信号线。MACD 柱 2 × (DIF - DEA)。这个顺序是死的不能跳先有快慢两条线才有差值 DIF先有 DIF 序列才能对它做 EMA 得到 DEA最后两者相减放大才是柱。国内软件里经常出现“快线”“慢线”“柱”的说法不同平台叫法混乱但底层都是同一套数据流收盘价 → EMA12 / EMA26 → DIF → DEA → MACD柱有一个细节值得单独说国内股票软件里 MACD 柱一般写成 (DIF - DEA) × 2但国际平台很多不乘 2。乘 2 纯粹是为了让红绿柱在视觉上更明显不会改变 DIF 和 DEA 的位置所以同一个品种、同一段行情不同平台柱高会有差异但金叉死叉的位置完全一致。搞懂这一点你就不会再疑惑“为什么通达信和 TradingView 的 MACD 数值对不上”。2. 从零实现 MACD一份可以直接跑的 Python 源码2.1 手写 EMA 和 MACD 主函数先给一份最小可运行的实现不依赖任何第三方库纯 Python 就能算def ema(data, period): alpha 2.0 / (period 1.0) out [0.0] * len(data) out[0] data[0] for i in range(1, len(data)): out[i] alpha * data[i] (1.0 - alpha) * out[i - 1] return out def macd(close, fast12, slow26, signal9): ema_fast ema(close, fast) ema_slow ema(close, slow) dif [f - s for f, s in zip(ema_fast, ema_slow)] dea ema(dif, signal) bar [2.0 * (d - e) for d, e in zip(dif, dea)] return dif, dea, bar调用方式很简单传入一个收盘价列表返回三个列表长度和输入一致。close_prices [10.0, 10.1, 10.2, 10.15, 10.3, ...] # 完整收盘价序列 dif, dea, bar macd(close_prices)有一点必须提醒EMA 是从第一根K线就开始往后递推的所以返回的列表从头到尾都有数值。但慢线是 26 日 EMA严格来说前 25 根K线里 EMA26 只是“热身”DIF 的参考意义有限。真正常用的 MACD 信号通常要等服务至少几十根K线以后再看。你在通达信里把 K 线数量缩到很少时会发现指标照样画但早期数值是不稳定的这也是后面要单独展开的坑。2.2 用 pandas 实现时最容易踩的 adjust 参数实际写代码的人很少用纯 Python 循环处理几千根K线基本都会上 pandas。pandas 里确实有ewm但默认情况和上面手写的 EMA 不是一回事import pandas as pd s pd.Series(close_prices) # 和通达信、手写 EMA 对得上 ema12_a s.ewm(span12, adjustFalse).mean() # 默认 adjustTrue早期数值和通达信对不上 ema12_b s.ewm(span12).mean()问题就出在adjust参数上。adjustTrue时pandas 会在早期样本上做无偏修正把初始阶段的计算方式改掉导致前几十根数值和股票软件明显不同数据越长差异会慢慢收敛但如果你验收的标准是“复现通达信的数字”那就必须设成adjustFalse。这个坑我见过太多次了很多人拿着 pandas 算出来的 MACD 去和行情软件比对怎么都对不上最后发现不是公式错是平滑方式不同。2.3 实时行情场景下的增量算法如果是做实时行情每分钟或每笔 tick 都在更新每次都重新读几万根K线全量算一遍就太笨了。EMA 是递推结构天然支持增量计算只要把上一次的 EMA 状态缓存住新来一根K线就能立刻算出新的 DIF、DEA、柱class IncrementalMacd: def __init__(self, fast12, slow26, signal9): self.alpha_fast 2.0 / (fast 1.0) self.alpha_slow 2.0 / (slow 1.0) self.alpha_signal 2.0 / (signal 1.0) self.ema_fast None self.ema_slow None self.dea 0.0 def update(self, close): if self.ema_fast is None: # 第一根K线直接用当前价初始化 self.ema_fast close self.ema_slow close dif 0.0 else: self.ema_fast self.alpha_fast * close (1 - self.alpha_fast) * self.ema_fast self.ema_slow self.alpha_slow * close (1 - self.alpha_slow) * self.ema_slow dif self.ema_fast - self.ema_slow self.dea self.alpha_signal * dif (1 - self.alpha_signal) * self.dea dif self.ema_fast - self.ema_slow return dif, self.dea, 2.0 * (dif - self.dea)初始化时第一根K线的 DIF 算出来是 0DEA 也从 0 开始递推所以前几个值和通达信会有微小差异跑几十根K线之后就能收敛到一致。实际工程里一般会先加载历史数据用上面的macd()全量跑一遍建立状态再用IncrementalMacd接收实时数据这样既保证精度又保证速度。3. 股票软件里的 MACDK线图背后的绘图逻辑3.1 通达信公式与参数开关国内行情软件里通达信和同花顺的 MACD 其实就是这么几句公式语言DIFF: EMA(CLOSE,12) - EMA(CLOSE,26); DEA: EMA(DIFF,9); MACD: (DIFF-DEA)*2;CLOSE是收盘价序列EMA是软件内置的指数移动平均函数。可以看到公式把我前面写的 Python 逻辑完整封装了没有任何额外魔法。这里有个不显眼但很重要的行为软件里的 EMA 是对整个行情数据序列从头递推的不是只拿你当前屏幕上那几十根K线重新算。所以你在通达信里缩放大图、切换K线数量MACD 的数值不会变因为它只依赖行情数据本身的全部历史不依赖显示范围。这一点很多写量化脚本的新手会搞反——他从接口只拉了 100 根K线去算然后发现和软件显示不一致。3.2 红柱绿柱、金叉死叉的底层判定绘图逻辑其实非常直白柱状图的每一根柱子高度就是 MACD 值正值画在零轴上方为红柱负值画在零轴下方为绿柱。金叉的定义是 DIF 从下往上穿越 DEA也就是某个K线处 DIF 还小于 DEA到了下一根K线 DIF 大于 DEA。死叉则完全反过来DIF 从上往下穿越 DEA。程序里做交叉判断并不是靠“画两条线看交点”而是逐根K线比较 DIF 和 DEA 的大小关系记录相邻两根符号变化。柱状图也一致相邻两根柱子从负变正就是零轴上方出现第一根红柱这个时间点通常和 DIF 上穿 DEA 同一天有时差一天因为柱用的是 DIF 减 DEA本质上就是 DIF 和 DEA 的距离。3.3 为什么不同软件显示数值不一样这个问题每隔一阵就有人问一次明明是同一只股票同一天为什么 App A 和 App B 的 MACD 柱高不一样四个主要原因差异来源表现对策柱状图乘子不同柱高成倍数差异金叉死叉位置一致看形态别直接比数值EMA 初始值不同前几十根K线数值不同之后收敛忽略早期数据复权基准不同除权除息后价格序列不同指标连锁变化统一用前复权行情源不一致停牌处理、除权因子、数据更新时间有差异固定一个数据源我最常建议的做法是跨平台比较时只看“形态”不要直接要求数值相等做量化回测时则从头到尾锁定一个数据源、一种复权方式保证内部一致性比跟某个软件对齐更重要。4. 进阶算法用程序识别双底、顶背离与三背离很多人在搜索“MACD 算法”的时候其实真正想要的是“用 Python 判断 MACD 双底、顶背离、三背离”这类形态识别代码。形态识别没有统一公式不同人的定义略有差异但核心思路是一致的先找价格和指标的局部极值点再比较“价格创新低/新高”和“指标是否同步创新低/新高”。4.1 局部极值点程序怎么找到“底”和“顶”用滑动窗口找局部高点/低点是最朴素也最稳定的做法。对第 i 根K线看它左边 window 根、右边 window 根如果它的价格比左右各 window 根都高记为局部高点比左右都低记为局部低点def find_pivots(close, window5): highs [] lows [] for i in range(window, len(close) - window): left close[i - window:i] right close[i 1:i 1 window] if close[i] max(left) and close[i] max(right): highs.append(i) if close[i] min(left) and close[i] min(right): lows.append(i) return highs, lowswindow就是灵敏度参数window 小能找到更多细节极值window 大只认比较明显的波段高低点。实际中常用 5 到 10。这个步骤是后面所有背离判断的地基极值找得不对后面的结论自然错。4.2 底背离与双底的完整判断底背离的经典定义是股价创了新低但 DIF 没有再创新低。程序实现时只需要拿“最近两个价格低点”做比较def check_bottom_divergence(close, dif, lows): if len(lows) 2: return False p1, p2 lows[-2], lows[-1] # 价格创新低但指标没有创新低 → 底背离 if close[p2] close[p1] and dif[p2] dif[p1]: return True return False双底的判定比单次底背离更严格通常要求满足三个条件第一个低点和第二个低点价格接近第二个低点没有跌破第一个太多比如跌幅控制在 3% 以内第二个低点对应的 DIF 明显高于第一个低点对应的 DIF说明下跌动能在衰减随后出现金叉作为确认信号。把这三个条件翻译成代码就是对上面check_bottom_divergence再加一个价格幅度限制和一个金叉校验。这种“双底抬高 金叉确认”的组合是搜索热词里“Python 源代码 MACD 双底 高 低”背后真正想要的那份东西。4.3 三背离把“背离次数”写成公式热词里还有个“MACD 三背离公式”。所谓三背离就是连续三次出现背离现象通常指价格连续三次创新低、DIF 的低点却一次比一次高。代码上可以写一个循环计数器def count_bottom_divergences(close, dif, pivot_lows): cnt 0 for i in range(1, len(pivot_lows)): p_prev pivot_lows[i - 1] p_cur pivot_lows[i] if close[p_cur] close[p_prev] and dif[p_cur] dif[p_prev]: cnt 1 return cnt返回 3 就是三背离。顶背离就把大小比较反过来检查价格创新高、DIF 高点走低。背离次数越多通常代表趋势动量衰竭越明显但要注意背离之后完全可能不反转而是通过横盘震荡让指标慢慢跟上价格也就是“用时间换空间”。所以这一类信号永远是概率参考不能说出现了三背离就断定行情必然反转。4.4 形态识别参数的局限写了上面的代码之后必须泼一盆冷水同样的行情数据window 取 5 和取 10识别出来的极值点完全不同最终背离结论也可能不同。很多开源代码把 window 写死成 5结果换一只股票、换一个周期出来的信号就乱七八糟。我自己的处理方式是window 按品种波动率动态调整或者先做一次去噪再找极值拿到信号之后还要用成交量、均线结构做二次过滤。形态识别这东西参数敏感度非常高千万别以为跑出一个“三背离”就是稳赚信号。5. 实际项目中 MACD 算法最容易踩的五个坑5.1 除权除息与复权数据这是最隐蔽也最致命的一个坑。某只股票 10 派 5 元除权日价格直接跳空低开一大截EMA 对这种价格断层非常敏感MACD 瞬间被拉出一个尖峰金叉死叉信号可能完全是除权造成的“假动作”。规避办法只有一个必须使用复权数据。用前复权时要注意历史数据会随着每次新的分红送转而整体变化所以回测中途数据会被“更新”用后复权时历史价格稳定但当前最新价不是真实价格直接拿后复权价格计算买入卖出会有偏差。我的习惯是做策略研究和回测统一用前复权数据源锁定一家回测结果只做内部对比。5.2 分钟级 MACD 不是日线 MACD 的切片有不少人问能不能用日线 MACD 推算出 60 分钟 MACD答案是不行。分钟级 MACD 是把每一根分钟K线当作一个独立数据点重新算 EMA12、EMA26 和 DEA9而不是把日线指标按时间切碎。默认参数 12、26、9 本来就是按日线周期设计的放到分钟图上时每个数字代表“多少根分钟线”比如 60 分钟图上的 EMA12 只代表最近 12 根 60 分钟K线覆盖大约 3 个交易日敏感度比日线高得多。如果想让分钟图反映类似日线的节奏可以手动把参数放大比如 12 小时/26 小时/9 小时这样的等效配置。5.3 数据长度不足导致指标不稳定EMA 是递归算法初始值的影响需要时间衰减。如果一只次新股只有四五十根日K线慢线 EMA26 还在初始值影响很大的温吞阶段指标数值和最终收敛值差不少。经验值是至少要有慢线周期的两倍以上K线也就是日线 MACD 至少准备 60 根以上建议 200 根左右再谈信号可靠性。程序里可以加一个“跳过热身区”的开关比如前 2×slow 根K线只算不算信号。5.4 pandas ewm 和股票软件对不上前面提过adjust参数这里再给一个验收方法拿同一只股票最近 50 根日K线分别用手写ema()、ewm(span12, adjustFalse)、以及通达信里显示的值三组数字挨个对。如果前几十个值完全一致说明你的计算链路已经和软件对齐了如果刚开始对不上大概率就是adjust用了默认值或者表里那根数据源里包含了停牌、复权的额外处理。5.5 实时行情下的性能与状态管理实时场景最容易犯的错误是“每个 tick 都全量重算”。行情一分钟往往几十个 tick每次都拉几万根K线跑一遍 EMA性能是不可接受的。正确做法是用增量函数维护状态缓存ema_fast、ema_slow、dea三个变量。但还有第二个容易忽略的细节分钟线是不断拼接的同一根 60 分钟K线在收盘前价格一直在变中间任何一次 update 得到的 MACD 都只是“临时值”真正定型要等这根分钟K线收盘。所以增量更新通常用最新收盘价刷新显示但信号计算必须等K线收盘后再触发否则同一个信号可能会在一根分钟线里反复消失又出现。写到这里MACD 从数学定义、源码实现、软件画图逻辑到工程坑点基本都过了一遍。我自己的体会是MACD 这类指标最大的价值不是某个金叉信号有多神而是它把“趋势加速度差异”这种抽象概念变成了可以计算、可以对比的数值结构。当你亲手用 Python 算出 DIF、DEA、柱三条曲线再回头看行情软件里那张图你会自然地把它当成一个普通技术工具而不是什么玄学指南。
返回列表