Why two libraries give different RSI, EMA and ATR values

RSI, EMA and ATR are recursive, so the seed, the smoothing constant, price adjustment and bar boundaries all change the number. Where libraries differ.

Usually because the formula agrees and everything around it does not. RSI and ATR are recursive: each value folds in the one before, so the result depends on how the recursion was seeded and how many bars preceded it. Libraries differ on that seed, on Wilder's 1/n smoothing versus the EMA's 2/(n+1), on default periods, on whether the prices were adjusted for splits and dividends, and on which trades each bar contains.

How it works

A simple moving average forgets. A 14-bar SMA is the mean of the last fourteen closes, and two libraries given the same fourteen numbers return the same answer to the last bit. Three of the most-used indicators are not built that way.

The EMA is a recursion: today's value is yesterday's value nudged toward today's price by a fixed fraction, alpha. RSI runs the same kind of recursion twice, once over the bar-to-bar gains and once over the losses, and divides one by the sum of both. ATR runs it over the true range — the largest of high minus low, high minus the previous close, and low minus the previous close. A comment in TA-Lib's ATR source notes that Wilder's averaging gives it an unstable period comparable to an EMA's, and the project's documentation puts the general point plainly: each output "depends on the previous one, seeded from the start of the data".

So every value on the chart carries a trace of every bar before it, back to the first bar the calculation was given. That is where the disagreements come from, and there are five of them, each independent of the others:

  1. The seed — what the recursion is started from on its first bar.
  2. The smoothing constant — which alpha a given period number turns into.
  3. The defaults — the period, the averaging type and the implementation a call gets when it names none.
  4. The input — raw closes, split-adjusted closes, or split-and-dividend-adjusted closes.
  5. The bars — which trades went into each open, high, low and close.

The first two are properties of the library and can be read in its source. The last two are properties of the data and are usually decided upstream of the code, by whoever built the bars. None of them is a bug. Each is a choice, and the choices are rarely printed next to the number.

The seed: where the recursion starts

A recursion needs a yesterday on its first day, and there are two common answers.

Seed with a simple average. TA-Lib sums the first period values, divides by the period, and starts the recursion from that. Its RSI does it for the first fourteen gains and losses, its ATR for the first fourteen true ranges, its EMA for the first period prices. The same convention appears in backtrader's ExponentialSmoothing, whose docstring says an arithmetic mean "is used as the seed value considering the first period values of data", and in pandas-ta-classic, whose rma and ema both compute an SMA of the first length valid values and place it at the seed position — the rma comment reads "SMA-seeded Wilder smoothing (matches TA-Lib)".

Seed with the first value. pandas' own ewm(..., adjust=False) is defined as y0 = x0: the first observation is the first average. The last release of the original pandas-ta, 0.4.71b0, builds its Wilder average as close.ewm(alpha=1/length, adjust=False).mean() with no SMA step in front of it, so its native RSI starts from the first gain and the first loss rather than from an average of fourteen. Its ATR, in the same release, does put an SMA seed in front of the true range, behind a presma option that defaults to on — two indicators in one package, seeded two ways. TA-Lib used to offer the same choice for its EMA as a MetaStock compatibility mode; the 0.8.1 release notes record that variant as removed, with the default behaviour unchanged.

A third answer is pandas' default, adjust=True, which is not a recursion at all but a weighted mean over every value so far, with weights (1 − alpha)^i normalised by their sum. It converges to the same place and takes a different path getting there.

How long the seed matters is arithmetic, not opinion. After k further bars the seed's weight in the average is (1 − alpha)^k:

SmoothingalphaSeed weight after 50 barsafter 100Bars until under 1%under 0.1%
Wilder, period 141/142.5%0.06%6394
EMA, period 142/150.08%0.00006%3349
EMA, period 302/313.6%0.13%70104
Wilder, period 201/207.7%0.6%90135

Two consequences follow. First, two libraries with different seeds agree only once enough bars have passed, and for a 14-period RSI that is roughly a hundred bars, not fourteen. Second, the same library disagrees with itself when it is given a different amount of history: an RSI computed from 1 January and one computed from 1 March will not match on 1 April, because they were seeded on different bars. A charting platform seeds on the first bar it loaded, which is a property of the chart's history, not of the formula.

TA-Lib makes the warm-up explicit. The first RSI value it returns is at index period, the first EMA at period − 1, and on top of that it has an unstable period per function — zero by default — which computes that many extra bars and throws them away before returning anything. The documented list includes EMA, RSI, ATR, NATR, ADX, CMO, KAMA and T3. In Python it is talib.set_unstable_period('RSI', 100). The setting follows the function into anything built on it, so the EMA setting also moves MACD and DEMA. The project recommends no value, saying only that "the larger the value, the later the first output", and its own illustration — three 20-bar EMAs of the same prices fed from bars 0, 10 and 20 — shows them settling within an acceptable margin from bar 65 on. The same page is candid that ignoring the problem is "what most charting sites do, and usually fine", because the latest bar has plenty of history behind it; it is the start of a short series, or of a backtest, that quietly carries the distorted values.

Wilder's smoothing is not the EMA's

"Period 14" does not name a smoothing constant. It names one of two, and which one depends on the indicator.

  • The EMA convention: alpha = 2 / (period + 1). For period 14 that is 2/15, about 0.133. TA-Lib's EMA computes 2.0 / (period + 1); backtrader's default is 2.0 / (1.0 + period); pandas calls the same mapping span.
  • Wilder's convention: alpha = 1 / period. For period 14 that is 1/14, about 0.071. TA-Lib's RSI implements it the long way round — multiply the previous average by period − 1, add today's value, scale by 1/period — and its ATR as (period − 1)/period of the previous average plus 1/period of today's true range; both are the same recursion. pandas reaches it with ewm(alpha=1/period) or, equivalently, com=period − 1. backtrader calls it SmoothedMovingAverage, pandas-ta calls it rma, and TradingView's help page for RSI shows its averages as rma(gain, 14) and rma(loss, 14).

Wilder's 1/14 is the EMA's alpha for a period of 27. A "14-period RSI" rebuilt with the EMA mapping — ewm(span=14) over the gains and losses, which is what a hand-rolled pandas version often does — is therefore not a slightly different RSI but an RSI with roughly half the memory, and it diverges from TA-Lib's on every bar, not only in the warm-up. backtrader ships the variants side by side under separate names: RSI_EMA, and RSI_SMA (aliased RSI_Cutler), beside a default RSI — aliased RSI_Wilder — whose movav parameter is the smoothed average.

ATR has the same fork, and here it is a user setting rather than a coding slip. TradingView's help page says its ATR is by default an RMA of the true range, "but the smoothing type can be changed to SMA, EMA or WMA in the settings". pandas-ta's atr takes a mamode argument defaulting to "rma". A chart that someone once switched to EMA smoothing keeps showing a number labelled ATR 14 that no library called with defaults will reproduce.

There is a quieter version of this in the RSI formula itself. Wilder's 100 − 100 / (1 + RS) and TA-Lib's 100 × gain / (gain + loss) are algebraically identical — TA-Lib's source says so and uses the second for speed — but they part company when both averages are zero, in a series that has not moved since the seed. TA-Lib returns 0. pandas-ta's native path divides zero by zero and returns NaN. backtrader divides as well unless safediv is switched on, in which case its docstring says it returns safelow, 50 by default, for the 0 / 0 case and safehigh, 100, for x / 0. It takes fourteen unchanged closes in a row, which daily bars of a traded stock almost never produce and one-minute bars of a thinly traded one can.

Same name, different defaults

A call that names no period gets the library's opinion, and the libraries do not share one. RSI and ATR default to 14 almost everywhere — TA-Lib, pandas-ta, pandas-ta-classic and backtrader all agree. The EMA does not: TA-Lib's defaults to 30, backtrader's moving averages to 30, pandas-ta's and pandas-ta-classic's to 10. EMA(close) in one and df.ta.ema() in the other are a 30-bar and a 10-bar average under the same name.

Defaults also move between versions of the same library. TA-Lib 0.8.1's release notes list two that change output without changing a line of calling code: BBANDS' default period went from 5 to 20, and PPO and APO now default to an EMA where they used to default to an SMA, with TA_MAType_SMA to be passed explicitly for the old behaviour. The same release re-ordered floating-point work in RSI, EMA and ATR; the notes quantify it at no more than 5.7e-14 on RSI's 0–100 scale at the default period, which is noise, and is listed here only so it is not mistaken for the cause of a real disagreement.

The quietest default is which implementation runs at all. pandas-ta 0.4.71b0's rsi, atr and ema each take a talib argument whose default is True, and the code path is if Imports["talib"] and mode_tal: — so when TA-Lib happens to be installed, the call returns TA-Lib's numbers, and when it is not, the call returns pandas-ta's own. For RSI those are the two different seeds described above. The same notebook gives different early values on two machines because of a package neither line of it imports. pandas-ta-classic reversed the default: its talib flag defaults to False, so TA-Lib is used only when asked for.

Adjusted or raw prices

Every indicator here is computed from price differences or price levels, so it inherits whatever was done to the prices before they arrived. Why two providers give different returns for the same stock covers how adjustment works; what follows is what each choice does to an indicator.

Raw prices put the corporate action into the indicator. A four-for-one split in an unadjusted series is a one-bar fall of three quarters of the price. RSI books it as a single loss of that size, and its share of the loss average then decays on the schedule in the table above — slowly, from a loss dozens of times the size of an ordinary day's move, so it can outweigh everything else in that average for weeks of daily bars. ATR's true range on the ex-date is the distance from the previous close to the new low, again about three quarters of the old price, and ATR takes it in at weight 1/14. An EMA drifts down from the old level toward the new one over the following bars. None of this is an error in the library: the input says the price fell, and the formula believed it. A cash dividend does the same on a small scale, as a one-bar gap down of roughly the amount paid.

Ratio adjustment rescales history, and the indicators measured in price rescale with it. A multiplicative factor applied to every bar before an event multiplies every EMA and ATR value before it by the same factor, so the ATR a chart shows for three years ago is not the dollar range anybody saw that day. RSI is a ratio of two averages built from the same differences, so a factor applied uniformly to all of its history cancels; its values before the newest event do not move when that event is applied. Where it does change is across the event, because the differences either side of it are now on different scales.

Difference adjustment preserves differences. Subtracting the cash from every earlier price leaves every bar-to-bar change exactly as traded and removes only the ex-date gap, so RSI and ATR before the event are untouched and differ afterwards only by the ex-date bar's contribution, decaying on the schedule above. It changes the price levels an EMA is built from, and over decades it can drive early prices below zero, as the adjusted-close guide shows.

The upshot: raw, ratio-adjusted and difference-adjusted inputs give three different ATR histories and three different RSI paths through every split and dividend, from the same library, called the same way. A chart and a script disagree most often not because of the code but because one was fed adjusted bars and the other was not, and the provider's documentation is where to find out which.

Where a bar begins and ends

Before any library runs, somebody decided which trades belong to each bar, and for US equities there are three decisions in it.

Which session. NYSE's core session runs 9:30 a.m. to 4:00 p.m. ET. Its sister venues trade outside it — NYSE Arca from 4:00 a.m., and late trading until 8:00 p.m. — and the exchange closes at 1:00 p.m. on certain days around holidays. A bar series built from regular hours only and one that includes extended hours differ in their highs and lows, and therefore in every true range, before a single ATR is computed. TradingView makes it a chart setting — the ETH option, or the Symbol tab in Settings — and its help page describes extended hours as something shown on intraday charts, where the symbol's exchange provides them. A 5-minute RSI on a chart with ETH on and a 5-minute RSI from a regular-hours feed are computed over different bars.

Where an intraday bar starts. A regular session that opens at 9:30 does not divide into clock hours. pandas' resample aligns its bins to midnight of the first day by default (origin='start_day') and labels each bin by its left edge, so hourly bars built from minute data with default arguments run 9:00–10:00, 10:00–11:00 and so on, and the first "hour" holds thirty minutes. Pass offset='30min' and the bins start at 9:30 instead — and now the last one, from 15:30, is the half-hour bar. Neither is wrong; they are different bars, with different closes, and an hourly EMA over one does not match an hourly EMA over the other at any bar.

Which clock. The same resample bins in whatever timezone the index carries. A minute series stored in UTC and resampled to daily bars cuts each "day" at midnight UTC, which is 8 p.m. or 7 p.m. in New York depending on daylight saving time. If the minute data includes extended hours, the daily bar then holds the session plus the trading around it, and in winter the last hour of late trading lands in the next day's bar; where one day ends and the next begins moves by an hour twice a year. Converting the index to America/New_York before resampling, or building daily bars from an exchange calendar, removes the problem. exchange_calendars publishes each session's open and close, in UTC, early closes included, which is the list a daily bar is supposed to follow.

What you can do about it

Feed more history than the period, and throw the start away. For a 14-period RSI or ATR, a hundred bars of warm-up takes the seed's weight under 0.1%, and the differences between seeding conventions go with it. Either slice the first hundred outputs off yourself or let TA-Lib do it with talib.set_unstable_period. The corollary: an indicator value for the first few weeks of a series, or of a newly listed stock, is a property of the seed more than of the prices, and should not be compared across tools at all.

Pin the implementation, not just the package. If you use pandas-ta, pass talib=True or talib=False explicitly so the result does not depend on what else is installed. With TA-Lib, read the release notes before upgrading across 0.8.1 — BBANDS, PPO and APO changed defaults there. Pass every period and averaging type explicitly instead of relying on a default; EMA(close, timeperiod=20) is a 20-bar EMA wherever it runs, while EMA(close) is 30 bars in one library and 10 in another.

Match the smoothing to the indicator. Wilder's RSI and ATR use alpha = 1/period. In pandas that is ewm(alpha=1/period, adjust=False) over an SMA seed — not ewm(span=period), and not the default adjust=True. On a chart, check the ATR's smoothing setting before comparing it with anything.

Reconcile on one bar, from the same inputs. To find out why a chart and a script disagree, export the chart's bars if the platform allows it, run the library over exactly those bars with the same history length, and compare. If the numbers now agree, the difference was in the data — adjustment, session, or bar boundaries — and not in the formula. If they still disagree after a hundred bars, the smoothing constant or the averaging type differs.

Decide the input before the indicator. Choose raw, ratio-adjusted or difference-adjusted prices on purpose and record the choice next to the output, the way the adjusted-close guide recommends storing raw prices plus a corporate actions table. An ATR from three years ago is the dollar range that traded that day only on the raw series; on a ratio-adjusted one it has been rescaled by every event since.

Build bars in exchange time. Convert timestamps to America/New_York before resampling, align intraday bins to 9:30 when that is what you are comparing against, decide whether extended hours belong in your bars, and take session opens, closes and early closes from exchange_calendars rather than from a hard-coded 9:30–16:00. The rest of the libraries in this section are listed under analysis libraries.

Tools this bears on

Cards in the catalogue where what is above changes the decision.

  • TA-Lib

    The C indicator library everything else wraps — and pip now ships the C part with it.

    FreeFree tierOpen source

  • pandas-ta

    The DataFrame indicator library whose repo is gone and whose last release is a year old.

    FreeFree tierOpen source

  • TradingView

    Charting across 50-plus markets, with Pine Script, server-side alerts and broker trading.

    $12.95/moFree tier

  • exchange_calendars

    Sessions, minutes and holidays for 69 exchanges, keyed by ISO-10383 code.

    FreeFree tierOpen source

FAQ

Why does my RSI not match TradingView's for the same stock?

The formula is almost certainly the same Wilder average; the inputs around it are not. The usual causes, in order, are a different amount of history before the bar you are comparing, split or dividend adjustment applied on one side and not the other, extended-hours bars on an intraday chart, and hourly bars that start at 9:00 in one place and 9:30 in the other.

How many bars of history does a 14-period RSI need before it is reliable?

RSI uses Wilder's smoothing, alpha 1/14, so the seed's weight falls below 1 per cent after 63 further bars and below 0.1 per cent after 94. Supplying about a hundred bars before the first value you use removes almost all of the difference between seeding conventions. Fourteen bars is the minimum to produce a number, not the minimum to produce a stable one.

Is Wilder's smoothing the same as an EMA?

It is the same recursion with a different constant. An EMA of period n uses alpha 2/(n+1); Wilder's average of period n uses alpha 1/n, which is the EMA of period 2n-1. So a 14-period Wilder average behaves like a 27-period EMA, and an RSI rebuilt with a 14-period EMA is a faster, different indicator.

Why does pandas-ta give different values on two machines?

In release 0.4.71b0, rsi, atr and ema default to calling TA-Lib when TA-Lib is installed and fall back to their own code when it is not, and the native RSI seeds its average differently from TA-Lib's. Pass the talib argument explicitly, or use pandas-ta-classic, where the default is to use its own code.

Should indicators be computed on adjusted or unadjusted prices?

It depends on what the number is for, and the choice has to be made on purpose. Unadjusted prices put every split and dividend into the indicator as a real price move; ratio-adjusted prices remove those gaps but rescale every earlier price-denominated value such as ATR and EMA. Record which one you used next to the output.

Sources

  1. ta_RSI.c — TA-Lib project (GitHub), read
  2. ta_EMA.c — TA-Lib project (GitHub), read
  3. ta_ATR.c — TA-Lib project (GitHub), read
  4. ta_EMA.c at v0.6.4 (TA_MA_METASTOCK seeding) — TA-Lib project (GitHub), read
  5. talib/__init__.py (set_unstable_period) — TA-Lib Python wrapper project (GitHub), read
  6. Unstable Period — TA-Lib project,
  7. TA-Lib 0.8.1 release notes — TA-Lib project (GitHub),
  8. pandas-ta 0.4.71b0 (wheel source, overlap/rma.py, overlap/ema.py, momentum/rsi.py, volatility/atr.py) — Python Package Index,
  9. pandas_ta_classic/overlap/rma.py — pandas-ta-classic project (GitHub), read
  10. pandas_ta_classic/overlap/ema.py — pandas-ta-classic project (GitHub), read
  11. backtrader/indicators/basicops.py — backtrader project (GitHub), read
  12. backtrader/indicators/mabase.py (default period 30) — backtrader project (GitHub), read
  13. backtrader/indicators/rsi.py — backtrader project (GitHub), read
  14. pandas.DataFrame.ewm (pandas 3.0.6) — pandas development team, read
  15. pandas.DataFrame.resample (pandas 3.0.6) — pandas development team, read
  16. Relative Strength Index (RSI) — TradingView, read
  17. Average True Range (ATR) — TradingView, read
  18. I want to access Extended Hours data — TradingView, read
  19. Holidays and Trading Hours — New York Stock Exchange, read

The catalogue next door

This page is background, not a listing. The products it bears on are in Market Analysis & Portfolio Optimization Libraries, each filled in against the same schema, with the fields to narrow it yourself.

Last updated . Corrected in place: this is a reference page, not a dated post.