Technique explained as code
Bollinger bands
An average with a band above and below it, set by how much price has been moving. Computed from scratch, then tested against the common belief about how often price leaves the bands.
Risk warning: CFDs and spread bets are complex instruments and come with a high risk of losing money rapidly due to leverage. You can lose money on these products, and each provider publishes its own loss figure under FCA rules. You should consider whether you understand how they work and whether you can afford to take the high risk of losing your money. Automated and API trading can amplify losses through bugs, connectivity failures and runaway orders.
Teaching material only. This page explains how a technique works as code and how to test an idea honestly. It is not investment advice, not a recommendation and not an invitation to trade. Every price on it is invented, so no result here says anything about any real market, and nothing here suggests that a technique makes money.
What it is
Bollinger bands are three lines. The middle one is a moving average, usually of twenty days. The other two sit above and below it at a distance of two standard deviations of the same twenty prices.
Standard deviation is a measure of spread: how far the prices in the window sit from their own average. When price has been moving a lot the bands are wide. When it has been quiet they are narrow.
The idea behind it
John Bollinger introduced them in the 1980s as a way to say whether a price is high or low relative to its own recent behaviour, with a yardstick that adapts as the market gets calmer or wilder.
A figure often repeated is that price stays inside the bands about 95 percent of the time. That figure comes from the textbook bell curve, and it is worth testing in code, because prices are not draws from a bell curve around a fixed centre.
The code
The first loop prints the bands the way a chart draws them, with today's close included in the window. The second loop asks a stricter question. It draws the bands from the twenty days before, and only then looks at where the next close landed. That is the version a rule would have to use, since a rule cannot know today's close in advance.
This script does not simulate trades. It counts events, and it compares what followed one kind of day with what followed any day. Always make that comparison before believing an event means something.
Data the script reads: invented-prices.csv. Invented values, never a real market. Save it in one folder and run the script from that folder.
The file that was run: bollinger_bands.py
# Bollinger bands, computed from scratch: an average with a band either side of it,
# set a number of standard deviations away.
# Prices come from invented-prices.csv: a random walk made by a script, never a real market.
# The result therefore says nothing about any real market. Read it for the method.
import csv
dates, closes = [], []
with open("invented-prices.csv", newline="") as f:
for row in csv.DictReader(f):
dates.append(row["date"])
closes.append(float(row["close"]))
def average(values):
return sum(values) / len(values)
def standard_deviation(values):
"""How spread out the values are around their own average."""
mean = average(values)
squared = [(v - mean) ** 2 for v in values]
return (sum(squared) / len(values)) ** 0.5
# The usual textbook values. They are a convention, not a suggestion.
DAYS = 20
WIDTH = 2.0
print("date lower middle upper close")
for day in range(len(closes) - 3, len(closes)):
window = closes[day - DAYS + 1:day + 1] # the last DAYS closes, ending today
middle = average(window)
spread = standard_deviation(window)
print(dates[day], f"{middle - WIDTH * spread:7.2f} {middle:7.2f} {middle + WIDTH * spread:7.2f} {closes[day]:7.2f}")
# How often does a close land outside bands that were drawn BEFORE that close was known?
# And when it lands below, is the next day any different from an ordinary day?
below = above = tested = 0
up_after_below = 0
up_on_any_day = 0
for day in range(DAYS, len(closes) - 1):
window = closes[day - DAYS:day] # ends at yesterday
middle = average(window)
spread = standard_deviation(window)
tested += 1
rose_next_day = closes[day + 1] > closes[day] # looked up afterwards, to measure, not to decide
if rose_next_day:
up_on_any_day += 1
if closes[day] > middle + WIDTH * spread:
above += 1
if closes[day] < middle - WIDTH * spread:
below += 1
if rose_next_day:
up_after_below += 1
print("Days tested:", tested)
print("Closes above the upper band:", above)
print("Closes below the lower band:", below)
print("Share of closes outside the bands:", round((above + below) / tested * 100, 1), "percent")
print("Next day was higher, after a close below the band:", up_after_below, "of", below)
print("Next day was higher, on any day at all:", up_on_any_day, "of", tested)What the run printed
date lower middle upper close 2025-11-12 89.38 93.83 98.28 91.95 2025-11-13 89.21 93.74 98.27 91.89 2025-11-14 88.95 93.53 98.12 91.25 Days tested: 729 Closes above the upper band: 69 Closes below the lower band: 75 Share of closes outside the bands: 19.8 percent Next day was higher, after a close below the band: 31 of 75 Next day was higher, on any day at all: 357 of 729
Output of the run on , copied as printed. Every input to this run was invented, so the result says nothing about any real market.
How to read it
About one close in five landed outside bands drawn beforehand, nowhere near the one in twenty that the 95 percent figure suggests. Two things explain it. A band that already includes today's close is harder for that close to escape. And a wandering price keeps moving away from an average of where it used to be.
The last two lines are the comparison. After a close below the lower band the next day was higher 31 times out of 75. On any day at all it was higher 357 times out of 729, a little under half. So on this file a close below the band was not followed by a rise more often than usual. If anything it was followed by one less often, though 75 events is too few to read anything into the gap.
The prices are a random walk, where the next day does not depend on the last, so no real difference exists to be found. It says nothing about any real market.
How it fails or misleads
- A touch of the band is not a signal. In a strong move price can sit on or outside a band for many days in a row, which traders call walking the band.
- The 95 percent figure does not hold for prices, as the output shows. Anything built on that assumption is built on the wrong number.
- The bands widen after a large move, so the yardstick changes just when you are using it. A price that was outside yesterday's band can be inside today's without moving.
- Narrow bands tell you price has been quiet. They do not tell you which way it will go next, or when.
- The length and the width are free settings, with the usual danger of fitting them to the past.
The page on how a backtest lies shows the mistakes that make any rule look better in a test than it is.
What to try changing
These are programming exercises. Each one changes the code and asks you to explain what you see. None of them is a search for settings to use.
- Change the second loop so the window includes today's close and count again. How far does the share of closes outside the bands fall?
- Count the longest run of days in a row that closed above the upper band.
- Add the same next day comparison for closes above the upper band.
- Compute band width as the upper band minus the lower band, divided by the middle. Print its smallest and largest values and the dates they fell on.
- Replace the standard deviation function with one that divides by one less than the number of values, the sample form, and see how much the bands move.
How this page was made
Each script on this page was run with Python 3.12 on Windows, standard library only, from a folder holding the data files listed. No network and no account. Each exited with code 0, and what is shown is everything it printed.
The data files whose names begin with the word invented are written by make_invented_data.py from fixed seeds, so anyone can make the same files again and get the same output. This site is not published if the code or a data file has changed since the run.