Basic Futures Moving Average Trend Strategy in Python

We are going to code up a simple moving average trend strategy based on the excellent book by Andreas Clenow, Following the Trend. Clenow does not provide any code directly, but he is clear enough and the strategy simple enough that it can be coded up in Python pretty quickly.

We will start by assuming we already have clean and adjusted Futures data, which can be a challenge in itself and I will try and cover in another post, so we will just use clean data from Quandl.

This model has two simple indicators, a Fast Moving Average (FMA) and a Slow Moving Average (SMA) where the former is the average price over the past 10 days, and the latter is the average price over the past 100 days. These values are quite arbitrary and you can play around with them, but the message and end result will be quite similar.

The trading rules are as follows:

  • If FMA > SMA = Buy
  • IF FMA < SMA = Sell

Let’s first import the data from Quandl and save it to a folder called ‘data’. (To install Quandl simply type this in your terminal:

pip install quandl

You can save the following as a standalone script and run it when you want to update the data. This ticker only goes to 31 December 2014, but it’s clean futures data, so it’s good enough for this demo.

import quandl

def download_from_quandl(symbol):
    data = quandl.get(symbol)
    return data

def save_data(data,path,name):

if __name__ == "__main__":
    symbol = 'SCF/CME_SP1_FW'
    name = 'sp500'
    path = 'data'
    quandl.ApiConfig.api_key = 'XXX''  # Replace this with your authorisation token from Quandl

    # Download the contracts into the directory
    data = download_from_quandl(symbol)
    save_data(data, path, name)

The main part of our script will import the data, calculate the FMA and SMA, and plot these three data series.

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

sp500 = pd.read_csv('data/sp500.csv', parse_dates=True, index_col='Date')
fast_days = 10
slow_days = 100

FMA = sp500['Settle'].rolling(center=False,window=fast_days, min_periods=fast_days).mean()
SMA = sp500['Settle'].rolling(center=False,window=slow_days, min_periods=slow_days).mean()

data = pd.DataFrame({'price' : sp500['Settle'], 'FMA' : FMA, 'SMA': SMA})

plt.legend(data.columns, loc=2)
plt.title('S&P 500 price with FMA and SMA')

Which (hopefully) generates the following plot:


What does this strategy look like if we actually implement it using the trading rule described above?

Let’s implement it using the following code:

data['FMA-SMA'] = data['FMA'] - data['SMA']
Threshold = 0
data['Regime'] = np.where(data['FMA-SMA'] > Threshold,1,0)
data['Regime'] = np.where(data['FMA-SMA'] < -Threshold,-1,data['Regime'])

data['Market'] = np.log(data['price'] / data['price'].shift(1))
data['Strategy'] = data['Regime'].shift(1) * data['Market']

plt.plot(data[['Market', 'Strategy']].cumsum().apply(np.exp))
plt.legend(data[['Market', 'Strategy']].columns, loc=2)
plt.title('Trend following strategy on the S&P 500')

The line:

In[1]: data['Regime'].value_counts()
 1    1678
-1     724
 0      99

Shows us that the model has 1678 days of “Long” signal, 724 days of ‘Short” signal and 99 days of neither.

Which generates this:


It looks like this basic strategy would have lagged the S&P 500 for quite a few years until the GFC kicked in. It then managed to greatly outperform the index by shorting it; with this outperformance lasting for 3 or so years. Once the market started whipsawing around at the end of 2011, the strategy struggled. It essentially broke even with the index by the start of 2015.

A few basic stats on both strategies:

 Annual ReturnAnnual VolatilityDownside VolAnnual SharpeMax Drawdown

They are pretty much identical under all aspects apart from the drawdown.

I have included a “Threshold” parameter in the code above which allows you to tweak how big of a price difference is required before an actual position is taken. This will reduce transaction costs (which we have ignored here for simplicity) somewhat and avoid a situation where the price is forcing you to open and close a position often by eliminating false positives.

Setting “Threshold” to 10 reduces the number of days the strategy is invested:

In[1]: data['Regime'].value_counts()
 1    1570
-1     628
 0     303

The strategy now performs as follows:

 Annual ReturnAnnual VolatilityDownside VolAnnual SharpeMax Drawdown

Even with such a simple tweak we have improved on our basic model. As this threshold value is absolute rather than relative, we could look into making this a dynamic value which adjusts based on the price of the S&P 500, say 5%. We can explore this in another post.

I hope you found this post useful. If you have questions about any aspect of this project (code, theory etc), please shoot me an email and I will do my best to get back to you as soon as possible.


  • Andrews Reply 13/08/2018

    Hi Chris,

    Great article. Have a Question: The clean Quandl futures is unadjusted prices, so the returns definitions is possibly incorrect, yes?


    • Christian Contino 14/08/2018

      Hi Andrew,
      I believe this is actually a clean series from Quandl with the Futures chains attached together, so it should be correct. Thanks for checking out my blog, not that there is much on it at the moment! 🙂

  • Me Reply 14/02/2019

    Hi Chris,

    have you imported numpy as np?


Leave a comment