Christopher Rotunno

Aug 8, 2021

7 min read

Optimize the SMA Crossover Strategy for BTC, ETH and DOGE Markets

GIF by Feliks Tomasz Konczakowski

Software Requirements

Introduction

The simple moving average (SMA) crossover strategy can be optimized to maximize returns on securities including cryptocurrencies like Bitcoin, Ethereum, and Dogecoin. The previous article arbitrarily selects a 50-day and 150-day combination, which underperforms when compared to the benchmark strategy of holding the securities and going long. This article tries to improve upon those returns, or lack thereof, by optimizing the time windows and using a larger sample size. Perhaps a different combination of time window parameters will yield more optimistic results. Read on to find out!

*** Disclaimer*** The content of this article is strictly educational and not to be considered as professional investment advice.

Upon finishing this blog post, the reader will be able to do the following in python:

  • Generate sequences of numbers✔️
  • Read and write a for-loop that combines sequences of numbers and optimizes the SMA crossover strategy ✔️
  • Infer if this is a useful strategy for coin markets ✔️

Optimization

Different combinations of short-term and long-term time windows must be tested to find the optimal SMA crossover strategy. The 50-day and 150-day parameters are a randomly chosen combination and there’s a chance that other combinations perform better. This section will generate a sample set of combinations and then iterate over each combination to perform vectorized backtesting and then sort and rank the combinations based on highest earning potential.

Step 1: Generate the parameters

Python’s built-in function, range(start, stop, step), is used below to specify the parameter values for sma1 and sma2. The first argument of the range function starts the sequence, and the second argument stops the sequence. The third argument is the step constraint, which tells the function how many steps to take between the numbers in the sequence.

For the short-term window (sma1), the sequence of numbers generated are 20 24 28 32 36 40 44 48 52 56 60 which range from 20 to 60 and steps in increments of 4. The numbers generated for the long-term window (sma2) are 180 190 200 210 220 230 240 250 260 270 280 and range from 180 to 280 in increments of 10. Notice the stop argument is 61 and 281; this is because in python begins at 0 instead of 1 so the ending needs to end a step up.

Now there are two sets of parameters (sma1 and sma2) that represent the short-term and long-term time windows. The next step is to find all the possible combinations of the two sets of parameters, and the itertools module is a great resource for identifying such combinations.

Step 2: Import product from the itertools module

Now that the parameters have been generated, a new dependency will be used to combine inputs.

Python’s itertools️ module performs algebra underneath the hood to construct and return iterators. In this case, the iterator is a combination of short-term and long-term time windows. Itertools product() iterator returns the cartesian product of input iterables, equivalent to a nested for-loop. The product iterator will provide a good sample of different combinations to compare returns for the SMA crossover strategy.

Step 3: Compute the cartesian product

The scientific notation for the cartesian product is denoted as follows:

The cartesian product can be computed now that the itertools dependency is available. The cartesian product identifies all the possible combinations from these two parameters sets. i.e., (20,180), (20,190), (20,200), (20,210), (20,220), (20,230), (20,240), (20,250), (20,260), (20,270), (20,280), (24,180), (24,190), (24,200), (24,210), (24,220), (24,230), (24,240), (24,250), (24,260), (24,270), (24,280), et cetera .... Each combintation is unique and is sorted by the rightmost element advancing in every iteration.

Step 4: Construct a for-loop that iterates over each combination

The code below produces the combination sets listed in step 3 with the combined values for sma1 and sma2. First an empty dataframe is initiated to store the results from vectorized backtesting for each combination. Then a for-loop is constructed to iterate of the values for SMA1 and SMA2 to combine values and then perform backtesting for each combination.

There are three new functions introduced in this code block that are not discussed in the first article. The first new function is DataFrame(data=None, index=None, columns=None, dtype=None, copy=None)️. All of the arguments for this function are optional; which is perfectly demonstrated on line 9, where a two-dimensional, size-mutable, and tabular data is created with nothing in it. The DataFrame function is used once more within the for-loop to construct a table that stores the price, Returns, SMA1, SMA2, Position, Strategy. Another new function is dropna(axis=0, how='any', thresh=None, subset=None, inplace=False). This function removes missing values and its arguments are also optional. However, the inplace argument equals True instead of its default value of False because the operation needs to preserve the placement of the missing value. This function is performed three times within the for-loop (lines 16, 24, and 30) to remove missing values as new columns are being added to the DataFrame. The last function introduced is the append(other, ignore_index=False, verify_integrity=False, sort=False). This function is used to on lines 34–38 to populate the empty DataFrame created on line 9. The results for SMA1, SMA2, MARKET, STRATEGY, OUT are recorded to the dataframe.

The next step will print the output and evaluate the results.

Step 5: Print the output and evaluate the results

The following code shows the 10 best performing parameter combinations. The ranking is done by using the outperformance of the SMA crossover trading strategy (STRATEGY) compared to the benchmark investment (MARKET).

Pandas has a sorting function called sort_values(by, axis=0, ascending=True, inplace=False, kind='quicksort', na_position='last', ignore_index=False, key=None). This function sorts values along either the x-axis or y-axis. Here the OUT is sorted in ascending order to capture the best performing combinations. Let’s analyze the results for each coin market.

BTC Results

The top ten results for bitcoin are all positive. The best strategy is a combination of SMA1=60 and SMA2=260, which outperforms the benchmark strategy by 39 percentage points.

ETH Results

The results for ethereum are just insane. The best strategy is a combination of SMA1=20 and SMA2=180, which outperforms the benchmark strategy by some 162 percentage points.

DOGE Results

Doge gets only a couple positive results. The best strategy is a combination of SMA1=52 and SMA2=260, which outperforms the benchmark strategy by 20 percentage points.

Conclusion

The SMA crossover strategy performs much better when the time window parameters are optimized. We went from negative returns in the previous article to having double digit returns in this article. That’s amazing! One might be inclined to use this strategy in for their next trading bot, however caution is still advised. The results seem nice on the surface, but they are heavily dependent of the sample data and are likely overfitting the dataset. That is, the parameters chosen may perform very well on chosen sample but performs terribly on other datasets. Overfitting will be the topic for future articles as it shows up in other models. Other assumptions are also used to simpilfy this analysis by not including fixed-fees, bid-ask spreads, and lending costs. These variables would need to be considered in a more realisic investing scenario.

References

Kanungo, Deepak. “Safari — The Best Books and Video Courses from the Brightest Minds.” O’Reilly | Safari, 2019, learning.oreilly.com/learning-paths/learning-path-hands-on/9781492082613?cohort=false.

Read the documentation for the functions used in this article series!