100% FreeNo Signup Required

Indicator Formulas, Cross-Checked

Every formula below was implemented independently in NumPy from its definition, then computed again with pandas_ta 0.4.71b0 on 750 real AAPL bars, and the two compared point by point. Agreement is the evidence that the formula on this page is the formula a library actually implements.

Finding 1 — a correct RSI still needs ~117 bars

Wilder's smoothing is recursive, so the seed value decays rather than disappearing after the window. Two correct implementations that seed differently took 117 bars to agree to within 0.01, and about 184 to match to machine precision. If your RSI is right and still disagrees with your platform, this is usually why — not the formula.

Finding 2 — pandas_ta's Bollinger bands use the sample standard deviation

The bands were computed both ways against the same library output. Only ddof = 1 matched, so pandas_ta's bands sit slightly wider than Bollinger's own population-form definition.

population (Bollinger's own definition)4.24e+0does not match
sample (pandas .std default)2.54e-11matches

Relative Strength Index

length = 14

Reproduces pandas_ta
gain_t = max(close_t - close_{t-1}, 0)
loss_t = max(close_{t-1} - close_t, 0)
avg_gain = Wilder(gain, 14),  avg_loss = Wilder(loss, 14)
RS = avg_gain / avg_loss
RSI = 100 - 100 / (1 + RS)

What trips people up

The averages are WILDER's smoothing, not a simple or standard exponential average. Wilder's is an EMA with alpha = 1/n, while a standard EMA uses 2/(n+1) -- for n = 14 that is 0.0714 against 0.1333, nearly double. An RSI built on a plain EMA looks right, moves right, and disagrees with every charting platform. This is the single most common RSI implementation bug. Second, because Wilder's smoothing is recursive the SEED persists far longer than the 14-bar window suggests -- see the measured warm-up below.

Seriespandas_ta columnBars to ±0.01Max diff after warm-upLatest
RSIRSI_141177.48e-553.9971

Average True Range

length = 14

Reproduces pandas_ta
TR_t = max(high_t - low_t, |high_t - close_{t-1}|, |low_t - close_{t-1}|)
ATR = Wilder(TR, 14)

What trips people up

True range uses the PREVIOUS close, which is what makes it capture overnight gaps -- a high-minus-low range cannot. The smoothing is Wilder's again, not a simple moving average of TR.

Seriespandas_ta columnBars to ±0.01Max diff after warm-upLatest
ATRATRr_14exact from bar 16.22e-157.6528

Average Directional Index

length = 14

Reproduces pandas_ta
up = high_t - high_{t-1},  down = low_{t-1} - low_t
+DM = up   if up > down and up > 0   else 0
-DM = down if down > up and down > 0 else 0
+DI = 100 * Wilder(+DM, 14) / ATR,  -DI = 100 * Wilder(-DM, 14) / ATR
DX  = 100 * |+DI - -DI| / (+DI + -DI)
ADX = Wilder(DX, 14)

What trips people up

ADX is smoothed TWICE -- once into the DI values and again over DX -- so it needs roughly 2n bars before it means anything, and implementations differ in where they seed the second pass. Note also that only ONE of +DM and -DM can be non-zero on any bar; computing both from the raw moves without that exclusive rule is a frequent error.

Seriespandas_ta columnBars to ±0.01Max diff after warm-upLatest
ADXADX_141164.92e-519.9304
DMPDMP_14964.93e-527.7636
DMNDMN_14994.72e-525.4345

Moving Average Convergence Divergence

fast = 12, slow = 26, signal = 9

Reproduces pandas_ta
MACD  = EMA(close, 12) - EMA(close, 26)
signal = EMA(MACD, 9)
histogram = MACD - signal

What trips people up

These are standard EMAs with alpha = 2/(n+1), NOT Wilder's -- the opposite of RSI and ATR. The signal line is an EMA of the MACD line, not of price, and how its warm-up is seeded is where implementations diverge slightly in the first few dozen bars.

Seriespandas_ta columnBars to ±0.01Max diff after warm-upLatest
MACDMACD_12_26_9exact from bar 10-1.6424
MACDsMACDs_12_26_9exact from bar 10-0.9288
MACDhMACDh_12_26_9exact from bar 10-0.7135

Bollinger Bands

length = 5, stdev = 2 (pandas_ta defaults)

Reproduces pandas_ta
middle = SMA(close, n)
upper  = middle + k * stdev(close, n)
lower  = middle - k * stdev(close, n)

What trips people up

Which standard deviation? Bollinger's own definition and most charting platforms use the POPULATION form (ddof = 0); pandas_ta computes the bands with pandas' .std(), which defaults to the SAMPLE form (ddof = 1). That was measured here, not assumed -- the bands were computed both ways against the same library output, and only ddof = 1 matched. Because the sample form has the smaller divisor it gives a larger deviation, so pandas_ta's bands are slightly WIDER than the classical definition, and the gap grows as the window shrinks. NumPy's np.std defaults the other way (ddof = 0), so hand-rolled NumPy code and pandas_ta disagree by default. Note also that pandas_ta 0.4.x defaults to length 5, not the conventional 20.

Seriespandas_ta columnBars to ±0.01Max diff after warm-upLatest
BBLBBL_5_2.0_2.0exact from bar 12.97e-11298.8771
BBMBBM_5_2.0_2.0exact from bar 11.03e-11308.7280
BBUBBU_5_2.0_2.0exact from bar 12.54e-11318.5789

Stochastic Oscillator

k = 14, d = 3, smooth_k = 3

Reproduces pandas_ta
raw %K = 100 * (close - lowest_low(n)) / (highest_high(n) - lowest_low(n))
%K = SMA(raw %K, smooth_k)
%D = SMA(%K, d)

What trips people up

What most references call %K is already SMOOTHED -- the unsmoothed version is 'raw %K' or 'fast %K'. Reading the formula literally and comparing to a platform's %K compares two different series. The extremes also use the period's high and low, not closes.

Seriespandas_ta columnBars to ±0.01Max diff after warm-upLatest
STOCHkSTOCHk_14_3_3exact from bar 14.26e-1443.4120
STOCHdSTOCHd_14_3_3exact from bar 14.26e-1425.1495

Wilder's smoothing, in code

Three of the six indicators here depend on it, and it is the piece most published formulas leave out. It is an exponential average with alpha = 1/n, seeded with a simple average of the first n values:

import numpy as np

def wilder(x: np.ndarray, n: int) -> np.ndarray:
    """Wilder's smoothing (RMA). Not the same as an EMA of the same length."""
    out = np.full_like(x, np.nan, dtype=float)
    out[n - 1] = np.nanmean(x[:n])          # seed: simple average of the first n
    for i in range(n, len(x)):
        out[i] = out[i - 1] + (x[i] - out[i - 1]) / n
    return out

# Equivalent to an EMA with alpha = 1/n.  A standard EMA uses 2/(n+1):
#   n = 14  ->  Wilder alpha = 0.0714,  EMA alpha = 0.1333

The seed choice is what causes the long warm-up. Different libraries seed differently — some with a simple average, some by running the recursion from the first observation — and because the filter is recursive those choices take hundreds of bars to wash out, not fourteen.

Method

Each formula is implemented independently here in plain NumPy from its definition, then computed again with pandas_ta on the same real bars, and the two are compared over every point where both are defined. Agreement is evidence the published formula is the one the library implements; disagreement is published as a finding rather than hidden.

Agreement is judged on the converged region, with the warm-up measured separately. That distinction is the point: a single max-difference over the whole series conflates "the formula is wrong" with "the first bars are seeded differently", and those are completely different problems for someone implementing this. Tolerance: relative difference below 1e-6 against the series' own scale.

Regenerate with python -m pipeline.indicator_verify. Every figure is tagged with the library version that produced it, so it goes visibly stale rather than quietly wrong.

Frequently asked questions

Why does my RSI not match TradingView or my broker?+

Two reasons, both measured on this page. First, the averages must use Wilder's smoothing (an EMA with alpha = 1/n), not a standard EMA (alpha = 2/(n+1)) or a simple average — for n = 14 that is 0.0714 against 0.1333, so a plain EMA gives a plausible but wrong RSI. Second, Wilder's smoothing is recursive, so the starting value persists: an independent implementation and pandas_ta took 117 bars to agree to within 0.01 on real data. If you feed 100 bars in, your last value is still visibly wrong regardless of a correct formula.

How much history does an RSI or ADX need?+

Far more than the period suggests. Measured against pandas_ta on real daily bars, RSI took about 117 bars and ADX about 116 bars before the two implementations agreed to within 0.01, and roughly 180 before they matched to machine precision. The common advice of "n + 1 bars" or "2n bars for ADX" produces values that are defined but not yet correct.

Which standard deviation do Bollinger Bands use?+

pandas_ta computes the bands with pandas' .std(), which defaults to ddof=1 -- the SAMPLE standard deviation. Bollinger's own definition and most charting platforms use the population form (ddof=0). Both were computed against the same library output; the match is measured, not assumed.

Which indicators use Wilder smoothing and which use a normal EMA?+

RSI, ATR and ADX use Wilder's (alpha = 1/n). MACD uses standard EMAs (alpha = 2/(n+1)). Bollinger and Stochastic use simple moving averages. Mixing these up is the most common source of "my numbers are close but not equal" — the shapes look right, so the bug survives a visual check.

How were these verified?+

Each formula is implemented independently here in plain NumPy from its definition, then computed again with pandas_ta on the same real bars, and the two are compared over every point where both are defined. Agreement is evidence the published formula is the one the library implements; disagreement is published as a finding rather than hidden. Measured on 750 real AAPL daily bars (2023-08-23 to 2026-08-19) against pandas_ta 0.4.71b0.

Related

Measured on 750 AAPL daily bars (2023-08-23 to 2026-08-19) against pandas_ta 0.4.71b0, generated 2026-08-20T16:22:25+00:00. Indicator values are arithmetic on historical prices and are not investment advice or trading signals.