← 返回目录
p2.1动手⏱ 约 15 分钟

趋势指标:SMA / EMA / MACD

手写、对拍、再审判一次金叉

🔎 最后验证 2026-07📚 来源:对拍、速度与金叉统计审判于 2026-07-31 在 ECS 实测(50 只沪深300 成分股 + 指数,2010-01-04~2026-07-29,199150 行),输出见 code/outputs/stdout.txt🧰 pandas 3.0.5、numpy 2.5.1、scipy 1.18.0
为什么学这个

两个人都说"我用 EMA(12)",他们算出来的可能不是同一条线。

实测:同一份数据、同一个周期参数,我的手写实现与 pandas 默认的 ewm(span=12),最大绝对差 8.72 点(相对 0.259%)。换一个参数(adjust=False),差距缩到 0.88 点。

三条线都"对",只是初始化方式不同。而如果你拿 A 平台的信号去验证 B 平台的策略,你会把实现差异误当成策略差异——这类 bug 极难查,因为代码两边都没错。

这一节做三件事:手写三个趋势指标、与库对拍、然后对最流行的 MACD 金叉做一次横截面统计审判。

💡 打个比方

两个厨师都说"加一勺盐"。一个用的是茶匙,一个用的是汤匙。

菜谱没写错,两个人也都照做了。但做出来的是两道菜。

指标也一样:名字相同、参数相同,实现细节不同,结果就是两条线。

一、手写:先写清楚定义

def sma(s, n):
    """前 n 项无定义 —— 不要用 0 或前值填充,那是在造数据。"""
    return s.rolling(n, min_periods=n).mean()

def ema_recursive(s, n):
    """EMA_t = a*x_t + (1-a)*EMA_{t-1},a = 2/(n+1);初值取前 n 项算术平均。"""
    a = 2 / (n + 1)
    out = np.full(len(s), np.nan)
    x = s.to_numpy(dtype=float)
    out[n - 1] = x[:n].mean()
    for i in range(n, len(x)):
        out[i] = a * x[i] + (1 - a) * out[i - 1]
    return pd.Series(out, index=s.index)

min_periods=n 那一行不是洁癖:如果用前 3 天的均值冒充 20 日均线,你就是在给自己造数据,而且这个错误会一直污染序列的开头,恰好是回测开始的地方。

二、对拍:SMA 谁写都一样,EMA 不是

对比最大绝对差最大相对差末值相对差
手写 SMA(20) vs rolling(20).mean()0.00e+00
手写 EMA(12) vs ewm(span=12, adjust=True)(pandas 默认)8.71700.2590%0.000000%
手写 EMA(12) vs ewm(span=12, adjust=False)0.88270.0252%0.000000%

三件事:

  1. SMA 没有歧义,谁写都一样
  2. EMA 有歧义:adjust=True(pandas 默认)在序列开头用的是加权平均的精确展开,adjust=False 用纯递推,而交易软件常用"前 n 项均值作初值"——三种都合理,前期能差 0.26%
  3. 末值完全一致——因为 EMA 的初值影响会随时间指数衰减
⚠️ 避坑

"末值一致"正是这个坑最阴的地方。

你随手打印最后几行对比一下,发现完全一样,于是放心了。但回测是从序列开头跑起的——开头那段差 0.26%,足以让金叉死叉的时点错开好几天,而且错在样本的最前面,影响会顺着净值曲线一路累积。

验证指标实现,要对拍整条序列的最大差,不是末值。

🔧 动手做:SMA 谁写都一样,EMA 不是(5 分钟)

python trend_indicators.py,把手写指标和 pandas 现成的对拍。

你会看到(实测):手写 SMA(20) 和 rolling(20).mean() 最大绝对差 0.00——谁写都一样。但 EMA 不是:同一条数据,ewm(adjust=True)adjust=False 给出不同的值,而很多库默认口径还不一样。

想明白:指标是定义,不是真理。SMA 只有一种算法,EMA 却有好几种(初值怎么设、adjust 开不开)。你直接 import 一个 EMA,可能和别人算的、和你回测里假设的根本不是一条线。用库没问题,但你得知道你用的是哪种口径——所以先手写一遍、对拍一次,你才知道现成的那个到底在算什么。

三、那为什么不直接用现成的库

这是个好问题,而且必须正面回答:pandas-ta、TA-Lib 提供 150+ 个现成指标,为什么这门课要手写?

答案分两半。

教学上必须手写

因为只有手写才暴露得出实现分歧。上面那 8.72 点的差异、下一节 RSI 两种平滑导致"上穿 70"次数相差 2.7 倍——直接调库,这些坑会被完全掩盖,而它们恰恰是你复现别人策略时最费时间的部分。

生产上应该用库,但上线前必须对拍

库经过更多人验证、边界处理更全。我们把手写实现与 pandas-ta 0.4.71b0 逐个对拍:

手写 vs pandas-ta最大绝对差相关系数结论
SMA(20)0.0000001.000000完全一致
EMA(12)0.0000001.000000完全一致
RSI(14)0.0000001.000000完全一致
MACD DIF0.0000001.000000完全一致
ATR(14)5.5685720.999966高度一致,平滑细节不同
BOLL 位置0.0490601.000000高度一致,ddof 取值不同
⚠️ 避坑

这张表里藏着一个比"一致"更重要的信息。

手写 EMA 与 pandas-ta 完全一致(差 0.000000),却与 pandas 原生 ewm(adjust=True) 差 8.72 点

原因:pandas-ta 用的是交易软件的惯例(纯递推 + 前 n 项均值作初值),而 pandas 原生 ewm 默认 adjust=True,用的是加权平均的精确展开。

两个都叫 EMA,两个都是对的,而它们的默认行为不同。

所以规则不是"用库就安全",而是:换任何一个库/平台,都要先对拍一遍,并把差异量级记进文档。这和 P4 章「自研引擎必须与 backtrader 对拍」是同一条方法论。

本课的分工:课程内手写,让你看清定义与分歧;真做项目时用 pandas-ta,但把 _shared/lib_crosscheck.py 挂进 CI,每次升级库都跑一遍。

四、向量化不是风格问题,是能力问题

纯 Python 循环 7.0ms | pandas 向量化 0.2ms | 快 45 倍(4023 个点)

单条序列差 45 倍,看起来无所谓。但当你要做的是:50 只标的 × 20 组参数 × 6 个持有期的检验矩阵——这个倍数决定你一天能试几次,也就决定你的研究迭代速度

五、审判:MACD 金叉到底有没有用

不看图说话,做横截面统计检验:50 只标的、7518 次金叉信号,把"金叉后 N 日收益"与"全样本随机时点的同期收益"比。

持有期金叉后均值基准均值差值t 值p 值
1 日+0.139%+0.055%+0.084pp3.000.0027
3 日+0.292%+0.167%+0.125pp2.510.0120
5 日+0.371%+0.280%+0.091pp1.410.1593
10 日+0.673%+0.558%+0.115pp1.240.2146
20 日+1.300%+1.128%+0.172pp1.260.2094
60 日+3.740%+3.432%+0.307pp1.230.2183

胜率:金叉后为正 50.6%,全样本 49.7%

诚实的读法:

  • 方向是对的——六个持有期的差值全部为正,不是随机乱跳
  • 短期(1–3 日)在 5% 水平上显著,幅度约 0.08–0.13 个百分点
  • 5 日以后不显著,信息很快被消化掉
  • 0.084pp 是什么概念:回想 p0.2 的死亡线——单边成本 18.8bp 就能让一个策略跑输躺平。1 日 +0.084pp = 8.4bp,连一次双边交易成本都盖不住

所以结论不是"金叉没用",而是:它携带的信息量,小于交易它所需的成本。

这比"金叉是玄学"更准确,也更有用:它告诉你该往哪个方向改进——要么降低成本,要么找信息量更大的信号,而不是继续调 MACD 的参数。

⚠️ 避坑

这张表报了 6 个持有期,等于做了 6 次检验。

按 5% 显著性水平,即使金叉完全无效,平均也会有 0.3 次"显著"。我这里出现了 2 次。

那么这 2 次是真的吗?单看 p 值回答不了这个问题——需要对多重检验做校正。这笔账 P3.4 专门算,而且会把 P2 整章累计的检验次数一起清算。

从现在开始,养成一个习惯:每报一个 p 值,同时报你一共试了多少次。

❓ 测验
你把某平台的 MACD 策略搬到自己的代码里,末值对得上,但回测收益差了 15%。最可能的原因?
✏️ 填空
验证指标实现是否一致,要对拍整条序列的 ___ 差,而不是只看末值。

📌 免责:本课为技术教学,统计结果为历史模拟,不构成投资建议;标的为沪深300 成分股按代码序取样,非精选。

✅ 小结

三件事:

  1. SMA 没歧义,EMA 有:与 pandas 默认实现最大差 8.72 点(0.259%),而末值完全一致——所以要对拍整条序列
  2. 向量化快 45 倍:决定你一天能试几次
  3. MACD 金叉:方向对、短期显著、但幅度太小——1 日 +0.084pp = 8.4bp,连一次双边成本都盖不住

最后那条结论的形式,是这一章反复要用的:不说"有用/没用",说"信息量有多大、够不够付成本"。

下一节审判三句更流行的口诀:RSI 超过 70 该卖、低于 30 该买、KDJ 的 J 值破 0 是极度超卖。其中一条的实测结果,和口诀说的方向完全相反

下一节 → 动量指标:RSI 与 KDJ,以及那三句口诀
🔎 来源与核验· 4 条,点开核对
本节每个关键论断都对应一个可追溯的来源 —— 这是本课程"靠谱、不过时"的底线。
「手写 SMA(20) 与 pandas rolling(20).mean() 最大绝对差 0.00e+00;手写 EMA(12) 与 ewm(adjust=True) 最大绝对差 8.7170(相对 0.2590%)、与 ewm(adjust=False) 最大绝对差 0.8827(0.0252%),两者末值相对差均为 0.000000%」
📚 本节 code/trend_indicators.py,2026-07-31 于 ECS 实测(沪深300 指数 2010-01-04~2026-07-29,4023 个交易日),输出见 code/outputs/stdout.txt✓ 已核验 2026-07
「同一 20 日均线计算:纯 Python 循环 7.0ms,pandas 向量化 0.2ms,快 45 倍(4023 个点)」
📚 同上实测✓ 已核验 2026-07
「手写实现与 pandas-ta 0.4.71b0 对拍:SMA(20)/EMA(12)/RSI(14)/MACD DIF 最大绝对差均为 0.000000;ATR(14) 最大差 5.568572(相关 0.999966)、BOLL 位置最大差 0.049060(相关 1.000000)。手写 EMA 与 pandas-ta 一致,但与 pandas 原生 ewm(adjust=True) 差 8.72 点」
📚 _shared/lib_crosscheck.py,2026-07-31 于 ECS 实测,输出见 code/outputs/lib_crosscheck.txt✓ 已核验 2026-07
「50 只标的共 7518 次 MACD 金叉:1 日 +0.084pp(t=3.00,p=0.0027)、3 日 +0.125pp(p=0.0120)、5/10/20/60 日均不显著(p=0.16~0.22);胜率 50.6% vs 全样本 49.7%」
📚 同上实测(Welch t 检验,基准为全样本同期收益)✓ 已核验 2026-07
统计显著不等于可交易:本节 1 日 +0.084pp 折合 8.4bp,低于单次双边交易成本。多重检验校正见 P3.4。
智图软件的赞赏码
都看到这了,打个赏呗!
接下来 · p2.2
动量指标:RSI 与 KDJ
继续读下一节 →