# How to Build a Moving Average Pullback Backtester in ThinkorSwim

> Moving average pullback backtester for ThinkorSwim: tests pullbacks to the 8, 21 or 34 EMA or 50 or 200 SMA in a stacked trend, with ATR targets and stops.

Page: https://tosindicators.com/backtesters/moving-average-pullback  
Video: https://www.youtube.com/watch?v=9Et1T5JKT-g  
Section: Backtesters  
Skill: Beginner-Friendly  
Access: free  
Updated: 2026-10-17

In this tutorial I build a moving average pullback backtester for ThinkorSwim from scratch. It buys pullbacks into one of 5 moving averages while all 5 are stacked in an uptrend, or shorts rallies into them in a downtrend, and it exits at a target or a stop measured in ATR. The Floating P/L study under the chart shows how each version has done.

The backtester answers three questions: do moving average pullbacks work on a given market, which moving average works best there, and should you wait for a confirmation candle or buy the pullback blindly? The tutorial is beginner-friendly, and every choice is an input, so you can run a new test from the settings window without touching the code.

## Key Takeaways

- The moving average pullback backtester is a free ThinkorSwim strategy that enters on pullbacks to the 8, 21 or 34 EMA or the 50 or 200 SMA, and only while all 5 averages are stacked in order.

- It has 2 entry types. Pullback only buys at the moving average as soon as the low touches it. Confirmation waits for a touch on the current or prior bar and a close above the previous bar's high.

- Exits are a target and a stop set as multiples of the 14-period ATR on the entry bar. The defaults are a 2x ATR target, a 1x ATR stop and 100 shares, with long entries on and short entries off.

- In the video's comparison on SPY (5 years of daily bars, longs only, 3x ATR target, 2x ATR stop), the 8 EMA showed $1,732.10 of P/L buying pullbacks blindly and $6,023.84 with confirmation. The video gives no trade counts.

- In the same SPY test, the 21 EMA, 34 EMA and 50 SMA did better without confirmation. The 34 EMA showed $5,934.91 blind and $4,556.75 confirmed.

## What Is a Moving Average Pullback?

A moving average pullback is a dip back to a moving average inside a trend, which traders use as a place to join that trend. In an uptrend, price pulls back down to an average such as the 21 EMA and then moves higher. In a downtrend, price rallies up to the average and turns lower.

The moving average pullback backtester puts that idea to the test with fixed rules. I built it to answer three questions:

- Does buying and selling pullbacks into moving averages work, and does it work better on some markets and timeframes than others?

- If it does, which moving average is the most effective for the market you trade, such as the S&P 500?

- Should you wait for a confirmation pattern, or enter blindly on the first pullback?

![Moving average pullback backtester on a 5-year daily SPY chart in ThinkorSwim, with pink entry tags, stacked moving averages and the Floating P/L histogram below](https://tosindicators.com/wp-content/uploads/2022/03/Moving-Average-Pullback-TOS-Indicators-1024x544.jpg)The backtester on a 5-year daily SPY chart. Pink tags mark entries, and the green bars under the chart are the Floating P/L study.

## The Rules Behind the Backtester

The moving average pullback backtester is built on 5 requirements I set before writing any code, so the result is a repeatable process:

- **A trend structure.** It only takes pullbacks while the 8, 21 and 34 EMAs and the 50 and 200 SMAs are stacked in order. That filters out a lot of the chop.

- **Any market.** Stocks, ETFs, futures and forex, anything ThinkorSwim supports.

- **Any timeframe**, daily or intraday.

- **Longs and shorts**, each with its own input, so you test one side at a time and their P/L stays separate.

- **Clear entries and exits**, with little room for discretion.

Our free [Stacked Moving Averages indicator](https://tosindicators.com/indicators/stacked-moving-averages) plots the same 5 averages if you want to see the structure on your chart.

## Pullback Only vs Confirmation Entries

The moving average pullback backtester has 2 entry types, picked from the `entryType` input. Both need the stack in place. They differ in what price has to do at the average and in the price the backtest pays.

| Rule | Pullback only | Confirmation |
| --- | --- | --- |

| Long entry | Bullish stack and the bar's low at or below the chosen average | Bullish stack, the lowest low of the last 2 bars at or below the average, and a close above the previous bar's high |

| Short entry | Bearish stack and the bar's high at or above the average | Bearish stack, the highest high of the last 2 bars at or above the average, and a close below the previous bar's low |

| Entry price | The moving average's value | The close of the signal bar |

Confirmation waits for price to show that buyers have taken control by closing above the previous candle's high. It checks 2 bars because the low that tags the average often comes one bar before the close that confirms it.

## Results From the Video: Which Moving Average Pullback Is Best on SPY?

The moving average pullback comparison at the start of the video ran on SPY, the S&P 500 ETF, over 5 years of daily bars. It tested long trades only, with a target of 3 times the ATR from the entry price and a stop of 2 times the ATR.

These are historical results from the video. It shows P/L only: no trade counts, no win rates, and no order size for this test. The strategy's default is 100 shares per order.

| Moving average (SPY, daily, 5 years, longs only, 3x ATR target, 2x ATR stop) | P/L, pullback only | P/L, with confirmation | Trades |
| --- | --- | --- | --- |

| 8 EMA | $1,732.10 | $6,023.84 | Not shown |

| 21 EMA | $4,291.46 | $1,551.53 | Not shown |

| 34 EMA | $5,934.91 | $4,556.75 | Not shown |

| 50 SMA | $5,046.69 | $3,034.42 | Not shown |

![Table of SPY moving average pullback backtest P/L for the 8, 21 and 34 EMA and 50 SMA, pullback only versus with confirmation](https://tosindicators.com/wp-content/uploads/2022/03/SP-500-Best-Moving-Average-for-a-Pullback-1024x388.jpg)The SPY comparison from the video as a table: 5 years of daily bars, longs only, 3x ATR target and 2x ATR stop. The highlighted cell is the highest P/L.

- **Confirmation turned the 8 EMA around.** Waiting took it from $1,732.10 to $6,023.84, the highest figure in the table. In the video I put that down to the confirmation turning many aggressive, losing entries into winners.

- **Deeper pullbacks favored blind entries.** On the 21 EMA, 34 EMA and 50 SMA, buying the pullback blindly showed more P/L than waiting.

- **The 50 SMA was more efficient.** It reached about the same P/L as the 34 EMA with drastically fewer trades, though the video gives neither count. Its confirmed version fell to $3,034.42, most likely because waiting missed a few trades.

## How to Create the Backtester in ThinkorSwim

The moving average pullback backtester is a ThinkorSwim strategy, so you create it on the Strategies tab, which is what gives you P/L reporting.

- Click the **Studies** icon on your chart and switch to the **Strategies** tab.

- Click **Global Strategy Settings** and check **Display Floating P/L study with strategies**. That adds a sub panel with the P/L chart.

- Click **Create** and name the strategy. I called mine moving average pullbacks.

- Replace the starter code with the code from the Code tab on this page, then click **OK** and **Apply**.

- Double-click the strategy to pick the moving average, the entry type and the multipliers.

To import the downloaded file instead of pasting, follow our guide on [how to import a thinkScript file into ThinkorSwim](https://tosindicators.com/thinkscript/how-to-import-thinkorswim).

## The Inputs

The moving average pullback backtester has 7 inputs in its settings window. The SPY comparison used a 3x target and a 2x stop, so change the two multipliers to repeat it.

| Input | Default | What it controls |
| --- | --- | --- |

| `longEntries` | yes | Buys pullbacks in a bullish stack |

| `shortEntries` | no | Shorts pullbacks in a bearish stack |

| `entryMA` | EMA8 | The average for the pullback: EMA8, EMA21, EMA34, SMA50 or SMA200 |

| `entryType` | pullbackOnly | Enter blindly at the average, or wait for Confirmation |

| `targetMultiplier` | 2 | Target distance from the entry price, in ATRs |

| `stopMultiplier` | 1 | Stop distance from the entry price, in ATRs |

| `quantity` | 100 | Shares per order |

![ThinkorSwim settings window for the moving average pullback backtester with the entry ma dropdown open, listing EMA8, EMA21, EMA34, SMA50 and SMA200](https://tosindicators.com/wp-content/uploads/2022/03/Moving-Average-Pullback-Backtester-Choices.jpg)The entry ma dropdown lists the 5 moving averages by name.

## How the Code Works

The moving average pullback backtester code has 6 parts: the inputs, the stack, two switches, the stored ATR and the orders. The snippets come from the downloaded file.

### Step 1: Text Choices for the Inputs

```
input entryMA = {default EMA8, EMA21, EMA34, SMA50, SMA200};
input entryType = {default pullbackOnly, Confirmation};
```

Curly brackets turn an input into a dropdown of text choices. I could have used a number, but picking EMA21 from a list is more intuitive.

### Step 2: The Stack

Five `def` lines build the averages with `ExpAverage(close, 8)`, 21 and 34, and `SimpleMovingAvg(close, 50)` and 200. Then the bullish stack needs each one above the next slower one:

```
def bullishStackedMAs = EMA8 > EMA21 and EMA21 > EMA34 and EMA34 > SMA50 and SMA50 > SMA200;
```

The bearish stack is the same line with every sign flipped to less than.

### Step 3: Connect the Dropdown With a Switch

```
def entryPullbackMA;
switch (entryMA){
case EMA8:
    entryPullbackMA = EMA8;
case EMA21:
    entryPullbackMA = EMA21;
case EMA34:
    entryPullbackMA = EMA34;
case SMA50:
    entryPullbackMA = SMA50;
case SMA200:
    entryPullbackMA = SMA200;
}
```

Each text choice becomes a `case` that sets `entryPullbackMA`, the one variable the rest of the code uses.

The strategy wouldn't compile at this point, so in the video I added a temporary `AddOrder` line to check the dropdown, then replaced it later. I also note that SMA200 triggers very few times: by the time price pulls back that far, it has usually broken the stack.

### Step 4: Entry Signals and Entry Prices

A second switch sets 4 variables for each entry type: a true or false entry signal for each side, and the entry price the P/L is measured from.

```
case pullbackOnly:
    bullEntrySignal = bullishStackedMAs and low = entryPullbackMA;
    bearEntryPrice = entryPullbackMA;
```

Pullback only buys the average itself, at its value.

```
case Confirmation:
    bullEntrySignal = bullishStackedMAs and Lowest(low,2)  high[1];
    bullEntryPrice = close;
    bearEntrySignal = bearishStackedMAs and Highest(high,2) >= entryPullbackMA and close < low[1];
    bearEntryPrice = close;
```

Confirmation enters at the close of the bar that confirms the move.

### Step 5: Store the ATR on the Entry Bar

Average true range (ATR) measures how far a market typically moves in one bar, averaged over a set number of bars. The backtester sizes its target and stop with it.

```
def ATRvalue = if (bullEntrySignal and !bullEntrySignal[1]) or (bearEntrySignal and !bearEntrySignal[1]) then ATR(14) else ATRvalue[1];
```

Put `ATR()` straight into the exits and the target and stop would move with every candle. This line saves the 14-period ATR on the first bar of a new signal and carries it forward until the next new signal.

### Step 6: The Long Orders

The code passes `AddOrder` 4 parameters: the order type, the condition that has to be true, the price, and the quantity. ThinkorSwim offers 6 order types: BUY_AUTO, BUY_TO_CLOSE, BUY_TO_OPEN, SELL_AUTO, SELL_TO_CLOSE and SELL_TO_OPEN.

```
#Long-Side Trades
AddOrder(OrderType.BUY_TO_OPEN, longEntries and bullEntrySignal, bullEntryPrice, quantity);
AddOrder(OrderType.SELL_TO_CLOSE, high >= entryPrice() + targetMultiplier * ATRValue, entryPrice() + targetMultiplier * ATRValue, quantity);
AddOrder(OrderType.SELL_TO_CLOSE, low

## thinkScript code

```
#TOS Indicators
#Home of the Volatility Box
#Indicator Name: Moving Average Pullback Backtester
#Full tutorial here: https://tosindicators.com/backtesters/moving-average-pullback

input longEntries = yes;
input shortEntries = no;
input entryMA = {default EMA8, EMA21, EMA34, SMA50, SMA200};
input entryType = {default pullbackOnly, Confirmation};
input targetMultiplier = 2;
input stopMultiplier = 1;
input quantity = 100;

def EMA8 = ExpAverage(close, 8);
def EMA21 = ExpAverage(close, 21);
def EMA34 = ExpAverage(close, 34);
def SMA50 = SimpleMovingAvg(close, 50);
def SMA200 = SimpleMovingAvg(close, 200);

def entryPullbackMA;
switch (entryMA){
case EMA8:
    entryPullbackMA = EMA8;
case EMA21:
    entryPullbackMA = EMA21;
case EMA34:
    entryPullbackMA = EMA34;
case SMA50:
    entryPullbackMA = SMA50;
case SMA200:
    entryPullbackMA = SMA200;
}

def bullishStackedMAs = EMA8 > EMA21 and EMA21 > EMA34 and EMA34 > SMA50 and SMA50 > SMA200;

def bearishStackedMAs = EMA8 < EMA21 and EMA21 < EMA34 and EMA34 < SMA50 and SMA50 < SMA200;

def bullEntrySignal;
def bullEntryPrice;
def bearEntrySignal;
def bearEntryPrice;

switch(entryType){
case pullbackOnly:
    bullEntrySignal = bullishStackedMAs and low <= entryPullbackMA;
    bullEntryPrice =  entryPullbackMA;
    bearEntrySignal = bearishStackedMAs and high >= entryPullbackMA;
    bearEntryPrice = entryPullbackMA;
case Confirmation:
    bullEntrySignal = bullishStackedMAs and Lowest(low,2) <= entryPullbackMA and close > high[1]; 
    bullEntryPrice = close;
    bearEntrySignal = bearishStackedMAs and Highest(high,2) >= entryPullbackMA and close < low[1]; 
    bearEntryPrice = close;
}

def ATRvalue = if (bullEntrySignal and !bullEntrySignal[1]) or (bearEntrySignal and !bearEntrySignal[1]) then ATR(14) else ATRvalue[1];

#Long-Side Trades
AddOrder(OrderType.BUY_TO_OPEN, longEntries and bullEntrySignal, bullEntryPrice, quantity);
AddOrder(OrderType.SELL_TO_CLOSE, high >= entryPrice() + targetMultiplier * ATRValue, entryPrice() + targetMultiplier * ATRValue, quantity); 
AddOrder(OrderType.SELL_TO_CLOSE, low <= entryPrice() - stopMultiplier * ATRValue, entryPrice() - stopMultiplier * ATRValue, quantity); 

#Short-Side Trades
AddOrder(OrderType.SELL_TO_OPEN, shortEntries and bearEntrySignal, bearEntryPrice, quantity);
AddOrder(OrderType.BUY_TO_CLOSE, low <= entryPrice() - targetMultiplier * ATRValue, entryPrice() - targetMultiplier * ATRValue, quantity); 
AddOrder(OrderType.BUY_TO_CLOSE, high >= entryPrice() + stopMultiplier * ATRValue, entryPrice() + stopMultiplier * ATRValue, quantity);
```

## Questions

### What is a moving average pullback?

A moving average pullback is a dip back to a moving average inside a trend, such as a pullback to the 21 EMA in an uptrend before price moves higher. The backtester only counts pullbacks while the 8, 21 and 34 EMAs and the 50 and 200 SMAs are stacked in order, so the market is already trending.

### How do I backtest moving average pullbacks in ThinkorSwim?

Create the backtester as a strategy. Click the Studies icon, switch to the Strategies tab, turn on the Floating P/L study in Global Strategy Settings, then click Create and paste in the code. Double-click the strategy to pick the moving average, the entry type and the target and stop multipliers.

### Which moving average pullback did best on SPY in the video?

On SPY over 5 years of daily bars, longs only, with a 3x ATR target and 2x ATR stop, the 8 EMA with confirmation showed the highest P/L, $6,023.84. Buying the 34 EMA pullback blindly came next at $5,934.91. These are historical results from the video, which gives no trade counts.

### Is it better to wait for confirmation or buy a moving average pullback blindly?

In the video it depended on the average. On SPY, confirmation raised the 8 EMA from $1,732.10 to $6,023.84, but blind entries did better on the 21 EMA, 34 EMA and 50 SMA. On Home Depot, confirmation cut the 8 EMA from $5,214 to $919. Test both entry types on the market you trade.

### How does the backtester set its target and stop?

It stores the 14-period ATR on the bar where a new entry signal appears and keeps that value until the next new signal. The target is the entry price plus the target multiplier times that ATR, and the stop is the entry price minus the stop multiplier times it. The defaults are 2 and 1, and the SPY comparison used 3 and 2.

### Why does the entry arrow show up one bar after the signal?

ThinkorSwim plots strategy orders on the candle after the signal bar. The price is still correct: in the video I checked the Show Report, and the entry price matched the close of the signal bar for a confirmation entry. Read the report for exact prices instead of the position of the tag.

### Why does the 200 SMA pullback rarely take a trade?

The backtester only trades when all 5 averages are stacked, with the 200 SMA at the bottom in an uptrend. By the time price pulls back as far as the 200 SMA, it has usually broken that stacked structure, so all the entry conditions are rarely true at the same time.

### Can I use the moving average pullback backtester on any market or timeframe?

Yes. It was built to run on stocks, ETFs, futures and forex, on any timeframe, for longs and shorts. In the video I tested SPY, Verizon and Home Depot, and mentioned 10 years of data or a 5-minute chart as other options. Results changed a lot from one market to the next.

## Related

- [What Moving Average Pullback Is Best for Trend Days?](https://tosindicators.com/research/trend-days)
- [How to Build an Election Backtester in ThinkorSwim](https://tosindicators.com/backtesters/election)
- [How to Build a Santa Claus Rally Backtester in ThinkorSwim](https://tosindicators.com/backtesters/santa-claus-rally)
- [TTM Squeeze Histogram Backtester for ThinkorSwim](https://tosindicators.com/backtesters/ttm-squeeze-histogram)
- [How to Build a SPY Meltdown Backtester in ThinkorSwim](https://tosindicators.com/backtesters/spy-meltdown)
- [How to Build a Moving Average Crossover Backtester in ThinkorSwim](https://tosindicators.com/backtesters/moving-average-crossover)

