【Application】Bollinger Bands Trading Strategy
Using Python to construct Bollinger Bands Trading Strategy and backtesting with the historical price.
Keyword:bollinger band、backtesting
Highlights
Difficulties:★☆☆☆☆
Using the Moving Average and standard deviation to construct a Bollinger Band, determine when to buy and sell.
Preface
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.
Strategy:
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 plotly.express as px
import plotly.graph_objects as go
tejapi.ApiConfig.api_key = "Your Key"
tejapi.ApiConfig.ignoretz = True
Database
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,
x=data.index,
y=["close_d","bbu20","bbl20"],
color_discrete_sequence = px.colors.qualitative.Vivid
)
fig.show()
Trading Strategy
We define some parameter here.
● principal:the total amount of funds invested by investors.
● position:the amount of unit 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(
pd.Series(
[
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(
pd.Series(
[
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(
pd.Series(
[
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.
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.
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':
tmp_period.append(trade_book_["mdate"][i])
tmp_return.append(trade_book_['CashFlow'][i])
else:
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()
fig.add_trace(go.Scatter(
x = data_.index, y = data_.AccDailyReturn, mode = 'lines', name = '交易策略'
))
fig.add_trace(go.Scatter(
x = data_.index, y = data_.AccBHReturn, mode = 'lines', name = '買進持有'
))
fig.add_trace(go.Scatter(
x = data_.index, y = data_.AccMarketReturn, mode = 'lines', name = '市場大盤'
))
fig.update_layout(
title = stock_id + '累積報酬圖', yaxis_title = '累積報酬(%)', xaxis_title = '時間'
)
fig.show()
Conclusion
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
Extended Reading
Related Link
You could give us encouragement by …
We will share financial database applications every week.
If you think today’s article is good, you can click on the applause icon once.
If you think it is awesome, you can hold the applause icon until 50 times.
Any feedback is welcome, please feel free to leave a comment below.