最近出于兴趣,自己写了个小工具拉A股数据,顺带把MACD和KDJ都算了一遍,打算用来做简单的量化选股。代码跑起来了,数据也有了,然而拿去和通达信比对的时候发现根本对不上——而且偏差还在慢慢变大,完全不像随机误差。排查了一圈,最后发现问题处在没有遵守行业的默认设置。
这篇文章就是把这个排查过程整理下来,给同样在折腾量化的朋友做个参考,省得踩同样的坑。
MACD 和 KDJ 是什么
在讲问题之前,简单介绍一下这两个指标,方便后面理解偏差是怎么产生的。
MACD(移动平均收敛发散指标)
MACD核心是用两条不同周期的指数移动平均线(EMA)之差来捕捉趋势动量。计算链条是这样的:
收盘价序列
-> EMA(12) 快线(12日指数均线)
-> EMA(26) 慢线(26日指数均线)
DIF = EMA(12) - EMA(26) <- 快慢线之差,反映短期动量
DEA = EMA(DIF, 9) <- DIF的9日均线,起平滑作用
MACD = 2 × (DIF - DEA) <- 柱状图,放大差异便于观察
金叉(DIF上穿DEA)通常被视为买入信号,死叉(DIF下穿DEA)则是卖出信号。这是MACD最常见的用法。
KDJ(随机摆动指标)
KDJ衡量的是当前价格在近N日(通常9日)高低价区间里的相对位置,公式是:
RSV = (今日收盘 - N日最低) / (N日最高 - N日最低) × 100
Kₜ = ⅔ · Kₜ₋₁ + ⅓ · RSVₜ <- RSV的指数平滑,初始值K₀ = 50
Dₜ = ⅔ · Dₜ₋₁ + ⅓ · Kₜ <- K的再次平滑,初始值D₀ = 50
Jₜ = 3K - 2D <- 放大KD之间的背离
J值超过100通常认为超买,低于0则超卖。
1. 发现问题
我写完指标计算之后,直觉上就觉得应该和主流软件对一下,毕竟这种东西要是算错了,后续选股信号全是垃圾。挑了贵州茅台(600519)作对比,因为消费基金我亏得太多了,茅台又是消费龙头,必须是它了。
一比就看出问题了:
| 日期 | 收盘价 | 我算的 DIF | 通达信 DIF | 偏差 |
|---|---|---|---|---|
| 2026-04-21 | 1409.50 | -3.21 | -5.86 | +2.65 |
| 2026-05-11 | 1354.03 | -15.80 | -20.42 | +4.62 |
偏差不是固定的,而是随时间在持续扩大。这说明不是哪天的数据抄错了,而是计算逻辑本身有系统性偏差。基于这个DIF产生的金叉死叉信号,时间上会有偏移,完全不能用。
2. 排查过程
2.1 先排除数据问题
最先想到的是:会不会是K线数据本身就不对?把数据库里的前复权收盘价逐日和通达信比对了一遍,完全一致。数据没问题,问题在计算。
2.2 找一个可靠的参照
光知道"我的和通达信不一样"还不够,得找一个带完整中间值的参照,才能定位到底哪一步算错了。我从Yahoo Finance拉了茅台54个交易日的日线,有完整的DIF、DEA、MACD、K、D、J,用这批数据作为后续比对的基准。
2.3 从最底层开始查
MACD的计算是一个链条,任何一环出问题都会向后传递。既然DIF出错了,就从最底层的EMA开始检查,看EMA(12)和EMA(26)各自算出来是什么。
3. 找到根因
3.1 问题出在哪里
当时写EMA的时候,直接用了pandas的ewm,这就是指数移动平均:
# 修改前
def _ema(series: pd.Series, span: int) -> pd.Series:
"""Exponential moving average, equivalent to TA-Lib EMA."""
return series.ewm(span=span, adjust=False).mean()
代码简洁,pandas也是公认的数据处理标准库,能有什么问题?但是我忘记处理第一个数值如何初始化。
3.2 两种初始化方式的差异
pandas ewm(adjust=False) 的做法:
EMA₀ = X₀ <- 直接把第一个收盘价当 EMA 起点
EMA₁ = (1-α)·EMA₀ + α·X₁
EMA₂ = (1-α)·EMA₁ + α·X₂
...
通达信 / 同花顺的做法(也是国内绝大多数平台的标准):
EMA₁₁ = SMA(close[0:12]) <- 先算前12天的简单均值作为起点
EMA₁₂ = (1-α)·EMA₁₁ + α·close₁₂
...
差别就在第一步:pandas直接用第一个收盘价冷启动,而通达信用前N天的简单均值(SMA)作为初始值。
看起来只是第一个值的差异,但影响会持续很久。EMA(12) 的平滑系数 α = 2/(12+1) ≈ 0.154,每一步只会把误差修正15%左右,大约要20个周期才能让两种初始化方式的结果逐渐靠拢。而MACD正好依赖 EMA(12)和EMA(26)的差值,两者各自有初始化偏差,相减之后偏差叠加,DIF就出问题了。
3.3 用一个小实验验证
import pandas as pd
import numpy as np
data = pd.Series([100.0]*5 + [110.0]*5 + [120.0]*5)
# pandas ewm,首值直接用第一个数
ema_pandas = data.ewm(span=12, adjust=False).mean()
# SMA 初始化,标准做法
def ema_sma_init(series, span):
alpha = 2.0 / (span + 1)
result = np.full(len(series), np.nan)
sma = series.iloc[:span].mean()
result[span - 1] = sma
e = sma
for i in range(span, len(series)):
e = e * (1 - alpha) + series.iat[i] * alpha
result[i] = e
return pd.Series(result, index=series.index)
ema_correct = ema_sma_init(data, 12)
print(f"pandas ewm EMA[11] = {ema_pandas.iloc[11]:.4f}") # 109.73
print(f"SMA init EMA[11] = {ema_correct.iloc[11]:.4f}") # 107.50
起点就差了2.23,后续每一个值都会有偏差,而且短期内无法收敛。
3.4 pandas为什么这么设计?
pandas的ewm(adjust=False) 本质上是个"在线算法"——适用于流式数据,不知道历史有多长,所以直接用第一个值启动,在统计上是合理的。但金融工程里MACD的行业惯例是SMA初始化,通达信、同花顺、Wind等国内平台全部遵循这个不成文规定。两者在数学上都没有"错",只是服务不同的场景和标准。
4. 修复
知道了问题,修起来不复杂——把_ema函数改成显式的SMA初始化:
# 修改后
def _ema(series: pd.Series, span: int) -> pd.Series:
"""EMA with SMA initialization over the first `span` valid values.
Standard MACD convention: the first EMA value is the SMA of the first
`span` data points. This matches TongHuaShun / TongDaXin.
"""
alpha = 2.0 / (span + 1)
result = np.full(len(series), np.nan)
# 收集前 span 个有效值,取 SMA 作为初始 EMA
valid_count = 0
init_idx = -1
sma_sum = 0.0
for i in range(len(series)):
v = series.iat[i]
if np.isnan(v):
continue
sma_sum += v
valid_count += 1
if valid_count == span:
init_idx = i
break
if init_idx < 0:
return pd.Series(result, index=series.index)
result[init_idx] = sma_sum / span # SMA 作为首个 EMA 值
# 从下一个值开始递推
ema = result[init_idx]
for i in range(init_idx + 1, len(series)):
v = series.iat[i]
if not np.isnan(v):
ema = ema * (1 - alpha) + v * alpha
result[i] = ema
return pd.Series(result, index=series.index, dtype=np.float64)
改动点:
- SMA初始化:前
span个有效值取均值作为第一个EMA,而非直接用第一个收盘价 - NaN安全:跳过序列中的NaN——这在计算DEA时很关键,因为DIF的前25个位置是 NaN(EMA(26) 需要26个数据点才能产出第一个值)。
性能方面,在400个数据点上显式循环大约0.5ms,pandas ewm大约0.05ms。日批处理100只股票,额外开销完全可以接受。
5. KDJ呢?
排查MACD的时候顺手也验证了KDJ。KDJ同样用到了ewm:
# KDJ 里的 ewm 用法
first_valid = rsv.first_valid_index()
if first_valid is not None:
loc = rsv.index.get_loc(first_valid)
rsv.iat[loc] = 50.0 # 手动把初始值设成 50
k = rsv.ewm(alpha=1.0 / 3.0, adjust=False).mean()
d = k.ewm(alpha=1.0 / 3.0, adjust=False).mean()
这里pandas的首值初始化行为刚好是对的——KDJ的行业约定就是K₀ = D₀ = 50,手动把第一个有效RSV位置填成50,然后让ewm从这个值开始递推,结果和通达信完全一致,不需要改。
所以同样是 ewm(adjust=False),在 MACD 里是错的(应该 SMA 初始化),在 KDJ 里是对的(初始值本来就是 50)。关键是要理解每个指标自己的业务约定,不能无脑套用同一套写法。
6. 验证结果
修完之后,用茅台54个交易日的数据逐字段比对:
| 日期 | 计算值 | 参考值 | 误差 |
|---|---|---|---|
| 2026-04-16 DIF | 0.7833 | 0.7833 | 0.0000 |
| 2026-04-21 DIF | -5.8573 | -5.8573 | 0.0000 |
| 2026-05-11 DIF | -20.4208 | -20.4208 | 0.0000 |
误差归零,KDJ的K、D、J同样全部对齐。
另外做了几个基本合理性检查:
BAR = 2 × (DIF - DEA):DIF > DEA时BAR > 0,反之BAR < 0,符合;J = 3K - 2D:逐行验证恒等式成立,符合;- K、D的趋势方向与股价走势一致,符合。
以上是自己折腾量化时遇到的一个真实问题,整理出来供参考。如果你也遇到过类似的指标对不上的情况,欢迎交流。