ta / 数值验证
ta 的指标算得对吗:RSI、MACD、布林带、ATR 的三路对照
「和别的库对不上」是引入 ta 之后最常见的第二个问题(第一个是 NaN)。 但绝大多数情况下不是 bug,而是平滑方式、标准差自由度、播种方式这三件事不同。
这一页不用「一般来说」这类说法:本站在同一份 400 行 OHLCV 上,
用 ta 的实现与自己手写的公式逐点做差,把实测偏差写出来。
结论先给:RSI 走 Wilder 平滑、布林带用 ddof=0、MACD 严格等于两条 EMA 之差、ATR 是 Wilder 递归。
差异来源
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 前导 NaN | 13 行 | 窗口 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.224063 | ta 与 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 | — | 就是两条线的差,没有额外处理 |
| ATR | vs 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 建一个可复现的验证基线
搭一个可复现的验证基线(本站用的就是这套)
- 固定数据:用固定随机种子的合成行情,或把一段真实行情导出成 CSV 存起来——不要每次现从网上取数,网络数据会变。
- 固定环境:把
ta、numpy、pandas三个版本一起钉住(本机是 0.11.0 / 2.4.6 / 3.0.6)。 - 记录三列输出:指标值、前导 NaN 行数、第一个有效日期——只比最后一行的值会掩盖起步阶段的差异。
- 两种口径各算一次:对同一指标分别用 ta 与自写公式算,输出最大绝对差与平均绝对差两个数。
- 把结论写成断言:例如
assert (ta_rsi - wilder).iloc[50:].abs().mean() < 0.1,这样升级依赖后能立刻发现行为变化。
本站的验证样本是 400 行、固定种子 20260101 的合成 OHLCV;原始读数保存在研究目录的 _evidence/_runtime_corrected.json,本页所有数字都从那份 JSON 里取,没有手工抄写。
FAQ
关于 ta 指标数值的常见问题
ta 的 RSI 和 TradingView / 同花顺 显示的不一样,是 ta 错了吗?
ewm(alpha=1/14, adjust=False)),本站实测它与同口径的 Wilder 实现平均差 0.050526;如果你对比的是「简单移动平均版 RSI」,第 50 行之后的平均差是 5.735240。差异来自平滑方式,不是精度问题。另外行情源口径(复权、时区)不同也会造成差异。以官方实现与你实际使用的数据为准。布林带的上下轨为什么和别人差一点点?
ddof=0),本站实测上轨与 SMA20 + 2×σ(ddof=0) 的最大绝对差是 0.000000000,而与 ddof=1 的版本差 0.238858。很多自写实现默认用样本标准差,所以会有稳定的小偏差。ATR 的第一个值是怎么来的?
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 版本变化吗?
ewm 或 rolling 的边界行为。建议按本页的「验证基线」做法把关键断言写进测试,升级后立刻能发现变化。