Broker API HubMenu

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.

Independent and unofficialEducational onlyInvented data onlyCode run

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

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.

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.