Bollinger Bands Trading Strategy

Photo by Maxim Hopman on Unsplash




Using the Moving Average and standard deviation to construct a Bollinger Band, determine when to buy and sell.


Bollinger Band is a technical indicator that John Bollinger invents in the 1980s. Bollinger Bands consist of the concepts of statistics and moving averages. The moving Average(MA) is the average closing price of a past period. Normally the period of MA in Bollinger Band is 20 days, and Standard Deviation(SD) is usually represented by σ in mathematical sign, which is used to evaluate the data’s degree of discrete.

Bollinger Band is composed of three tracks:
● The upper track:20 MA+double standard deviation
● The middle track:20 MA
● The lower track:20 MA+double standard deviation

During the long-term observation period, the investment target price distribution will be Normal Distribution. According to statistics, there is a 95% of probability that the price will present between μ − 2σ and μ + 2σ, which is also called a 95% Confidence Interval(CI). Bollinger Band is the technical indicator base on the theories above.


When the closed price touches the upper track, we consider it a signal of price fall, and then we will sell our holding position at tomorrow’s open price.

When the closed price touch the lower track, we consider it to be a signal of price rise, and then we’re going to buy 1 unit at tomorrow’s opened price; when the conditions above are already satisfied, we remain adequate principal, both the holding position and the closed price are lower than last time buying price, we will buy one more unit.

The Editing Environment and Module Required

This article uses Mac OS as a system and jupyter as an editor.

import pandas as pd 
import re
import numpy as np 
import tejapi
from functools import reduce
import matplotlib.pyplot as plt
from collections import defaultdict, OrderedDict
from tqdm import trange, tqdm
import as px
import plotly.graph_objects as go

tejapi.ApiConfig.api_key = "Your Key"
tejapi.ApiConfig.ignoretz = True


Listed company adjusted price(day)-average price(TWN/AAPRCDA)
Unadjusted(day) technical indicators(TWN/AVIEW1)

Data Import

For the period from 2021–06–01 to 2022–12–31, we take AUO Corporation(2409) as an example, we will use unadjusted closed price、BB-Upper(20)、BB-Lower(20) to construct the Bollinger Band, and then we will compare the return with Market Return Index(Y9997)

stock = tejapi.get('TWN/APRCD',
                   paginate = True,
                   coid = '2409',
                   mdate = {'gte':'2021-06-01', 'lte':'2022-12-31'},
                   opts = {
                       'columns':[ 'mdate', 'open_d', 'high_d', 'low_d', 'close_d', 'volume']

ta = tejapi.get('TWN/AVIEW1',
                paginate = True,
                coid = '2409',
                mdate = {'gte':'2021-06-01', 'lte':'2022-12-31'},
                opts = {
                    'columns':[  'mdate', 'bbu20', 'bbma20', 'bbl20']

market = tejapi.get('TWN/APRCD',
                   paginate = True,
                   coid = "Y9997",
                   mdate = {'gte':'2021-06-01', 'lte':'2022-12-31'},
                   opts = {
                       'columns':[ 'mdate', 'close_d', 'volume']

data = stock.merge(ta, on = ['mdate'])
market.columns = ['mdate', 'close_m', 'volume_m']
data = data.set_index('mdate')

After acquiring the investment target price and technical indicator data, let’s draw the Bollinger Band first. Here we use Plotly. express to draw the line chart. In the diagram, bbu20 will be the upper track 、bbl20 will be the lower track, and close_d will be the closed price.

fig = px.line(data,   
            color_discrete_sequence = px.colors.qualitative.Vivid
友達光電(2049) 2021–06–01~2022–12–31 布林通道
友Bollinger Band of AUO Corporation(2049) 2021–06–01~2022–12–31

Trading Strategy

We define some parameters here.
● principal: the total amount of funds invested by investors.
● position: the number of units investors held.
● cash: the amount of funds investors held after each trade.
● order_unit: each trade’s amount of share unit

principal = 500000
cash = principal
position = 0
order_unit = 0
trade_book = pd.DataFrame()

for i in range(data.shape[0] -2):
    cu_time = data.index[i]
    cu_close = data.loc[cu_time, 'close_d']
    cu_bbl, cu_bbu = data.loc[cu_time, 'bbl20'], data.loc[cu_time, 'bbu20']
    n_time = data.index[i + 1]
    n_open = data['open_d'][i + 1]
    if position == 0: #進場條件
        if cu_close <= cu_bbl and cash >= n_open*1000: 
            position += 1
            order_time = n_time
            order_price = n_open
            order_unit = 1
            friction_cost = (20 if order_price*1000*0.001425 < 20 else order_price*1000*0.001425)
            total_cost = -1 * order_price * 1000 - friction_cost
            cash += total_cost
            trade_book = trade_book.append(
                    stock_id, 'Buy', order_time, 0, total_cost, order_unit, position, cash
                ]), ignore_index = True)
    elif position > 0:
        if cu_close >= cu_bbu: # 出場條件
            order_unit = position
            position = 0
            cover_time = n_time
            cover_price = n_open
            friction_cost = (20 if cover_price*order_unit*1000*0.001425 < 20 else cover_price*order_unit*1000*0.001425) + cover_price*order_unit*1000*0.003
            total_cost = cover_price*order_unit*1000-friction_cost
            cash += total_cost
            trade_book = trade_book.append(pd.Series([
                stock_id, 'Sell', 0, cover_time, total_cost, -1*order_unit, position, cash
            ]), ignore_index=True)
        elif cu_close <= cu_bbl and cu_close <= order_price and cash >= n_open*1000: #加碼條件: 碰到下界,比過去買入價格貴
            order_unit = 1
            order_time = n_time
            order_price = n_open
            position += 1
            friction_cost = (20 if order_price*1000*0.001425 < 20 else order_price*1000*0.001425) 
            total_cost = -1 * order_price * 1000 - friction_cost
            cash += total_cost
            trade_book = trade_book.append(
                stock_id, 'Buy', order_time, 0, total_cost, order_unit, position, cash
            ]), ignore_index = True)
if position > 0: # 最後一天平倉
    order_unit = position
    position = 0
    cover_price = data['open_d'][-1]
    cover_time = data.index[-1]
    friction_cost = (20 if cover_price*order_unit*1000*0.001425 < 20 else cover_price*order_unit*1000*0.001425) + cover_price*order_unit*1000*0.003
    cash += cover_price*order_unit*1000-friction_cost
    trade_book = trade_book.append(
        stock_id, 'Sell',0, cover_time, cover_price*order_unit*1000-friction_cost, -1*order_unit, position, cash
    ]), ignore_index = True)    
trade_book.columns = ['Coid', 'BuyOrSell', 'BuyTime', 'SellTime', 'CashFlow','TradeUnit', 'HoldingPosition', 'CashValue']

Transaction Record

Let’s review our transaction record after executing above trading strategy code. You can find the source code about how to make this transaction record table at the bottom of the article.

Part of Transaction Record(2021/07/15~2022~03/15)

By observing the following graph, we can find out that there is a rising trend from 2021/11 to 2021/12(the light blue area) because the closed price can’t touch the lower track of the Bollinger band; no action of buying. The result is that we can not earn any profit from this upward trend.

The same issue also appears in the continuous falling downward trend, like the interval starting from 2022/04(light green area), repeatedly touching the lower track of the Bollinger band and then back up slightly. Because the upper track of the Bollinger band is too low, which means easy to touch, the holding position will be sold sooner, leading to a negative return in this interval.

In fact, because of the hysteresis of the 20MA Bollinger band, a short-term fluctuation in price can not be reflected by the 20MA Bollinger band.
We suggest shortening the term of the MA or use with other technical indicators which observe the trend of the investment target if the target stock you are analyzing has a higher rate of fluctuation.

The points of transactions

Performance Evaluation

cash = principal
data_ = data.copy()
data_ = data_.merge(trade_book_, on = 'mdate', how = 'outer').set_index('mdate')
data_ = data_.merge(market, on = 'mdate', how = 'inner').set_index('mdate')

# fillna after merge
data_['CashValue'].fillna(method = 'ffill', inplace=True)
data_['CashValue'].fillna(cash, inplace = True)
data_['TradeUnit'].fillna(0, inplace = True)
data_['HoldingPosition'] = data_['TradeUnit'].cumsum()

# Calc strategy value and return
data_["StockValue"] = [data_['open_d'][i] * data_['HoldingPosition'][i] *1000 for i in range(len(data_.index))]
data_['TotalValue'] = data_['CashValue'] + data_['StockValue']
data_['DailyValueChange'] = np.log(data_['TotalValue']) - np.log(data_['TotalValue']).shift(1)
data_['AccDailyReturn'] =  (data_['TotalValue']/cash - 1) *100

# Calc BuyHold return
data_['AccBHReturn'] = (data_['open_d']/data_['open_d'][0] -1) * 100

# Calc market return
data_['AccMarketReturn'] = (data_['close_m'] / data_['close_m'][0] - 1) *100

# Calc numerical output
overallreturn = round((data_['TotalValue'][-1] / cash - 1) *100, 4) # 總績效
num_buy, num_sell = len([i for i in data_.BuyOrSell if i == "Buy"]), len([i for i in data_.BuyOrSell if i == "Sell"]) # 買入次數與賣出次數
num_trade = num_buy #交易次數

avg_hold_period, avg_return = [], []
tmp_period, tmp_return = [], []
for i in range(len(trade_book_['mdate'])):
    if trade_book_['BuyOrSell'][i] == 'Buy':
        sell_date = trade_book_["mdate"][i]
        sell_price = trade_book_['CashFlow'][i] / len(tmp_return)
        avg_hold_period += [sell_date - j for j in tmp_period]
        avg_return += [ abs(sell_price/j) -1  for j in tmp_return]
        tmp_period, tmp_return = [], []

avg_hold_period_, avg_return_ = np.mean(avg_hold_period), round(np.mean(avg_return) * 100,4) #平均持有期間,平均報酬
max_win, max_loss = round(max(avg_return)*100, 4) , round(min(avg_return)*100, 4) # 最大獲利報酬,最大損失報酬
winning_rate = round(len([i for i in avg_return if i > 0]) / len(avg_return) *100, 4)#勝率
min_cash = round(min(data_['CashValue']),4) #最小現金持有量

print('總績效:', overallreturn, '%')
print('交易次數:', num_trade, '次')
print('買入次數:', num_buy, '次')
print('賣出次數:', num_sell, '次')
print('平均交易報酬:', avg_return_, '%')
print('平均持有期間:', avg_hold_period_ )
print('勝率:', winning_rate, '%' )
print('最大獲利交易報酬:', max_win, '%')
print('最大損失交易報酬:', max_loss, '%')
print('最低現金持有量:', min_cash)

We can notice that there are only 29 transactions in one and a half years. Maybe improving the strategy by taking the suggestions we mentioned before is the way to increase the number of transactions.

Compare with the market



fig = go.Figure()
    x = data_.index, y = data_.AccDailyReturn, mode = 'lines', name = '交易策略'
    x = data_.index, y = data_.AccBHReturn, mode = 'lines', name = '買進持有'
    x = data_.index, y = data_.AccMarketReturn, mode = 'lines', name = '市場大盤'
    title = stock_id + '累積報酬圖', yaxis_title = '累積報酬(%)', xaxis_title = '時間'


From the second half of 2021 to the end of 2022, the trend of AUO Corporation(2409) is going down. If taking the “buy and hold” strategy,
Eventually, the accumulated return will be terrible, about -40% to -50%.
On the other hand, if we choose the Bollinger band strategy, the performance will be better than “buy and hold.” Besides, although AUO Corporation’s (2409) price trend is worse than the market average price trend during the observation period, the return performance of the Bollinger band strategy beats the Market.

However, this is the simplest Bollinger band strategy. We can find out that during the price recovery interval, which is just after the vast falling period, we will face the predicament of selling stock too early; Further, during the upward trend, there is prone to encounter the other predicament of seldom buying.

In light of the above predicaments, combining the strategy with other price-trend-detecting indicators is recommended for a considerable fluctuation investment target to optimize the strategy.

Last but not least, please note that “Stocks this article mentions are just for the discussion, please do not consider it to be any recommendations or suggestions for investment or products.” Hence, if you are interested in issues like Creating Trading Strategy , Performance Backtesting , Evidence-based research , welcome to purchase the plans offered in TEJ E Shop and use the well-complete database to create your own optimal trading strategy.

Source Code

Related Link

TEJ E-Shop