Algo Trading with REST API and Python – Developing a SMA Crossover Trend 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 Simple Moving Average (SMA) crossover strategy using Python and FXCM’s Rest API. This strategy will buy when the Fast SMA crosses over the Slow SMA, and sell when the Fast SMA crosses under the Slow SMA. Parameters will include symbol/instrument, timeframe, Fast & Slow SMA Periods, close on opposing signal switch, lot size, and stop/limit distance.

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 SMA logic from pyti.

In our template, we already import the fxcmpy, time, and datetime modules. For this strategy, we also need to import the Simple Moving Average 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 SMA logic into our code. We will add this just below our other import statements.

import fxcmpy
import time
import datetime as dt
from pyti.simple_moving_average import simple_moving_average as sma

Step 3. Add User Parameters

We need to add 6 additional parameters to give our SMA crossover strategy all the inputs it needs. For our SMA logic, we will need Fast and Slow SMA Periods as well as a switch named “close_on_opposing_signal” that will allow to choose how we want the strategy to handle trades when an opposing signal occurs. We also want to add parameters for trade size, stoploss distance and limit distance.

###### USER PARAMETERS ######
token = 'INSERT-TOKEN-HERE'
symbol = 'GBP/USD'
timeframe = "m1" # (m1,m5,m15,m30,H1,H2,H3,H4,H6,H8,D1,W1,M1)
fast_sma_periods = 10
slow_sma_periods = 20
close_on_opposing_signal = True
amount = 1
stop = -10
limit = 30
#############################

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”). The function is already coded to accept the amount, stop & limit parameters we created in our User Parameters section.

# This function places a market order in the direction BuySell, "B" = Buy, "S" = Sell, uses symbol, amount, stop, limit
def enter(BuySell):
    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=True,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 Fast SMA’s data stream as stream1 and the Slow SMA’s data stream as stream2, the function will return a True value if the Fast SMA crossed over the Slow SMA in the prevous 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 when they are closed. The first thing we want to do is calculate our Fast SMA and Slow SMA streams. We can easily do this by calling our sma calculation module (that we imported via pyti). We will also print the most recent close price and SMA 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
	iFastSMA = sma(pricedata['bidclose'], fast_sma_periods)
	iSlowSMA = sma(pricedata['bidclose'], slow_sma_periods)
	
	# Print Price/Indicators
	print("Close Price: " + str(pricedata['bidclose'][len(pricedata)-1]))
	print("Fast SMA: " + str(iFastSMA[len(iFastSMA)-1]))
	print("Slow SMA: " + str(iSlowSMA[len(iSlowSMA)-1]))

Next, we need to code our entry logic. If the Fast SMA crosses over the Slow SMA, we buy. If the Fast SMA crosses under the Slow SMA, we sell. We will use our crossesOver(), crossesUnder() and enter() functions. Also, note that we want to exit opposing positions if close_on_opposing_signal equals True and an opposing position exists. We use the countOpenTrades() function to see if we have any opposing positions and the exit() function to close them out if we do.

	# TRADING LOGIC
	# If Fast SMA crosses over Slow SMA, Open Buy Trade
	if crossesOver(iFastSMA,iSlowSMA):
		print("	  BUY SIGNAL!")
		if close_on_opposing_signal and countOpenTrades("S") > 0:
			print("	  Closing Sell Trade(s)...")
			exit("S")
		print("	  Opening Buy Trade...")
		enter("B")
	# If Fast SMA crosses under Slow SMA, Open Sell Trade
	if crossesUnder(iFastSMA,iSlowSMA):
		print("	  SELL SIGNAL!")
		if close_on_opposing_signal and countOpenTrades("B") > 0:
			print("	  Closing Buy Trade(s)...")
			exit("B")
		print("	  Opening Sell Trade...")
		enter("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 SMA Crossover strategy is a classic trend 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.