Finding the Best Moving Average Crossover Strategy for Bitcoin

Ape/rture
ChainSlayer
Published in
9 min readDec 21, 2020

The strategy research in this article is meant for educational purposes and is not investment or trading advice. Use of the strategy is at the traders own risk, since backtest performance is not a guarantee for real-time trading performance.

Various trading strategies can be used in cryptocurrency trading. One of the popular strategies is the moving average crossover. This strategy is simple to understand and can be a good indication of momentum in a price chart. Since cryptocurrency markets are often momentum driven, this strategy can help traders trade these markets better. In this article we will explain a method on how to find the optimal moving average strategy for a long only strategy and will show the results based on the previous big run up from 01–07–2020 to 30–11–2020, where the Bitcoin price gained around 110%. The strategy will be optimized based on two moving averages on BTC-USDT data from Binance provided by ChainSlayer. The article will feature some Python code, but feel free to skip the code examples if you are only interested in the principles and the final results.

The 110% increase on BTC visualized in TradingView
The 110% increase visualized in TradingView

Using a Moving Average in Trading

The price of an asset often prints random fluctuations in a chart. By calculating and/or visualizing moving averages price trends can be smoothed. There are several methods for calculating a moving average but the most simple version calculates the average price (often based on the closing price) over a period. This period is a parameter often expressed when mentioning the moving average (MA). The 20 MA takes the last 20 closing prices and averages them, resulting in a new data point. The calculation then moves 1 step up and calculates the next data point. The result is a line that represents this moving average.

The Moving Average Strategy

Traders often use multiple moving averages with different periods. In our strategy we focus on a short period MA and a long period MA. Shorter period MAs react faster to price movements, but are also more prone to deviations in price. Longer period MAs smooth the price movements more, but can be slow to react. These features can be used to create a strategy. If a short period MA crosses over a longer period MA, it means that price is catching momentum and the short period increases outperform the longer period.

The assumption with momentum trading is that once there is momentum, price will keep moving until momentum decreases. The crossover to the upside where the short period MA is above the long period MA is our entry signal. Once the short period MA crosses back below the long period MA we exit the trade since the momentum turned to the downside. In the picture below the entry and exit are visualized for an example.

A long only crossover strategy example with the 20 MA (blue line) and the 150 MA (orange line)

The benefits of a moving strategy is that the strategy:

  • can be traded manually
  • is easy to understand and not overly complex
  • can be used to set alerts based on the crossover, reducing screen time
  • can easily be optimized

Optimizing the Strategy

We can optimize this strategy by running backtests. Backtesting is testing a strategy on past data to get an idea of how this strategy would perform on future data. The results are an indication if the strategy could be viable in real-time trading. For a crossover strategy with two MAs we can compare the results for a range of values for each MA. In our example for the short period MA we chose the range between 10 and 110 in steps of five (10, 15, 20, …, 95, 100, 105) and for the long period MA we chose the range between 20 and 210 in steps of five (20, 25, 30, …, 195, 200, 205). For all combinations we generated the backtest results with the constraint that the short period MA should have a shorter period then the long period MA. From all these backtests we picked the two MAs that had the highest returns.

In the paragraphs below we discuss how this was done by programming the strategy and the optimizer in Python. Skip to The Final Backtest Results paragraph if you are interested in the results.

Optimizing the Strategy in Code

Optimizing a MA crossover strategy manually is a tedious task but optimizing such a strategy is fairly simple if you are familiar with Python and have access to quality market data. The code provided uses the backtesting.py library for the strategy optimization. The data is provided by a database from ChainSlayer. The database is a QuestDB which currently runs in an integrated cloud workspace and can be queried directly from the Jupyter Notebook in the same workspace. Get access to free, up-to-date market data by joining our community on Discord.

Gathering the Data

The data is retrieved from the corresponding table in the ChainSlayer database. We select the start and end date, which are 2020–07–01 to 2020–11–30, and send the query to the database. The result is processed and put in a Pandas DataFrame.

import pandas as pd
import io
import urllib.parse
import requests
# Get data from QuestDB
# Define query q
q = "select timestamp, open, high, low, close from {} where timestamp >= '2020-07-01' and timestamp <= '2020-11-30' ".format("qd_candles_spot_btc_usdt_binance_1h")
# Request data from QuestDB with query q
r = requests.get("http://localhost:9000/exp?query=" + urllib.parse.quote(q))
# Convert returned object r to pandas df
df = pd.read_csv(io.StringIO(r.text))
# Preprocess df:
# Change timestamp column object types from str to pd.datetime
df["timestamp"] = pd.to_datetime(df["timestamp"])
# Rename Columns
df.rename(columns={"timestamp": "Time", "open": "Open", "high": "High", "low": "Low", "close": "Close"}, inplace=True)
# Set column "Time" as the index
df.set_index('Time', inplace= True)
# view df
df.head()
First 5 rows of the DataFrame

Defining the Trading Strategy

Before running the actual backtesting we need to define the strategy and let backtesting.py know what condition we want to use to enter and exit positions. This is defined by creating a class for the trading strategy that outlines the trading rules. As crossovers are a common trading logic for a number of strategies there is a crossover detection function already prebuilt in backtesting.py called crossover() We have also imported that in the below code. In this strategy we use a short term MA and a long term MA. This strategy is long only and we go long when the short term moving average crosses the long term moving average. The 10 and 20 values for the MAs are just placeholders.

from backtesting import Strategy
from backtesting.lib import crossover
# Function to return the SMA
def SMA(values, n):
return pd.Series(values).rolling(n).mean()
class SmaCross(Strategy):
# Define the two MA lags as *class variables*
ma_short = 10
ma_long = 20
df = df

def init(self):
# Precompute the two moving averages
self.sma1 = self.I(SMA, self.df.Close.to_numpy(), self.ma_short)
self.sma2 = self.I(SMA, self.df.Close.to_numpy(), self.ma_long)

def next(self):
# If sma1 crosses above sma2 buy the asset
if crossover(self.sma1, self.sma2):
self.buy()
# Else, if sma1 crosses below sma2 sell the asset
elif crossover(self.sma2, self.sma1):
self.position.close()

Optimizing the Backtest

We can optimize the backtest by using bt.optimize. The result is stored in a heatmap variable we can use to check the results. Keep in mind that the starting capital is $10000. The best result has a 15 period lookback for the short period MA and a 150 lookback for the long period MA. This results in a Equity Final in dollars of $19787.42.

# reminder that we use this Backtest class
bt = Backtest(data=df, strategy=SmaCross, cash=10000, commission=.002)
# evaluate all possible combinations
stats, heatmap = bt.optimize(
ma_short=range(10, 110, 5),
ma_long=range(20, 210, 5),
constraint=lambda p: p.ma_short < p.ma_long,
maximize='Equity Final [$]',
return_heatmap=True)
# check the top 10 returns
heatmap.sort_values(ascending=False).iloc[:10]
Results for the top 10 in Equity Final [$]

Robustness Check for the Results

When optimizing strategies the risk of overfitting is present. We want the strategy also to be robust when we run this in the future. The market never acts exactly like it has done in the past, so we need to account for future random behavior. Luckily there are methods to make our backtesting and final strategy more robust. One of these methods is to add some variations to our parameters and see if the strategy is still profitable.

In our approach we already did something similar by evaluation every combination. We can use a heatmap to visualize which combinations are more profitable. If we find clusters of profitable parameter combinations that means these parameters are more robust.

# group 
hm = heatmap.groupby(['ma_short', 'ma_long']).mean().unstack()
#plot heatmap
%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns
plt.figure(figsize=(12, 10))
sns.heatmap(hm[::-1], cmap='viridis')

As you can see in the heatmap there is a big cluster ranging from 15–40 (ma_short) and from 125–205 (ma_long). It seems that the we can pick the 15 as a the short period MA (ma_short). Since 150 is around the center of the cluster I decided to go with that setting for the long MA (ma_long).

Let’s backtest the results and see how the strategy performed. Overall it looks like we found the optimal moving average crossover strategy for this Bitcoin move! The code below generates the backtest results for our winning strategy. The results are discussed in the next paragraph.

from backtesting import Strategy
from backtesting.lib import crossover
class SmaCross_15_150(Strategy):
# Define the two MA lags as *class variables*
ma_short = 15
ma_long = 150
df = df

def init(self):
# Precompute the two moving averages
self.sma1 = self.I(SMA, self.df.Close.to_numpy(), self.ma_short)
self.sma2 = self.I(SMA, self.df.Close.to_numpy(), self.ma_long)

def next(self):
# If sma1 crosses above sma2 buy the asset
if crossover(self.sma1, self.sma2):
self.buy()
# Else, if sma1 crosses below sma2 sell the asset
elif crossover(self.sma2, self.sma1):
self.position.close()

# run the backtest
bt = Backtest(data=df, strategy=SmaCross_15_150, cash=10000, commission=.002)
bt.run()
# plot the trades
bt.plot()

The Final Backtest Results

We have iterated over several possible moving average periods in our backtest and have found that the combination of the short period MA 15 and for the long period MA 150 gave the best returns. Let’s dive into the results.

We started the backtest with $10000. The Equity Final [$] was $19787.42. That is 9787.42 profit (+97.87%) in 152 days. This strategy is worse then a Buy & Hold approach (+115.92%), but we have less exposure to the market (54.41%), which in general means less risk. The risk of buying and holding is that profit is lost when the trend turns or Bitcoin dumps, while our strategy would give a sell signal at those moments and we won’t be exposed to the markets since profits are realized by exiting the position(s).

Example of the last trade with the 15 MA and 150 MA crossover strategy

Conclusion

Our backtest results show that with a fairly simple strategy you can be profitable in the cryptocurrency markets. By trading a crossover strategy with the 15 moving average and the 150 moving average the +97.87% return in these trending markets is impressive. The biggest portion of the move can be caught, while the market exposure time is reduced. For a real-time trading strategy it is advised to do additional backtesting and backtest on longer periods with more data.

The market data was provided by ChainSlayer. If you want to get FREE access to high quality historical and up-to-date cryptocurrency market data, feel free to join our Discord at https://discord.com/invite/r57denE. The goal of the community in the Discord server is to exchange ideas and collaborate on interesting quantitative research and trading automation.

ChainSlayer moves fast. Keep up with the latest developments by following us on Twitter at https://twitter.com/ChainSlayer_

--

--

ChainSlayer
ChainSlayer

Published in ChainSlayer

We create spaces for streamlining quant trading workflow and rapid testing

Ape/rture
Ape/rture

Written by Ape/rture

Always improving, always learning | Counsel member and building at Deus Ex DAO