Why doesn't my MACD match TradingView?
Quick answer
Your MACD usually doesn't match TradingView because you started with too little history, so the EMA seeds still dominate, or because your signal line is a different average or starts in the wrong place. Changing pandas' adjust setting only matters on short histories. With 250 or more bars, MACD(12,26,9) on the S&P 500 converged to 10.36, signal 21.21, on 20 October 2026 whichever EMA convention we used. Also confirm both MA type settings are EMA.
If your MACD doesn't match TradingView, the most likely cause is history, not the formula. MACD stacks three exponential averages (12, 26 and a 9-period signal line on top), so it needs far more bars than its parameters suggest before its value stops depending on where your data starts. The second most likely cause is the signal line: it is an EMA of the MACD line, and both where it starts and which average it uses vary between tools.
It is tempting to fix this by setting adjust=False in pandas. On a long history that change does nothing. On a short history it swaps one wrong answer for another.
What MACD is, exactly
The MACD line is the 12-period EMA of the close minus the 26-period EMA. The signal line is a 9-period EMA of the MACD line, not of the price, which is a common bug on its own. The histogram is MACD minus signal. Each EMA uses a weight of 2/(n + 1) on the newest value.
TradingView's MACD settings include an Oscillator MA type and a Signal MA type, each of which can be EMA or SMA. The standard definition uses EMA for both, so if a chart's numbers look strange, check those two settings before your code.
Cause 1: not enough history
An EMA has to start somewhere. Some implementations seed it with a simple average of the first n values, some with the first value, and pandas by default (adjust=True) uses a weighted average of all values so far. These seeds fade, but slowly, and in MACD they compound: the signal line cannot start until the 26-period EMA exists, so its own warm-up begins late.
We computed MACD(12,26,9) for the S&P 500 on 20 October 2026, when the index closed at 7,712.57, using Cboe's daily index closes from January 2000. With the full history, every seeding convention gives the same answer: MACD 10.36, signal 21.21, histogram -10.85. Then we fed in only the most recent bars:
- 35 bars, just past the minimum the signal line needs: 17.78 with SMA-seeded EMAs, 11.82 with pandas adjust=False, 5.84 with the pandas default.
- 50 bars: 4.28, 6.63 and 6.74.
- 60 bars: 9.67, 10.73 and 7.85.
- 100 bars: 10.28, 10.24 and 10.18.
- 250 bars or more: 10.36 for all three.
On 35 bars the three answers differ by a factor of three, and none of them is the converged 10.36. The adjust=False suggestion helps here only by coincidence: on 50 bars the default was closer. The fix that works every time is more data. Load at least 250 bars before the first value you use and the convention stops mattering.
Cause 2: the signal line
Even with enough history, the signal line has two traps.
The first is where it starts. The signal is an EMA of the MACD line, so it should begin once the MACD line exists, at bar 26, and be seeded from MACD values only. A common bug fills the bars before that with zeros and averages them in, which drags the early signal toward zero until that seed fades like any other.
The second is the average itself. If the Signal MA type is SMA instead of EMA, the numbers change permanently, not just during warm-up. On 20 October 2026 the signal was 21.21 with an EMA and 26.74 with a 9-day simple average, and the histogram went from -10.85 to -16.38. Over the 252 sessions from 20 October 2025, the EMA version crossed 22 times and the SMA version 24 times, and 14 crossover dates appear in one list but not the other. In a backtest, that means trades on different days.
Cause 3: different closes
Once history and settings match, any remaining gap is the input. Adjusted versus unadjusted prices, official close versus last trade, regular hours versus extended hours, a still-forming daily bar, or simply a different instrument (SPY, the S&P 500 index and the E-mini future are three series) will each move the EMAs. The RSI mismatch post walks through these checks; they apply unchanged to MACD.
A MACD in pandas you can check
import pandas as pd
def ema(x: pd.Series, n: int) -> pd.Series:
seed = x.iloc[:n].mean() # SMA seed
out = [float("nan")] * (n - 1) + [seed]
a = 2 / (n + 1)
for value in x.iloc[n:]:
out.append(a * value + (1 - a) * out[-1])
return pd.Series(out, index=x.index)
def macd(close: pd.Series, fast=12, slow=26, signal=9):
line = ema(close, fast) - ema(close, slow)
valid = line.dropna() # the signal starts where MACD starts
sig = ema(valid, signal).reindex(line.index)
return line, sig, line - sig
# close: daily closes, oldest first; use 250+ bars before the first value you read
line, sig, hist = macd(close)On the S&P 500 closes above, this returns 10.36, 21.21 and -10.85 for 20 October 2026, both on the full history and on the last 250 bars alone. If you prefer pandas' built-in EMA, close.ewm(span=12, adjust=False).mean() reaches the same values once the history is long enough; it just gets there from a different seed.
Charting MACD so readers can trust it
Put MACD in its own panel under the price, with the MACD and signal lines and the histogram as bars around a zero line. Label it with the settings and the averages ("MACD(12,26,9), EMA/EMA"). If your chart starts where your data starts, hide at least the first 100 bars: they are warm-up, and a crossover drawn there is an artifact of the seed, not of the market. MACD is in price points, so like ATR it grows with the price level; that matters if you compare readings years apart.
Where Quadesto fits
Quadesto charts price and time series from your own data or a connected market data source. Whatever draws the line, the same three checks apply: enough history, EMA for both averages, and the same closes as the chart you are comparing against. You can start free.