Algo Trading with REST API and Python – Developing a Bollinger Band ADX Range Strategy

(To download an already completed copy of the Python strategy developed in this guide, visit our GitHub.)

In this article, we will code a closed-bar Bollinger band ADX range strategy using Python and FXCM’s Rest API. This strategy buys when price breaks below the lower Bollinger band and sells when price breaks above the upper Bollinger, but only when ADX is below 25. Limit orders are set at the middle Bollinger band with an equidistant stop. Limit orders are updated to match the middle Bollinger at the close of each bar. Parameters will include symbol/instrument, timeframe, Bollinger bands periods & standard deviation, ADX periods, “adx_trade_below” (the level ADX must be below in order to open a position), and lot size.

Step 1. Download our “Python Strategy Template.py” file.

If you already have a copy of the “Python Strategy Template.py” you can go to Step 2. In a previous article, we developed a strategy template that makes future strategy development much easier to accomplish. We recommend readers go through this article at least once to understand how it works.

You may also download our template from our GitHub.

Step 2. Import Bollinger Band and ADX logic from pyti.

In our template, we already import the fxcmpy, time, and datetime modules. For this strategy, we also need to import Bollinger band and ADX logic using a module called pyti. If you have never used pyti before, you will want to make sure this module is installed on your machine by opening up a command prompt and running the command “pip install pyti”. The screenshot below shows I already have pyti installed.

We now need to import the Bollinger Band (BB) and ADX logic into our code. We will add this just below our other import statements.

import fxcmpy
import time
import datetime as dt
import pyti.bollinger_bands as bb
from pyti.directional_indicators import average_directional_index as adx

Step 3. Add User Parameters

We need to add 5 additional parameters to give our BB ADX range strategy all the inputs it needs. For our BB calculation, we will need the number of periods and the standard deviation we want to use. For our ADX calculation, we need the number of periods we want to use as well as “adx_trade_below,” the value that ADX must be under in order to open a position. We also want to add a parameter for trade size called “amount.”

###### USER PARAMETERS ######
token = 'INSERT-TOKEN-HERE'
symbol = 'EUR/USD'
timeframe = "m30"	        # (m1,m5,m15,m30,H1,H2,H3,H4,H6,H8,D1,W1,M1)
bb_periods = 20
bb_standard_deviations = 2.0
adx_periods = 14
adx_trade_below = 25
amount = 5
#############################

Step 4. Add enter(), exit(), crossedOver(), crossesUnder() and countOpenTrades() functions.

Before we write our trading logic inside the Update() function, there are a few ‘utility’ functions we need to add to our code that will make writing our strategy’s logic much simpler.

The enter() Function

First, let’s add the enter() function. When called, the enter() function places a market order. To place a Buy market order, use enter(“B”). For a Sell market order, use enter(“S”). This function also allows arguments for both stop and limit price levels. These two values will be passed through from the Update() function whenever a trade signal is created.

# This function places a market order in the direction BuySell, "B" = Buy, "S" = Sell, uses symbol, amount
# Edited to include stop and limit levels determined by BB from the Update() function
def enter(BuySell, stop, limit):
    direction = True;
    if BuySell == "S":
        direction = False;
    try:
        opentrade = con.open_trade(symbol=symbol, is_buy=direction,amount=amount, time_in_force='GTC',order_type='AtMarket',is_in_pips=False,limit=limit, stop=stop)
    except:
        print("	  Error Opening Trade.")
    else:
        print("	  Trade Opened Successfully.")

The exit() Function

Next, let’s add the exit() function. It works very similarly to the enter function. To close out all buy trades for our traded symbol, call exit(“B”). To close out all sell trades for our traded symbol, call exit(“S”). If we want to close out both buy and sell trades, we just call exit() with no arguments.

# This function closes all positions that are in the direction BuySell, "B" = Close All Buy Positions, "S" = Close All Sell Positions, uses symbol
def exit(BuySell=None):
    openpositions = con.get_open_positions(kind='list')
    isbuy = True
    if BuySell == "S":
        isbuy = False
    for position in openpositions:
        if position['currency'] == symbol:
            if BuySell is None or position['isBuy'] == isbuy:
                print("   Closing tradeID: " + position['tradeId'])
                try:
                    closetrade = con.close_trade(trade_id=position['tradeId'], amount=position['amountK'])
                except:
                    print("   Error Closing Trade.")
                else:
                    print("   Trade Closed Successfully.")

The crossesOver() Function

The next function is a workhorse for many different strategies. crossesOver() checks to see if one data stream’s value crossed over another data stream’s value in the previous candle/bar. When we execute the crossesOver() function with the instrument’s close price data as stream1 and the upper Bollinger band data stream as stream2, the function will return a True value if the closing price crossed over the upper Bollinger band in the previous candle. It will return false if it did not crossover.

# Returns true if stream1 crossed over stream2 in most recent candle, stream2 can be integer/float or data array
def crossesOver(stream1, stream2):
    # If stream2 is an int or float, check if stream1 has crossed over that fixed number
    if isinstance(stream2, int) or isinstance(stream2, float):
        if stream1[len(stream1)-1] <= stream2:
            return False
        else:
            if stream1[len(stream1)-2] > stream2:
                return False
            elif stream1[len(stream1)-2] < stream2:
                return True
            else:
                x = 2
                while stream1[len(stream1)-x] == stream2:
                    x = x + 1
                if stream1[len(stream1)-x] < stream2:
                    return True
                else:
                    return False
    # Check if stream1 has crossed over stream2
    else:
        if stream1[len(stream1)-1] <= stream2[len(stream2)-1]:
            return False
        else:
            if stream1[len(stream1)-2] > stream2[len(stream2)-2]:
                return False
            elif stream1[len(stream1)-2] < stream2[len(stream2)-2]:
                return True
            else:
                x = 2
                while stream1[len(stream1)-x] == stream2[len(stream2)-x]:
                    x = x + 1
                if stream1[len(stream1)-x] < stream2[len(stream2)-x]:
                    return True
                else:
                    return False

The crossesUnder() Function

This function is the mirror opposite of crossesOver(). It returns a True value if the first data stream crossed under the second; returns false if it did not crossunder.

# Returns true if stream1 crossed under stream2 in most recent candle, stream2 can be integer/float or data array
def crossesUnder(stream1, stream2):
    # If stream2 is an int or float, check if stream1 has crossed under that fixed number
    if isinstance(stream2, int) or isinstance(stream2, float):
        if stream1[len(stream1)-1] >= stream2:
            return False
        else:
            if stream1[len(stream1)-2] < stream2:
                return False
            elif stream1[len(stream1)-2] > stream2:
                return True
            else:
                x = 2
                while stream1[len(stream1)-x] == stream2:
                    x = x + 1
                if stream1[len(stream1)-x] > stream2:
                    return True
                else:
                    return False
    # Check if stream1 has crossed under stream2
    else:
        if stream1[len(stream1)-1] >= stream2[len(stream2)-1]:
            return False
        else:
            if stream1[len(stream1)-2] < stream2[len(stream2)-2]:
                return False
            elif stream1[len(stream1)-2] > stream2[len(stream2)-2]:
                return True
            else:
                x = 2
                while stream1[len(stream1)-x] == stream2[len(stream2)-x]:
                    x = x + 1
                if stream1[len(stream1)-x] > stream2[len(stream2)-x]:
                    return True
                else:
                    return False

The countOpenTrades() Function

The final utility function we will add is the counOpenTrades() function. As the name suggests, this function counts how many open positions we have in our account for the strategy’s symbol. Calling countOpenTrades(“B”) returns the number of open buy trades/tickets, countOpenTrades(“S”) returns the number of open sell trades/tickets, countOpenTrades() returns the number of all open trades/tickets.

# Returns number of Open Positions for symbol in the direction BuySell, returns total number of both Buy and Sell positions if no direction is specified
def countOpenTrades(BuySell=None):      
    openpositions = con.get_open_positions(kind='list')
    isbuy = True
    counter = 0
    if BuySell == "S":
        isbuy = False
    for position in openpositions:
        if position['currency'] == symbol:
            if BuySell is None or position['isBuy'] == isbuy:
                counter+=1
    return counter

Step 5. Writing our Trading Logic Inside the Update() Function

The Update() function is processed every time a candle/bar closes and will determine when we open a trade as well as where stops and limits are set. The first thing we want to do is calculate our Bollinger band and ADX streams. We can easily do this by calling our bb and adx calculation module (that we imported via pyti). We will print the most recent close price along with our Bollinger bands and ADX values so we can visually confirm that our strategy is updating properly.

# This function is run every time a candle closes
def Update():
    print(str(dt.datetime.now()) + "	 " + timeframe + " Bar Closed - Running Update Function...")

    # Calculate Indicators
    iBBUpper = bb.upper_bollinger_band(pricedata['bidclose'], bb_periods, bb_standard_deviations)
    iBBMiddle = bb.middle_bollinger_band(pricedata['bidclose'], bb_periods, bb_standard_deviations)
    iBBLower = bb.lower_bollinger_band(pricedata['bidclose'], bb_periods, bb_standard_deviations)
    iADX = adx(pricedata['bidclose'], pricedata['bidhigh'], pricedata['bidlow'], adx_periods)
	
    # Declare simplified variable names for most recent close candle
    close_price = pricedata['bidclose'][len(pricedata)-1]
    BBUpper = iBBUpper[len(iBBUpper)-1]
    BBMiddle = iBBMiddle[len(iBBMiddle)-1]
    BBLower = iBBLower[len(iBBLower)-1]
    ADX = iADX[len(iADX)-1]
	
    # Print Price/Indicators
    print("Close Price: " + str(close_price))
    print("Upper BB: " + str(BBUpper))
    print("Middle BB: " + str(BBMiddle))
    print("Lower BB: " + str(BBLower))
    print("ADX: " + str(ADX))

Next, we need to code our trading logic. The first logic we want to code to happen when a candle closes is to update any existing trades’ limit orders to be equal to the middle Bollinger bands. But we only want this portion of code to run if countOpenTrades() is larger than 0 (meaning only run if we have an open trade.) If we have an open trade, loop through all trades, and if the trade equals the symbol our strategy is trading, change the limit order to be equal to the middle Bollinger band.

    # TRADING LOGIC
	
    # Change Any Existing Trades' Limits to Middle Bollinger Band
    if countOpenTrades()>0:
        openpositions = con.get_open_positions(kind='list')
        for position in openpositions:
            if position['currency'] == symbol:
                print("Changing Limit for tradeID: " + position['tradeId'])
                try:
                    editlimit = con.change_trade_stop_limit(trade_id=position['tradeId'], is_stop=False, rate=BBMiddle, is_in_pips=False)
                except:
                    print("	  Error Changing Limit.")
                else:
	            print("	  Limit Changed Successfully.")

For the entry logic, we first need to filter out candles where ADX is greater than adx_trade_below. So, if ADX is less than adx_trade_below, we move on to placing a buy or sell order based on the following logic. Notice that the stop and limit values are passed through to our custom enter() function as well.

    # Entry Logic
    if ADX < adx_trade_below:
        if countOpenTrades("B") == 0 and close_price < BBLower: print(" BUY SIGNAL!") print(" Opening Buy Trade...") stop = pricedata['askclose'][len(pricedata)-1] - (BBMiddle - pricedata['askclose'][len(pricedata)-1]) limit = BBMiddle enter("B", stop, limit) if countOpenTrades("S") == 0 and close_price > BBUpper:
            print("	  SELL SIGNAL!")
            print("	  Opening Sell Trade...")
            stop = pricedata['bidclose'][len(pricedata)-1] + (pricedata['bidclose'][len(pricedata)-1] - BBMiddle)
            limit = BBMiddle
            enter("S", stop, limit)

For the exit logic, we want to ensure that all buy trades are properly closed out when price crosses above the middle Bollinger band, and that all sell trades are properly closed out when price crosses below the middle Bollinger band.

    # Exit Logic
    if countOpenTrades("B") > 0 and close_price > BBMiddle:
        print("	  Closing Buy Trade(s)...")
        exit("B")
    if countOpenTrades("S") > 0 and close_price < BBMiddle:
        print("	  Closing Sell Trade(s)...")
        exit("S")

    print(str(dt.datetime.now()) + "	 " + timeframe + " Update Function Completed.\n")

Step 6. Run our strategy inside our command console.

The last step is to run our strategy inside our command console. The python file we created I saved on to my desktop, so I execute the strategy by calling it like this:

The strategy is now up and running and will open and close trades per our rules!

What Next?

This BB ADX strategy is a fairly basic range trading strategy, but there are definitely ways it can be expanded upon and improved. Please edit and make this strategy your own! Also, make sure to check out our other Python strategies we have available at https://github.com/fxcm/RestAPI


Risk Warning: The FXCM Group does not guarantee accuracy and will not accept liability for any loss or damage which arise directly or indirectly from use of or reliance on information contained within the webinars. The FXCM Group may provide general commentary which is not intended as investment advice and must not be construed as such. FX/CFD trading carries a risk of losses in excess of your deposited funds and may not be suitable for all investors. Please ensure that you fully understand the risks involved.