Bollinger Band Trade Examples Part 1.

Using MetaTrader 5 and Python with Jupyter Notebooks.

Ben Cole
8 min readJan 8, 2023

Week 1, January 2022

DISCLAIMER

This post is for entertainment and educational purposes and does not constitute trading, financial or programming advice. Trading in the financial markets imposes a risk of financial losses and I am not responsible for any such losses that the viewers sustain. Past trading results are not an indication of future trading performance.

THE AIM

In this post I will begin a process of explaining, demonstrating and evaluating examples of a strategy called Bollinger Bands with a view for traders to see the beginning middle and end of the trading process before they begin a long road to achieving their own trading success.

THE REQUIREMENTS

These are the libraries, systems and software requirements to follow along with this tutorial.

You will need to download Python (I am running 3.8.10) and all versions are available at https://www.python.org/downloads/

Alternatively you might want to download Anaconda which is a fantastic open-source suite of Data Science packages and is available to download here https://repo.anaconda.com/archive/Anaconda3-2022.10-Windows-x86_64.exe

You will need to download and sign up to a MetaTrader 5 account and you can use the following link to do this https://download.mql5.com/cdn/web/metaquotes.software.corp/mt5/mt5setup.exe?utm_source=www.metatrader5.com&utm_campaign=download

BOLLINGER BANDS

The Bollinger Band is a technical analysis tool defined by trendlines plotted two standard deviations (negative and positive) away from a simple moving average of a security’s price. For our example we are going to use the standard settings of 20 SMA and a STANDARD DEVIATION of plus and minus 2 on the EURUSD Daily chart.

Bollinger Bands highlighted in Aqua

BACK TESTING

Back testing a system or strategy is one of, if not THE most important part of the strategy life cycle as this is where you will develop the knowledge of what your expected win rate, profit and loss totals and also the smaller details such as reward to risk ratio and largest win and loss amount will likely be going forward but once again I must stress past results is not an indication of future performance.

In your Jupyter Notebook you should import the libraries required to run the following code snippet below.

import MetaTrader5 as mt5
import pandas as pd
import numpy as np
import plotly.express as px
from datetime import datetime

If after running the cell using run (or shift and enter) any of the following libraries return an error you should install each missing one using PIP.

Once this is complete we will need to make a connection to MetaTrader 5 platform and if necessary login using your own login, password and server details.

mt5.initialize()

login = 12345678
password = "yourownpassword"
server = "EXAMPLE-SERVER"

mt5.login(login, password, server)

The output of running the cell above should be True, in which case you have successfully logged in and initialized your trading account. The next action will be to download historical values to use as a dataset for your strategy back test.

ohlc = mt5.copy_rates_range("EURUSD",
mt5.TIMEFRAME_M1,
datetime(2023, 1, 1),
datetime.now())

ohlc

The first parameter within the ohlc request is the symbol, which in the above example is “EURUSD” for the euro and dollar currency pair. The next parameter is the timeframe and this is set to minute candles using the M1 code. Other timeframes would be available by using H1 for hourly candles or D1 for the daily candles. After this we have the two date ranges of from and until dates. I have used a very small time period but please be aware that the longer the back test the better the analysis but obviously the longer the processing time and data may not be available past a certain date with your broker supplying the data. The output of this cell will be an array of datetime, open, high, low, close, volume and spread. This data is often referred to as OHLC data which is the open, high, low and close prices for each of the specified candles, in our case one minute candles (M1).

We will convert this array into a pandas dataframe to make further analysis easier for us using the following code.

df = pd.DataFrame(ohlc)

df

Once completed we should convert the datetime into a datetime format using the code below.

df['time'] = pd.to_datetime(df['time'], unit = 's')

df

Once the cell has run it will display the time column in a standard format. Now we have our data we can plot the close prices to see our data visualised as a line chart using the following code snippet.

fig_1 = px.line(df, x = 'time', y = 'close', title = "EURUSD Close Price")

fig_1

The output from this cell will produce a line chart plotting the close price against the time axis, which is what we need to begin plotting our Bollinger Band indicator. We will need to calculate the SMA, Standard Deviation, Lower Band and Upper Band and add these values to our dataframe.

sma_value = 20
sd_value = 2

df['sma'] = df['close'].rolling(sma_value).mean()

df['sd'] = df['close'].rolling(sma_value).std()

df['lb'] = df['sma'] - sd_value * df['sd']

df['ub'] = df['sma'] + sd_value * df['sd']

df.dropna(inplace = True)

df

After running the cell above the dataframe output will contain four new columns (sma, sd, lb & ub) and the index now starts at 19 as a result of dropping the first 20 rows of data whilst the sma mean was calculated.

The next code cell will visualise the Bollinger Band envelope by plotting the new columns and close price as follows.

fig_2 = px.line(df, x = 'time', y = ['close', 'sma', 'lb', 'ub'], 
title = "EURUSD Bollinger Bands (20, 2, 2)"))

fig_2

You have successfully plotted your own calculated Bollinger Bands onto data you have downloaded, so we are now ready to start finding signals for our back testing of the strategy using Python.

We will now write a function to create a signal and populate a new column in our dataframe with the Buy, Sell or Flat signal value.

def find_signal(close, lowerbb, upperbb):
if close < lowerbb:
return 'buy'
elif close > upperbb:
return 'sell'
else:
return 'flat'

df['signal'] = np.vectorize(find_signal)(df['close'], df['lb'], df['ub'])

df

In the latest version of our dataframe you will see a column labelled signal which will now be populated with the values from our signal function. The next part of our process will be to create both a position class and strategy class to perform our back test with.

class Position:
def __init__(self, open_datetime, open_price, order_type, volume, sl, tp):
self.open_datetime = open_datetime
self.open_price = open_price
self.order_type = order_type
self.volume = volume
self.sl = sl
self.tp = tp
self.close_datetime = None
self.close_price = None
self.profit = None
self.status = 'open'

def close_position(self, close_datetime, close_price):
self.close_datetime = close_datetime
self.close_price = close_price
self.profit = (self.close_price - self.open_price) * self.volume
if self.order_type == 'buy'
else (self.open_price - self.close_price) * self.volume
self.status = 'closed'

def _asdict(self):
return {
'open_datetime': self.open_datetime,
'open_price': self.open_price,
'order_type': self.order_type,
'volume': self.volume,
'sl': self.sl,
'tp': self.tp,
'close_datetime': self.close_datetime,
'close_price': self.close_price,
'profit': self.profit,
'status': self.status,
}
class Strategy:
def __init__(self, df, starting_balance, volume):
self.starting_balance = starting_balance
self.volume = volume
self.positions = []
self.data = df

def get_positions_df(self):
df = pd.DataFrame([position._asdict() for position in self.positions])
df['p&l'] = df['profit'].cumsum() + self.starting_balance
return df

def add_position(self, position):
self.positions.append(position)

def trading_allowed(self):
for pos in self.positions:
if pos.status == 'open':
return False

return True

def run(self):
for i, data in self.data.iterrows():

if data.signal == 'buy' and self.trading_allowed():
sl = data.close - 1 * data.sd
tp = data.close + 2 * data.sd
self.add_position(Position(data.time, data.close, data.signal,
self.volume, sl, tp))

elif data.signal == 'sell' and self.trading_allowed():
sl = data.close + 1 * data.sd
tp = data.close - 2 * data.sd
self.add_position(Position(data.time, data.close, data.signal,
self.volume, sl, tp))

for pos in self.positions:
if pos.status == 'open':
if (pos.sl >= data.close and pos.order_type == 'buy'):
pos.close_position(data.time, pos.sl)
elif (pos.sl <= data.close and pos.order_type == 'sell'):
pos.close_position(data.time, pos.sl)
elif (pos.tp <= data.close and pos.order_type == 'buy'):
pos.close_position(data.time, pos.tp)
elif (pos.tp >= data.close and pos.order_type == 'sell'):
pos.close_position(data.time, pos.tp)

return self.get_positions_df()

These two code cells will create classes which we can use to back test our Bollinger Bands strategy. Some of the elements within the classes that we should explain are volume which is the volume of purchase using your demo account funds. For example the standard lot of 1.0 equates to 100,000 currency units. ‘sl’ and ‘tp’ refer to “Stop Loss” and “Take Profit” and these will be values of Standard Deviation as a price and either added or subtracted from the close price dependant on signal direction. In the case of this example I have used 1 Standard Deviation for a Stop Loss and 2 Standard Deviations for a Take Profit. This equates to a 1:2 reward to risk ratio but we will still require a certain percentage of winning trades to be a profitable trading strategy.

RUNNING THE BACK TEST

The next action is to use our classes and indicator to run a back test function and display the results. The two variables we need to set for the Strategy are the starting balance, which we will start with $200,000 and the volume per trade which for this back test we will use 1.0 lots or 100,000 currency units.

bollinger_strategy = Strategy(df, 200000, 100000)
result = bollinger_strategy.run()

result

The resulting dataframe will be of positions opened and closed with the details of each trade included in each row of data. The final column is a cumulative profit of our starting balance but a visual representation of this dataframe can be more impactful and can be run using the code snippet below.

fig_2

for i, position in result.iterrows():
if position.status == 'closed':
fig_2.add_shape(type = "line", x0 = position.open_datetime,
y0 = position.open_price,
x1 = position.close_datetime,
y1 = position.close_price,
line = dict(
color = "green" if position.profit >= 0 else "red",
width = 5)
)

fig_2

This line chart will look very busy but using plotly you should be able to zoom into areas of interest to see the individual trades plotted as green lines for profitable trades and red lines as losing trades. If you would prefer to see you profit and loss as a simple line chart this can be achieved using the following code snippet.

px.line(result, x = "close_datetime", y = "p&l", title = "Profit and Loss")

BACK TEST CONCLUSION

So that concludes our back test for this particular strategy & timeframe and over the past week it has yielded a profit but as explained previously this is not a guarantee of future performance and given our dataframe is for a simple week this is by no means a large enough dataset to base a live trading strategy on.

NEXT STEPS

I intend to add a second part to this post very soon in which I will demonstrate the implementation of the Bollinger Band strategy to automate our trades for us, leaving the chance to back test other strategies or currency pairs before moving them forward into a demo account trading environment.

THANK YOU FOR READING

I really do appreciate you taking the time to read this and I would love to hear your feedback and any requests for future articles of this nature or any alternative subjects.

--

--

Ben Cole

Passionate about data analytics, I turn complex data into actionable insights. Using numbers to tell stories, I help businesses make data-driven decisions.