Bollinger Bands with Python applied to the S&P500

Using Bollinger Bands, we can create a trading strategy to buy and sell stocks based on the market's moving volatility.

Using Bollinger Bands, we can create a trading strategy to buy and sell stocks based on the market’s moving volatility.

Trading strategy using Bollinger Bands with highlighted buy and sell signals
F1. Trading with Bollinger Bands applied to the S&P500

We explain the process as we apply the strategy to the S&P500 data.


The S&P500 is an index that represents the performance of the 500 largest companies in the United States.

To simplify understanding, we selected one year of data.

import yfinance as yf
df ='SPY', start='2023-01-01', end='2024-01-01')
Line chart showing one year of S&P500 data
F2. S&P500 stock data during 2023


  1. What are Bollinger Bands, and how are they calculated?
  2. How to determine buy and sell signals with Bollinger Bands?
  3. How to avoid consecutive buy or sell signals in the trading strategy?
  4. How to calculate the outcome of the strategy?


Moving Averages and Standard Deviations

Bollinger Bands are calculated from the moving average (\(\)) and standard deviation (\(\)) based on a period \(r\).

\(\) bands = _r k _r \(\)

In our case, we will use 30 days.

df['Adj Close'].rolling(30).agg(['mean', 'std'])

Calculating Bands

Besides choosing the period, we must select the width of the bands with the \(k\) parameter, which will multiply the standard deviation.

     b_lower = lambda x: x['mean'] - 2 * x['std'],
     b_upper = lambda x: x['mean'] + 2 * x['std']
Calculation of Bollinger Bands for the S&P500, showing upper and lower bands
F3. Calculated Bollinger Bands

Buy and Sell Signals

Now comes the juiciest (and most complicated) part: calculating the buy and sell points.

If the closing price crosses the lower band, the asset is undervalued (worth more than it costs) and is likely to rise. Therefore, it is a buy signal.

For selling, we will use the crossing of the upper band.

Would this be interesting to one of your friends? Share it with them.

mask_sell = df.close > df.b_upper
mask_buy = df.close < df.b_lower

df.loc[mask_buy, 'signal'] = 1
df.loc[mask_sell, 'signal'] = -1

Here are the days when we should buy and sell.

Detail of buy and sell signals over time, indicated on the closing price of the S&P500
F4. Unrestricted buy and sell signals

But, if we invest all the capital in each operation, we cannot buy if we have already bought, nor sell if we have already sold.

There should not be two consecutive buy (1) or sell (-1) signals.

Signal Control

To avoid the above problem, we will consider the current state of operations with a flag. By default, it will be False because we do not have any open operation (we have not bought one).

flag_buy = False

We iterate over each day to calculate the signals according to the previous conditions.

l = []

for idx, day in df.iterrows():
    if (day.signal == 1) & (flag_buy == False):
        signal = 1
        flag_buy = True
    elif (day.signal == -1) & (flag_buy == True):
        signal = -1
        flag_buy = False
        signal = 0
df['signal'] = l

And we get the corrected signals, with only one buy or sell signal followed.

Trading strategy using Bollinger Bands with highlighted buy and sell signals
F5. Final execution of the trading strategy with Bollinger Bands

Strategy Outcome

Finally, we calculate the outcome of the strategy.

If we always buy with the same amount, the result will be the sum of the differences between the moments of purchase and sale.

(df.close * df.signal).mul(-1).sum()

Therefore, if we had bought one complete share at each signal, we would have obtained a total profit of $38.46 at the end of the period.

Questions? Let’s talk in the comments below.


  1. Bollinger Bands: rolling(n).agg(['mean', 'std']) calculates the moving average and standard deviation, the basis for Bollinger Bands, a measure of volatility.
  2. Buy/Sell signals: Optimal moments based on Bollinger Bands are identified using the condition df.close < df.b_lower for buying and df.close > df.b_upper for selling.
  3. Signal control: By implementing a flag and a loop, consecutive signals of the same type are avoided.
  4. Strategy outcome: (df.close * df.signal).mul(-1).sum() sums the differences between the buy and sell points.

If you could program whatever you wanted, what would it be? The upcoming tutorial might cover it ;)

Let’s talk in the comments below.

Great! You’ve successfully signed up.

Welcome back! You've successfully signed in.

You've successfully subscribed to datons.

Success! Check your email for magic link to sign-in.

Success! Your billing info has been updated.

Your billing was not updated.