Algo Trading with REST API and Python – Developing a RSI 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 RSI strategy using Python and FXCM’s Rest API. This strategy will buy when RSI crosses over 30, closing buy trades when RSI crosses above 70. This strategy will sell when RSI crosses below 70, closing sell trades when RSI crosses below 30. Parameters will include symbol/instrument, timeframe, RSI periods, 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 RSI logic from pyti.

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

import fxcmpy
import time
import datetime as dt
from pyti.relative_strength_index import relative_strength_index as rsi

Step 3. Add User Parameters

We need to add 6 additional parameters to give our RSI strategy all the inputs it needs. For our RSI indicator, we will need RSI Periods as well as upper and lower RSI levels that will trigger our trades. We also want to add parameters for trade size, stoploss distance and limit distance. Because our strategy will close out trades when RSI reaches an opposing extreme, I will make our limit “None” by default.

###### USER PARAMETERS ######
token = 'INSERT-TOKEN-HERE'
symbol = 'EUR/USD'
timeframe = "m15"           # (m1,m5,m15,m30,H1,H2,H3,H4,H6,H8,D1,W1,M1)
rsi_periods = 14
upper_rsi = 70.0
lower_rsi = 30.0
amount = 1
stop = -20
limit = None
#############################

Step 4. Add enter(), exit(), crossesOver(), 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 simple.

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. It also can check to see if a data stream’s value crossed over a static value the previous candle/bar. Most of our RSI strategy’s logic has to do with RSI crossing our chosen upper and lower levels. This will be one of the functions we use to do that. We will put our RSI data stream as crossesOver()’s first argument, and then 30 or 70 as our second argument, depending on where we are using the function. It will return a True value if the first data stream crossed over the second; returns 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. Just like crossesOver(), the first argument must be a data array, the second can be either a data array or a int/float.

# 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 RSI stream. We can easily do this by calling our rsi calculation module (that we imported via pyti). We will also print the most recent close price and RSI 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
    iRSI = rsi(pricedata['bidclose'], rsi_periods)
    
    # Print Price/Indicators
    print("Close Price: " + str(pricedata['bidclose'][len(pricedata)-1]))
    print("RSI: " + str(iRSI[len(iRSI)-1]))
    
    # TRADING LOGIC

    # Entry Logic
    # If RSI crosses over lower_rsi, Open Buy Trade
    if crossesOver(iRSI, lower_rsi):
        print("   BUY SIGNAL!")
        print("   Opening Buy Trade...")
        enter("B")
    # If RSI crosses under upper_rsi, Open Sell Trade
    if crossesUnder(iRSI, upper_rsi):
        print("   SELL SIGNAL!")
        print("   Opening Sell Trade...")
        enter("S")
    
    # Exit Logic
    # If RSI is greater than upper_rsi and we have Buy Trade(s), Close Buy Trade(s)
    if iRSI[len(iRSI)-1] > upper_rsi and countOpenTrades("B") > 0:
        print("   RSI above " + str(upper_rsi) + ". Closing Buy Trade(s)...")
        exit("B")
    # If RSI is less than than lower_rsi and we have Sell Trade(s), Close Sell Trade(s)
    if iRSI[len(iRSI)-1] < lower_rsi and countOpenTrades("S") > 0:
        print("   RSI below " + str(lower_rsi) + ". 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 RSI strategy is a classic 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.