Simple Moving Average Crossover Strategy Backtest for Bitcoin

Arguably the moving average crossover is the first trading strategy that a new comer to trading systems encounter. In this article I will backtest and then optimize the simple moving average crossover strategy for Bitcoin. First, I will do the analyses for the period of January 1st 2012 – Math 4th 2021 daily historical price data for Bitcoin using Backtesting.py. Then I will use the backtest results to see how they performed in the last two years. I have to remind that this is an educational article, it is not investment or trading advice. As you will see backtesting results are often unreliable.

This is in fact a series. SMA Crossover System for:

  1. Daily Bitcoin data (this article)
  2. Four hourly data
  3. Hourly data
  4. Five minute data

Conceptual Overview

In a chart what we usually see is random fluctuations of price. However, if we take moving averages we can smooth the data, allowing us to see price trends. The moving average (MA) is calculated for n periods, for example if n = 10, MA(10) means ten day moving average. For MA(10), for a given day (let’s call this k), we take the closing prices of the last 10 days from that given day and average them and use this as the data point. Then we move to the next day (k+1) and do the same calculation for this new day. The result is a line representing the ten day moving average. The simple moving average always lags behind the current price, because the current price is only one data point in the calculation.

Moving average can be taken for any number of days, for example we can take MA(20) where n = 20. Using larger n usually results in smoother curves and allows for detecting longer trends, however, they lag behind the price more than smaller n. Hence MA(10) is faster and MA(20) is slower to react to price motion.

Can we combine two moving averages to create a trading strategy? Yes, if we assume momentum trading principle that once price has momentum it will keep moving until it looses its momentum. Momentum means the rate of change in price, speed of the price. So if MA(10) crosses over MA(20), that is MA(10) > MA(20), it means that the price is gaining momentum, the ten day prices are performing better than the 20 day prices. This is the entry signal. As long as MA(10) is above MA(20) we keep the position open. When the MA(20) crosses over MA(10), that is MA(10) < MA(20), this is our exit signal we close the position.

SMA Crossover Strategy Conceptual Visualization

The figure above shows daily Bitcoin prices. The blue line is the 10 day moving average, and the red line is the twenty day moving average. So in our trading system we enter when blue line crosses over the red line, and we exit when the red line crosses over the blue line. Can you identify another entry and exit pair from the chart?

Moving average crossover is a simple strategy really. But does it work? Let’s backtest it.

Backtest Results

Why did we choose 10 day and 20 day moving averages? Because they are commonly used. So first we backtest the strategy using these two parameters. Also I have to say this is a long only strategy, no short positions were included. I will give the entire python code at the end of the article.

The results are very interesting:


Start 2011-12-31 00:00:00
End 2021-05-03 00:00:00
Duration 3411 days 00:00:00
Exposure Time [%] 60.51628
Equity Final [$] 17898915120.340019
Equity Peak [$] 25889057770.31496
Return [%] 1789791.512034
Buy & Hold Return [%] 1266403.71179
Return (Ann.) [%] 185.330451
Volatility (Ann.) [%] 214.78251
Sharpe Ratio 0.862875
Sortino Ratio 4.207606
Calmar Ratio 2.520917
Max. Drawdown [%] -73.517081
Avg. Drawdown [%] -8.134589
Max. Drawdown Duration 865 days 00:00:00
Avg. Drawdown Duration 33 days 00:00:00
#Trades 79
Win Rate [%] 45.56962
Best Trade [%] 570.024935
Worst Trade [%] -26.216965
Avg. Trade [%] 13.196582
Max. Trade Duration 105 days 00:00:00
Avg. Trade Duration 26 days 00:00:00
Profit Factor 9.055081
Expectancy [%] 26.003983
SQN 1.392253

The return in about 10 years is 1,789,791%. Yes almost 1,8 million percent. Compare this to buy and hold return: 1,266,403% about 1,2 million percent. Yearly return of the strategy is 185%. It also limited our time to exposure to risk, we were only exposed 60% of the time. However, it only has a 45% winning trades rate. So even in its basic form this simple strategy looks good. But can it be better?

Optimization of the Strategy

Are 10 and 20 magic numbers for simple moving averages? Maybe. Let’s try and find if there are better number combinations. We will maximize the final equity and find the optimal n1 and n2. The results of the best 10 combinations are as follows:


n1 n2
5 70 2.711497e+10
65 2.680452e+10
15 80 2.630954e+10
5 60 2.580943e+10
20 70 2.391964e+10
5 120 2.327474e+10
75 2.315296e+10
10 40 2.285185e+10
5 95 2.234390e+10
100 2.226907e+10

Short moving average of 5 and long of 70, 65 and even 60 lead to the most returns. 15 and 80 is also yielding high returns. Let’s look at the heat map. We are looking for yellow and green zones.

Bitcoin daily price data SMA Crossover system optimization heatmap

Finally let’s see the results of 5 and 70 day moving average cross over strategy.


Start 2011-12-31 00:00:00
End 2021-05-03 00:00:00
Duration 3411 days 00:00:00
Exposure Time [%] 61.308302
Equity Final [$] 27114971465.097225
Equity Peak [$] 34171319370.996483
Return [%] 2711397.14651
Buy & Hold Return [%] 1266403.71179
Return (Ann.) [%] 198.305722
Volatility (Ann.) [%] 236.494528
Sharpe Ratio 0.838521
Sortino Ratio 4.332388
Calmar Ratio 2.69741
Max. Drawdown [%] -73.51708
Avg. Drawdown [%] -9.549877
Max. Drawdown Duration 703 days 00:00:00
Avg. Drawdown Duration 36 days 00:00:00
#Trades 33
Win Rate [%] 45.454545
Best Trade [%] 850.075793
Worst Trade [%] -14.19312
Avg. Trade [%] 36.251019
Max. Trade Duration 199 days 00:00:00
Avg. Trade Duration 63 days 00:00:00
Profit Factor 25.051308
Expectancy [%] 82.880405

Again our time in market is 61%, which is good for risk reduction. We only executed 33 trades, but our winning rate was again 45%. We made 2,7 million percent returns. More than doubling the buy and hold strategy. Annual return is almost 200%. Seems amazing. But did we curve fit, in other words, over-optimize? If this is a good strategy it should work most of the time.

Backtesting for Last Two Years

Let’s only look 24 months back this time. Not include the data before. Had we entered the Bitcoin market last year how would our optimized strategy perform?


Start 2019-05-02 00:00:00
End 2021-04-30 00:00:00
Duration 729 days 00:00:00
Exposure Time [%] 51.643836
Equity Final [$] 4399772.47766
Equity Peak [$] 5218013.57766
Return [%] 339.977248
Buy & Hold Return [%] 969.090791
Return (Ann.) [%] 109.756346
Volatility (Ann.) [%] 106.40569
Sharpe Ratio 1.031489
Sortino Ratio 3.58288
Calmar Ratio 4.041837
Max. Drawdown [%] -27.155066
Avg. Drawdown [%] -6.840093
Max. Drawdown Duration 267 days 00:00:00
Avg. Drawdown Duration 21 days 00:00:00
#Trades 5
Win Rate [%] 60.0
Best Trade [%] 367.233381
Worst Trade [%] -8.519406
Avg. Trade [%] 34.602496
Max. Trade Duration 193 days 00:00:00
Avg. Trade Duration 75 days 00:00:00
Profit Factor 41.95373
Expectancy [%] 72.415019

This time we are below buy and hold return, but our exposure time is 51% and we only made 5 trades and 3 are winning trades. This doesn’t look as amazing as the previous run does it? What if we optimized only for this period? The best parameters were 5 and 40. Here are the results.


Start 2019-05-02 00:00:00
End 2021-04-30 00:00:00
Duration 729 days 00:00:00
Exposure Time [%] 50.136986
Equity Final [$] 11362141.11364
Equity Peak [$] 13477593.71364
Return [%] 1036.214111
Buy & Hold Return [%] 969.090791
Return (Ann.) [%] 237.077752
Volatility (Ann.) [%] 186.26313
Sharpe Ratio 1.272811
Sortino Ratio 8.090248
Calmar Ratio 10.440443
Max. Drawdown [%] -22.707634
Avg. Drawdown [%] -5.199964
Max. Drawdown Duration 65 days 00:00:00
Avg. Drawdown Duration 10 days 00:00:00
#Trades 3
Win Rate [%] 100.0
Best Trade [%] 485.488326
Worst Trade [%] 0.917533
Avg. Trade [%] 124.955075
Max. Trade Duration 299 days 00:00:00
Avg. Trade Duration 121 days 00:00:00
Profit Factor NaN
Expectancy [%] 193.023574

Better than the original optimization. And the top ten combinations are:


n1 n2
115 120 1.136214e+07
110 130 1.029734e+07
100 140 1.029160e+07
175 9.810890e+06
105 135 9.810122e+06
110 125 9.808867e+06
140 9.704499e+06
115 125 9.701306e+06
95 180 9.432339e+06
105 155 9.224955e+06

n1 = 5 and n= 70 is not even in the top 10. Also 8 of the strategies outperformed buy and hold returns. Let’s look at the heatmap.

Bitcoin daily price data SMA Crossover system optimization heatmap 2

Making Sense of Backtests

So what did we learn? We can get amazing backtest results, but do they hold up in new periods? This is an important question. As we saw not necessarily. That’s why finding anomalies in Bitcoin price data is not as easy as it looks. I think we will keep looking. If you want to you can read the backtest results for four hourly data. Let me finish this with the warning: this is not investment or trading advice.

Complete Python Code

If you want to use TALib as I did you need to install it first. I use SQLite, if you have your data there you can use it like I did.

import sqlite3 as sql
import pandas as pd
import numpy as np
import talib as talib
from datetime import datetime
from datetime import timedelta
from backtesting import Strategy, Backtest
from backtesting.lib import crossover
from matplotlib import pyplot as plt
import seaborn as sns

dbfile = 'bitcoin.db'
table = 'bitcoin_daily'
conn = sql.connect(dbfile)

start_date = '2021-05-04'
how_many_months = "-24"
SQL = "SELECT DISTINCT Date,Open,Close,Low,High,Volume_USD FROM " + \
 table + " WHERE Date < date('"+ start_date +"') and \
Date > date('"+ start_date+"','start of month','" \
+how_many_months+" month') ORDER BY Date"
print(SQL)

data = pd.read_sql(SQL, conn)
data['Date'] = pd.to_datetime(data['Date'])
data.set_index("Date", inplace = True)
data.rename({"open": "Open", "close": "Close", "low" : "Low",\
 "high" : "High", "volume": "Volume"}, axis='columns')
data.columns = ['Open','Close','Low','High','Volume'];

class SmaCross(Strategy):
n1 = 10
n2 = 20

    def init(self):
    self.sma1 = self.I(talib.SMA, self.data.Close, self.n1)
    self.sma2 = self.I(talib.SMA, self.data.Close, self.n2)

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

bt = Backtest(data, SmaCross, cash=1000000, commission=.002, \
exclusive_orders=True)
output = bt.run()
print(output)
bt.plot()
stats,heatmap = bt.optimize(n1=range(5, 120, 5),\
 n2=range(20, 220, 5), maximize='Equity Final [$]', \
constraint=lambda param: param.n1 < param.n2, return_heatmap=True)
print(stats)
print(stats.strategy)
bt.plot()
print(heatmap.sortvalues(ascending=False).iloc[:10])
hm = heatmap.groupby(['n1', 'n2']).mean().unstack()
plt.figure(figsize=(12, 10))
sns.heatmap(hm[::-1], cmap='viridis')
plt.savefig('sma-cross-heatmap.png')

References

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.