Moving Average Crossover Backtester

Test which moving average crossover is best on your favorite list of stocks, ETFs and futures markets.

For the 23rd episode of "How to thinkScript," we're going to build a moving average crossover backtester to help us answer one simple question:

What is the best moving average crossover strategy for a particular stock?

This thinkScript tutorial is going to be beginner-friendly, for those of you who are newer to the thinkScript language.

Moving Average Crossover Backtester for ThinkOrSwim

With the moving average crossover backtester that we create in this episode, we'll be able to very easily test two moving averages against one another.

For example...

Let's say you wanted to see if an 8 EMA x 21 EMA Crossover is actually a profitable strategy. Well, now you can test it!

Maybe you're curious what happens if you change that 21 EMA to a 34 EMA. Or perhaps, a Simple Moving Average instead of an Exponential.

What about when the moving averages cross in the opposite direction? Is that a good time to get out? Or should you use a close below the slower moving average?

We'll answer all of these questions and more, with the Moving Average Crossover Backtester (phew, that's a long name!) that we build.

So, let’s get started!

Moving Average Crossover Backtester Video Tutorial

Watch the video tutorial here, and follow along with the scripts away below.

Volatility Box Invite

We are TOS, home of the Volatility Box.

The Volatility Box is our secret tool, to help us consistently profit from the market place. We’re a small team, and we spend hours every day, after the market close, doing nothing but studying thousands of data points to keep improving and perfecting the Volatility Box price ranges.

We have two different Volatility Boxes - a Futures Volatility Box and a Stock Volatility Box.

The Futures Volatility Box comes with:

  • 5 Volatility Models for each market
  • Support for 10 Futures Markets (/ES, /NQ, /YM, /RTY, /CL, /GC, /SI, /ZB, /HG, /NG)
  • Video Setup Guide
  • Trade Plan
  • Access to all members-only resources, including Squeeze Course

Sign up for the Futures Volatility Box here.

The Stock Volatility Box comes with:

  • Powerful web-based platform to help you trade stocks like a pro
  • Supports 8,200+ stocks and ETFs, with daily Volatility Box price levels
  • Real-time live scanner for all liquid stocks to find active Volatility Box setups, so you can pick, and choose, and simply add them to your Dashboard for price levels
  • Completely platform agnostic, so even if you don’t have ThinkOrSwim, you’re all good
  • Access to all members-only resources, including Squeeze Course

Sign up for the Stock Volatility Box here.

If you trade both stocks and futures, e-mail us at [email protected] and we can offer you a trader-friendly bundle deal.

Start trading at the edge, with an edge, and join the Volatility Box family today.

Moving Avg. Crossover thinkScript Requirements

Before we start writing any code, it's important to first understand and map out what we're trying to build.

thinkScript Goal for Moving Average Crossover Backtester

As a user, if we use the 8 period EMA crossing with the 21 period EMA as an example... the following would be considered our "requirements"

  • We should be able to change the moving average length from 8 to 13 or 34 or any other number we wanted
  • We should be able to change the moving average type, from Exponential to Simple, Hull, Wilders, etc.
  • We should be able to test moving average crossovers to the bullish side, along with the bearish side

With our objectives clearly stated, we can now translate this to some thinkScript code.

Organizing All of the Input Variables

Let's start by creating an organized list of all of the input variables that we will need to define.

An input variable in thinkScript is a variable in which the user can control and change the parameters directly via the study's menu:

Input variables in ThinkOrSwim

In the screenshot above, the user can change the moving average length (among many other things), symbolizing the importance of an input variable.

The benefit of using a thinkScript input variable is that you can change aspects of the code in the studies menu, instead of needing to open and edit the code.

For our backtester, here are the input variables we'll need:

input longsEntries = yes;
input shortEntries = no;

input fastMALength = 8;
input slowMALength = 21;
input averageTypeFast = AverageType.Exponential;
input averageTypeSlow = AverageType.Exponential;
input quantity = 100;

The "FastMALength" variable allows us to change the length of the fast moving average, while the "SlowMALength" variable allows us to change the slow moving average's length.

The AverageType variable in thinkScript also unlocks all of the different moving averages that ThinkOrSwim supports. This list includes the Simple, Exponential, Weighted, Wilder's and Hull -- which is important later in the tutorial.

We separate this out by fast and slow moving averages as well, giving you granular control.

Finally, we have our strategy specific variables, which includes the quantity for shares/contracts used in the backtest P/L reports. We also have the "longsOnly" and "shortsOnly" variables, allowing us to switch between bullish and bearish moving average crosses.

How to Create a Moving Average in thinkScript

Now that we have all of the variables defined which give us context clues about the moving average we want, it's time to actually build it.

To define a moving average, we can use the "MovingAverage()" thinkScript function.

The format is the following:

  • MovingAverage(averageType, priceType, length);

To create our two moving averages, we can use the function above and plug in the input variables in the appropriate places:

plot fastMA = MovingAverage(averageTypeFast, close, fastMALength);
plot slowMA = MovingAverage(averageTypeSlow, close, slowMALength);

We also have the fastMA and slowMA set as a "plot" variable, which will show the appropriate moving averages on your ThinkOrSwim charts.

If you don't wish to see the moving averages plotted, you can simply change the "plot" variable above to a "def" variable.

AddOrder() Function Code

Finally, we need to define when and how to take a "trade."

To write that code, we can simply insert the appropriate labels in their respective parameters:

  • AddOrder(Buy/Sell to Open/Close, Boolean trigger variable, price, quantity);

And so, with all of the code we have so far, that translates to:

AddOrder(OrderType.Buy_To_Open, (longEntries) and fastMA crosses above slowMA, close, quantity);
AddOrder(OrderType.Sell_To_Close,fastMA crosses below slowMA, close, quantity);

AddOrder(OrderType.Sell_To_Open, (shortEntries) and fastMA crosses below slowMA, close, quantity);
AddOrder(OrderType.Buy_To_Close, fastMA crosses above slowMA, close, quantity);

The "Buy to Open" orders are for longs only, in which we're checking if the fast moving average has crosses above the slow moving average.

If it does, then we enter the trade at the closing price, with the default quantity.

We stay in this trade all the way until the fast moving average crosses below the slow moving average.

Moving Average Crossover Labels

It would be nice if our backtester told us exactly what moving average crossover scenario we were testing.

Here's an example of what the final labels will look like:

To display that information, we'll use the Add Label ThinkOrSwim function.

Each moving average type also has a key identifier associated with it, which will be helpful when we decide what to show on our labels.

Here's a helpful breakdown of each AverageType, and it's identifier value:

  • Simple Moving Average = 0
  • Exponential Moving Average = 1
  • Weighted Moving Average = 2
  • Wilder's Moving Average = 3
  • Hull Moving Average = 4

We can use the integer values above to create a boolean test in the labels, which displays the appropriate moving average in text form.

Here's what that looks like, translated to thinkScript:

AddLabel(yes, fastMALength + (if averageTypeFast == 0 then " Simple" else if averageTypeFast == 1 then " EMA" else if averageTypeFast == 2 then " Weighted" else if averageTypeFast == 3 then " Wilders" else " Hull") + " x " + slowMALength + (if averageTypeSlow == 0 then " Simple" else if averageTypeSlow == 1 then " EMA" else if averageTypeSlow == 2 then " Weighted" else if averageTypeSlow == 3 then " Wilders" else " Hull"), color.yellow);

And with that, our basic backtester is done.

Testing Moving Average Crossovers on S&P 500

Let's use the backtester we've built to test different moving average crossover strategies on the S&P 500 (SPY). We're going to use a daily time frame chart, going back 5 years.

Using the 8 EMA x 21 EMA crossover on the SPY Daily Chart, we see have a P/L of $5,662:

8 EMA x 21 EMA Moving Average Crossover on SPY

Now, let's go ahead and tweak one variable to test the improvement. Let's change our slower moving average from 21 to 34. Re-running that, our 8 EMA x 34 EMA crossover leads to a slight decrease in P/L:

8 EMA x 34 EMA Moving Average Crossover on SPY

Now, let's add in one further twist. Let's change this to a Simple Moving Average, looking at the 8 x 21 fast and slow lengths:

That P/L increased to $5,654 over the same time period we were measuring:

8 x 34 Simple Moving Average Crossover on SPY

And finally, let's see what happens if we change our slower moving average's length from 34 to 13. Do we see an increase then?

8 x 13 Simple Moving Average Crossover on SPY

Yes we do! In fact, the 8 Simple Moving Average crossing above the 13 Simple Moving Average has been the most effective crossover, from the ones we just tested.

And just like that, we've built a quick, yet useful moving average crossover using just a few lines of thinkScript code.


Click the button below to download the Moving Average Crossover Backtester for ThinkOrSwim.

Table of Contents
    Add a header to begin generating the table of contents

    Trade Like a Pro, With the Pros