2团
Published on 2026-05-20 / 3 Visits
0
0

用pandas计算K线的MACD_KDJ结果异常?问题出在ewm的首值处理

最近出于兴趣,自己写了个小工具拉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)

改动点:

  1. SMA初始化:前span个有效值取均值作为第一个EMA,而非直接用第一个收盘价
  2. 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的趋势方向与股价走势一致,符合。

以上是自己折腾量化时遇到的一个真实问题,整理出来供参考。如果你也遇到过类似的指标对不上的情况,欢迎交流。


Comment