Why don't my Bollinger Bands match TradingView?
Quick answer
Bollinger Bands usually fail to match TradingView because the standard deviation is calculated with the sample formula (divide by n - 1) instead of the population formula (divide by n) that John Bollinger specifies. pandas' rolling().std() and Excel's STDEV.S both default to the sample version, which makes every band offset about 2.6% wider on a 20-period setting. Use rolling(20).std(ddof=0) or STDEV.P. If the middle band also disagrees, your closes differ.
If your Bollinger Bands don't match TradingView, check the standard deviation first. John Bollinger defines the bands with the population standard deviation, which divides by n. pandas' rolling().std() and Excel's STDEV.S divide by n - 1, so on the default 20-period setting every band sits about 2.6% further from the middle line than the chart's does.
The good news is that Bollinger Bands are easier to debug than RSI or MACD. They have no memory: each value uses exactly the last 20 closes and nothing older. So unlike the smoothed indicators, a mismatch never comes from how much history you loaded. It comes from the formula or from the data, and the pattern of the error tells you which.
What the bands are, exactly
The middle band is a 20-period simple moving average of the close. The upper and lower bands sit two standard deviations above and below it, where the standard deviation is taken over the same 20 closes. Bollinger's own description is explicit that he uses the population calculation for the standard deviation, and the defaults of 20 periods and 2 deviations have not changed since he introduced them. TradingView's built-in indicator follows that definition.
The population formula divides the sum of squared deviations by 20. The sample formula divides by 19. The ratio between the two results is the square root of 20/19, which is 1.0260, so the sample version is 2.6% wider at every bar, whatever the market is doing.
The pandas and Excel defaults
In pandas, Series.rolling(20).std() has a ddof argument that defaults to 1, and the divisor is N - ddof. That default is the sample standard deviation, which is the right choice for estimating the volatility of a population from a sample, and the wrong one for matching a Bollinger chart. Not every library agrees: pandas_ta's bbands function defaults to ddof=0, while some other rolling-indicator libraries choose ddof=1 deliberately so that they match pandas. If you switched libraries and your bands moved, this is likely why. One more pandas_ta detail worth checking: its bbands source falls back to a length of 5, not 20, when you don't pass one, so call it with length=20 explicitly.
Excel has the same split. STDEV.S (and the older STDEV) is the sample version; STDEV.P is the population version. A spreadsheet built with STDEV over a 20-row window will produce the wider bands.
A worked example on the S&P 500
We ran both versions on the S&P 500's daily closes as of 20 October 2026, when the index closed at 7,712.57. The data is Cboe's daily index history, which matches FRED's SP500 series to the cent.
- Middle band (20-day SMA): 7,732.94 for both, because the average does not involve the standard deviation.
- Population, as Bollinger specifies: standard deviation 54.24, upper band 7,841.42, lower band 7,624.46.
- Sample, the pandas default: standard deviation 55.65, upper band 7,844.23, lower band 7,621.64.
That is about 2.8 index points on each band. Small, but it is enough to change a signal. On 6 October 2026 the S&P 500 closed at 7,818.93. The population upper band was 7,816.09, so the close was above the band. The sample upper band was 7,819.62, so it was not. Over the 252 sessions from 20 October 2025, the population bands had 7 closes above the upper band and 13 below the lower band; the sample bands had 6 and 13. If you backtest a band-touch rule, the wider bands quietly drop trades.
Derived measures shift too. BandWidth, (upper - lower) / middle, was 2.81% with the population formula and 2.88% with the sample one on 20 October, which matters if you use a squeeze threshold copied from someone's chart.
How to tell which problem you have
Compare your output with the chart on a few recent completed bars, and look at the middle band first.
- The middle band matches, and your upper minus middle divided by the chart's upper minus middle is 1.026 on every bar. It is the standard deviation divisor. Set ddof=0.
- The middle band does not match. The closes differ, or the middle line is not a 20-period simple average. Check adjusted versus unadjusted prices, the official close versus the last trade, and whether you are comparing a completed bar or one that is still forming. The same input checks are covered in more detail in our RSI mismatch post.
- The ratio is not constant and the middle band matches. Check the length and the multiplier in the indicator settings, and whether the chart shifts the bands with an offset. An offset moves the plotted line sideways; it does not change the calculation.
- The bands match except on the first 19 bars of your data. That is expected. There is no value until 20 closes exist, and any library that fills those bars is inventing numbers.
Bollinger Bands in pandas that match the definition
import pandas as pd
def bollinger(close: pd.Series, n: int = 20, k: float = 2):
mid = close.rolling(n).mean()
sd = close.rolling(n).std(ddof=0) # population standard deviation, as Bollinger specifies
return mid, mid + k * sd, mid - k * sd
# close: a Series of daily closes, oldest first
mid, upper, lower = bollinger(close)Run on the S&P 500 closes above, this returns 7,732.94, 7,841.42 and 7,624.46 for 20 October 2026. Remove ddof=0 and the upper band becomes 7,844.23. In Excel, the equivalent band is =AVERAGE(B2:B21)+2*STDEV.P(B2:B21).
Charting the bands so readers can check them
Label the settings on the chart ("Bollinger Bands (20, 2), population SD") so anyone reproducing it knows which variant you drew. Shade the area between the bands lightly rather than drawing three heavy lines, and keep the price as the strongest mark. If you are publishing a volatility argument rather than a trading signal, the bands are a price-level view of a return-volatility story, and the realized versus implied volatility chart is often the better primary chart. For a range-based measure that also counts overnight gaps, see why ATR doesn't match TradingView.
Where Quadesto fits
Quadesto charts price series from your own data or a connected market data source, and the rule for bands is the same in any tool: state the length, the multiplier and the standard deviation convention, and check the middle line before you blame the formula. You can start free.