A Simple Way to Backtest a Strategy in Python
Import Libraries, Retrieve Prices, and Define the Strategy
In this tutorial we will create an EMA strategy, backtest the strategy using an FXCM demo account, and chart the returns. We will use three open-source python libraries: fxcmpy for importing data, pyti for the EMA indicator, and matplotlib for charting. The EMA (exponential moving average) is a type of moving average, which measures the mean price of an asset over time, that includes an added calculation to account for the most recent changes in price. For demonstrative purposes we will use a short EMA and a long EMA for our signal. If the short EMA (the mean price over the short term) crosses above the long EMA (the mean price over the longer term) we will open a long position.
To begin, we will import the first set of libraries:
import fxcmpy import pandas as pd import numpy as np import datetime as dt from pyti.exponential_moving_average import exponential_moving_average as ema #allows for printing the whole data frame pd.set_option('display.max_columns', None) pd.set_option('display.max_rows', None)
Next we need to establish a connection to FXCM’s REST API. The API has streaming capability and will push data in JSON format, so we can stream FXCM’s real-time market data, retrieve historical price, and place live trades. We will use a config file to store the account access token for a secure connection to the API. To learn how to create a token, watch this tutorial.
con = fxcmpy.fxcmpy(config_file='fxcm.cfg')
Then, we will pull the data for the time period over which we want to backtest the strategy. fxcmpy allows us to use the following command:
df = con.get_candles('EUR/USD', period = 'D1', start = dt.datetime(2016, 1, 1), end = dt.datetime(2018, 7, 1))
For this strategy, remember that we want a buy signal when the EMA fast crosses above the EMA slow, and we plan to open a 10k EURUSD trade when the signal is triggered. To define the strategy we will first need to enter the pip value and the trade size. In FX, a pip is what you would consider a “point” for calculating profits and losses. Since a pip reflects a change in the counter currency and in this example USD is the counter currency, we can use this formula to calculate the per-pip value: trade size × one pip movement of the pair = 10,000 × 0.0001 USD = 1.00 USD per pip for EUR/USD. Next we will define the parameters. For demonstrative purposes, we will use a 12-period EMA for ema_fast and 20-period EMA for ema_slow. Then we’ll need to populate our dataframe with the EMA data, and define the the strategy logic. Finally, we can view the dataframe we have just created.
pip_cost = 1 lot_size = 10 #define parameters ema_fast = 12 ema_slow = 20 #populate dataframe df['mva_fast'] = ema(df['askclose'], ema_fast) df['mva_slow'] = ema(df['askclose'], ema_slow) #define strategy logic df['signal'] = np.where(df['mva_fast'] > df['mva_slow'],1,0) df['position'] = df['signal'].diff() #view data frame df
*mva_fast and mva_slow will not populate for the first 14 periods because 14 periods are required to calculate the EMA, so NaN is not an error.
Using matplotlib, we can perform a quick sanity check to ensure the signals are being created as intended. We will create a chart showing EUR/USD price (red), the fast EMA signal line (blue) and the slow EMA signal line (orange), and add markers to indicate where the trading signals occurred. We can see trades are being signaled when the EMA lines are crossing, and the EMA lines are trailing price, so it looks like the signal is working.
#import the matplotlib library import matplotlib.pyplot as plt %matplotlib inline #create chart settings fig = plt.figure(figsize=(12,8)) ax1 = fig.add_subplot(111, ylabel='EUR/USD Price') # Plotting market prices and moving averages df['askclose'].plot(ax=ax1, color='r', lw=1.) df[['mva_fast', 'mva_slow']].plot(ax=ax1, lw=2.) # Placing markers for our position entry ax1.plot(df.loc[df.position == 1.0].index, df.mva_fast[df.position == 1.0], '^', markersize=10, color='m') # Placing markers for our position exit ax1.plot(df.loc[df.position == -1.0].index, df.mva_slow[df.position == -1.0], 'v', markersize=10, color='k') plt.show()
A Simple Backtest for Our Strategy
Now that we’ve confirmed the signals are being triggered, let’s backtest the strategy to see what our returns would have been using this signal. First, we will create the difference (pips) column which will find the total pip movement for the day, and use that to create a daily profit/loss column, p/l. The daily profit or loss is added to the previous day’s returns, populating the total column.
returns = 0 # Gets the number of pips that the market moved during the day df['difference (pips)'] = (df['askclose'] - df['askopen']) * 100 df['p/l'] = (df['difference (pips)']) * pip_cost * lot_size # Calculates the daily return while a position is active # 'Total' column records our running profit / loss for the strategy for i, row in df.iterrows(): if row['signal'] == 1: returns += (row['difference (pips)'] * pip_cost * lot_size) df.loc[i,'total'] = returns else: df.loc[i,'total'] = returns
We can visualize our returns using matplotlib:
# Plotting our returns import matplotlib.pyplot as plt %matplotlib inline fig = plt.figure(figsize=(12,8)) ax1 = fig.add_subplot(111, ylabel='Profits in $') df['total'].plot(ax=ax1, color='r', lw=1.) # Placing markers for our position entry ax1.plot(df.loc[df.position == 1.0].index, df.total[df.position == 1.0], '^', markersize=10, color='m') # Placing markers for our position exit ax1.plot(df.loc[df.position == -1.0].index, df.total[df.position == -1.0], 'v', markersize=10, color='k') plt.show()
In short, we created a strategy which considers the exponential moving average of the price of EUR/USD to trigger trading signals. We then backtested the strategy and created a chart to track cumulative performance. While this was a very simple example, it can be used as a framework to build and backtest a more complex strategy in just a few minutes. To learn more about the EMA strategy, read this article. To see more technical indicators one could use to build a strategy, view this github library.
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.