ta / 数值验证

ta 的指标算得对吗:RSI、MACD、布林带、ATR 的三路对照

「和别的库对不上」是引入 ta 之后最常见的第二个问题(第一个是 NaN)。 但绝大多数情况下不是 bug,而是平滑方式、标准差自由度、播种方式这三件事不同。

这一页不用「一般来说」这类说法:本站在同一份 400 行 OHLCV 上, 用 ta 的实现与自己手写的公式逐点做差,把实测偏差写出来。 结论先给:RSI 走 Wilder 平滑、布林带用 ddof=0、MACD 严格等于两条 EMA 之差、ATR 是 Wilder 递归。

400 行实测样本三路对照偏差精确到小数核验 ta 0.11.0
RSIWilder α=1/14,与简单均值差 5.74 点
布林带SMA20 ± 2×总体标准差 σ(ddof=0)
MACDEMA12 − EMA26,实测误差 0.0
ATRWilder 递归,种子 = 前 14 根 TR 均值
依据本机手算对照实验结果(seo_reports/python-ta/_evidence/_runtime_corrected.json)整理的验证示意;非官方公式图。

差异来源

ta 与别的库对不上是什么原因:五处差异

差异来源具体表现为什么容易误判成 bug
平滑方式不同RSI 用 Wilder 平滑(ewm(alpha=1/window)),很多教科书和自写脚本用简单移动平均两者都是「RSI」,数值却来自不同公式;实测平均差 5.74 个点
标准差自由度不同布林带用 rolling().std() 的默认值 ddof=0(总体标准差),而不是常见的样本标准差 ddof=1差别只在小数第二位,肉眼看不出来,但会稳定存在
播种方式不同ATR 第一个有效值取「前 14 根 TR 的简单平均」,之后才转递归;RSI 从上到下用递归 EMA,没有单独的 SMA 播种起步阶段差异最大,几十行后收敛;只看开头会以为算法完全不对
价格数据口径不同复权方式、时区、是否含当日未收盘数据同一库同一参数,喂不同数据也会得到不同结果
图表库的口径不同有些界面为了好看会做 min_periods 调整或跳过预热期界面显示的值与 DataFrame 里的值不是一回事

排查顺序建议:先对齐数据口径,再对齐平滑方式,最后才怀疑实现。本站的结论只针对 ta 0.11.0 的默认参数;你传了不同 window 或改了数据口径后,请用本页的方法重跑一遍对照。

RSI 对照

ta 的 RSI 怎么算:用的是哪种平滑方式

# RSI 三路对照(同一份 400 行 close)
from ta.momentum import RSIIndicator

ta_rsi = RSIIndicator(close=close, window=14, fillna=False).rsi()

delta = close.diff()
gain, loss = delta.clip(lower=0), (-delta).clip(lower=0)

# A · Wilder 平滑(ta 用的就是这条)
wilder = 100 - 100 / (1 + gain.ewm(alpha=1/14, adjust=False, min_periods=14).mean()
                          / loss.ewm(alpha=1/14, adjust=False, min_periods=14).mean())

# B · 简单移动平均(很多教程写法)
simple = 100 - 100 / (1 + gain.rolling(14, min_periods=14).mean()
                          / loss.rolling(14, min_periods=14).mean())

print((ta_rsi - wilder).iloc[50:].abs().mean())   # 0.050526  -> 基本一致
print((ta_rsi - simple).iloc[50:].abs().mean())   # 5.735240  -> 明显不同
对照项实测值说明
ta 的 RSI 前导 NaN13 行窗口 14,第 14 行出第一个值
ta vs Wilder(全程最大差)18.994差异出现在起步阶段
ta vs Wilder(第 50 行之后最大差)1.468起步差异已经收敛
ta vs Wilder(第 50 行之后平均差)0.050526基本可认为两者是同一个算法
ta vs 简单移动平均(第 50 行之后平均差)5.735240这才是「对不上」的主因
第 100 行的三个值ta = 67.776126;Wilder = 67.805489;简单均值 = 63.224063ta 与 Wilder 差 0.029,与简单均值差 4.55

结论:ta 的 RSI 可以认为就是 Wilder RSI(ewm(alpha=1/14, adjust=False))。它与「简单均值版 RSI」在第 50 行之后仍平均相差 5.74 个点,这个量级足以让 70/30 的超买超卖判定完全反转。两种都不能说错,但必须知道自己在用哪一种。

布林带口径

ta 的布林带怎么算:用的是哪种标准差

# 布林带用哪种标准差?两个都算一遍就知道
from ta.volatility import BollingerBands
bb = BollingerBands(close=close, window=20, window_dev=2, fillna=False)

ma20   = close.rolling(20, min_periods=20).mean()
sd_ddof0 = close.rolling(20, min_periods=20).std(ddof=0)   # 总体标准差
sd_ddof1 = close.rolling(20, min_periods=20).std(ddof=1)   # 样本标准差

print((bb.bollinger_mavg() - ma20).abs().max())                        # 0.000000
print((bb.bollinger_hband() - (ma20 + 2*sd_ddof0)).abs().max())        # 0.000000
print((bb.bollinger_hband() - (ma20 + 2*sd_ddof1)).abs().max())        # 0.238858
对照项实测最大绝对差结论
中轨 vs SMA(20)0.000000000完全一致
上轨 vs SMA20 + 2σ(ddof=0)0.000000000完全一致——ta 用的是总体标准差
上轨 vs SMA20 + 2σ(ddof=1)0.238858对不上;差值虽小但恒定存在

这条差异最隐蔽:两种标准差只差一个 n/(n-1) 因子(20 窗口时约 1.0256 倍),在价格 100 附近只影响第二个小数位。但如果你在跟另一个系统做逐点对账,它会一直出现在误差列表里,而且下单阈值一旦卡在边界上就可能造成不一致。

MACD 与 ATR

ta 的 MACD 和 ATR 与手写实现差多少

指标实测对照最大绝对差说明
MACD 线vs EMA12 − EMA26(ewm(span=…, adjust=False))0.000000000完全一致,ta 的 MACD 就是两条 EMA 之差
MACD 信号线vs 对 MACD 线再做 EMA9—前导 NaN 33 行(26 + 9 − 2),比 EMA26 的 25 行更长
MACD 柱(macd_diff)vs macd − macd_signal—就是两条线的差,没有额外处理
ATRvs Wilder 递归(种子 = 前 14 根 TR 均值)0.000000000完全一致,可以照抄实现
ATR vs SMA(14) of TR两者不是同一个算法0.038390(末行)终值差 1.072195 vs 1.033805;越往后差别越小但不会归零

MACD 与 ATR 的对照结果是「完全一致」——这两条可以放心照实现复刻。但别忘了 ATR 的表头是 0 而不是 NaN(np.zeros 初始化),这一点在预热与 NaN 页已经单独讲过:它会让「数值对照通过」和「可直接使用」变成两件事。

排查顺序

指标对不上时怎么排查:六步顺序

顺序检查什么怎么查常见结果
1数据是否同一份比对两边用的收盘价序列长度与首尾值复权口径或时区不同——先统一
2窗口参数是否一致print(ta_rsi.index[13]) 看第一个有效日期是否相同有人把 window 从 14 改成 20 没告诉你
3平滑方式是否一致按本页的 Wilder vs 简单均值两种都算一次差 5 个点以上就是平滑方式不同
4标准差自由度对布林带做 ddof=0/1 双算差在小数第二、三位就是它
5预热期怎么处理看两边的 NaN 行数是否相同有人提前 fillna 或丢掉了头部
6是否被图表库加工过把界面显示值与 DataFrame 值逐点比对界面做了平滑或四舍五入

这个顺序能覆盖九成以上的「对不上」。前两步属于数据问题,第三到五步属于算法口径问题,最后一步属于展示层问题。只有六步都排除后,才需要去看 ta 的实现代码。想把数据口径先对齐,看数据契约;想在别的库上做同样对照,看三库对比;想把这些数当成特征用,先读回测边界。

验证基线

怎么给 ta 建一个可复现的验证基线

搭一个可复现的验证基线(本站用的就是这套)

  1. 固定数据:用固定随机种子的合成行情,或把一段真实行情导出成 CSV 存起来——不要每次现从网上取数,网络数据会变。
  2. 固定环境:把 ta、numpy、pandas 三个版本一起钉住(本机是 0.11.0 / 2.4.6 / 3.0.6)。
  3. 记录三列输出:指标值、前导 NaN 行数、第一个有效日期——只比最后一行的值会掩盖起步阶段的差异。
  4. 两种口径各算一次:对同一指标分别用 ta 与自写公式算,输出最大绝对差与平均绝对差两个数。
  5. 把结论写成断言:例如 assert (ta_rsi - wilder).iloc[50:].abs().mean() < 0.1,这样升级依赖后能立刻发现行为变化。

本站的验证样本是 400 行、固定种子 20260101 的合成 OHLCV;原始读数保存在研究目录的 _evidence/_runtime_corrected.json,本页所有数字都从那份 JSON 里取,没有手工抄写。

FAQ

关于 ta 指标数值的常见问题

ta 的 RSI 和 TradingView / 同花顺 显示的不一样,是 ta 错了吗?
大概率不是。ta 的 RSI 用 Wilder 平滑(ewm(alpha=1/14, adjust=False)),本站实测它与同口径的 Wilder 实现平均差 0.050526;如果你对比的是「简单移动平均版 RSI」,第 50 行之后的平均差是 5.735240。差异来自平滑方式,不是精度问题。另外行情源口径(复权、时区)不同也会造成差异。以官方实现与你实际使用的数据为准。
布林带的上下轨为什么和别人差一点点?
ta 的布林带用总体标准差(ddof=0),本站实测上轨与 SMA20 + 2×σ(ddof=0) 的最大绝对差是 0.000000000,而与 ddof=1 的版本差 0.238858。很多自写实现默认用样本标准差,所以会有稳定的小偏差。
ATR 的第一个值是怎么来的?
按 ta 源码:先用 np.zeros 建数组,第 window-1 位填「前 window 根 TR 的简单平均」作为种子,之后按 atr[i] = (atr[i-1]×(w-1) + tr[i]) / w 递归。本站用同样的递归复算,与 ta 输出的最大绝对差是 0.000000000,完全一致。
MACD 用的是什么均线?
指数移动平均,且 adjust=False。本站实测 ta 的 macd() 与 EMA12 − EMA26 的最大绝对差为 0.000000000。信号线是对 MACD 线再做一次 EMA9,因此信号线的前导 NaN 比 MACD 线更长(本机实测 33 行 vs 25 行)。
这些数值会随 ta 版本变化吗?
会有可能。ta 的代码自 2023-11 的 0.11.0 之后没有功能改动(本机采集时的 GitHub 数据),但依赖的 pandas / numpy 在升级时可能改变 ewm 或 rolling 的边界行为。建议按本页的「验证基线」做法把关键断言写进测试,升级后立刻能发现变化。
能不能直接照抄 ta 的公式到自己项目里?
代码许可是 MIT,允许使用与修改(以仓库 LICENSE 原文为准)。但要复制的是口径而不只是代码:平滑方式、标准差自由度、播种规则这三件事任何一样不同,结果都会长期对不上。所以正确的做法是先按本页把口径记录下来,再动手复刻。
为什么我算出来的偏差比你这里大很多?
本页的偏差是在一份 400 行合成行情上测的,偏差大小与样本的价格波动特征强相关。如果你的数据里有跳空、停牌、极端涨跌,起步阶段的最大偏差会比这里更大。重点看「第 50 行之后的平均差」这一列,它才是判断两个实现是否同一算法的依据。

接下来读哪一页?

单个指标验证完,下一个问题是「一次算 43 个会怎样」。批量特征工程页给出 86 列的完整清单和几个隐蔽开关。