Skip to Content
Course content

132: Backtesting Trading Strategies in R

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I worked with a quantitative developer who was convinced he'd found a "holy grail" momentum strategy. He showed me a backtest result with a staggering 400% annual return and a near-perfect equity curve. I asked him one simple question: how was he calculating the moving average? It turned out he was using a function that centered the window, meaning his "past" average actually included data from the future. He wasn't predicting the market; he was accidentally reading the answers from the back of the book. This is called look-ahead bias, and it's the fastest way to lose a lot of money in a real brokerage account.

Backtesting is the process of applying a trading strategy to historical data to see how it would have performed. In R, we have a massive advantage because the quantmod and TTR packages handle the heavy lifting of data acquisition and technical indicator calculation. But the logic of the backtest—the actual "if this, then that"—is where you need to be meticulous. If you shift your data by even one index incorrectly, your results are worthless.

Converting Indicators into Trade Signals

The core of any backtest is the signal. You aren't just looking at a line on a chart; you're creating a binary or ternary state: Buy (1), Sell (-1), or Neutral (0). Let's use a classic Simple Moving Average (SMA) crossover. The idea is simple: when a fast-moving average crosses above a slow-moving average, you go long.

library(quantmod)
library(TTR)

# Get historical data for Apple
getSymbols("AAPL", src = "yahoo", from = "2020-01-01")
prices <- Cl(AAPL) # Closing prices

# Calculate SMAs
fast_sma <- SMA(prices, n = 20)
slow_sma <- SMA(prices, n = 50)

# Generate signals: 1 when fast > slow, 0 otherwise
# We use ifelse to create a logic vector
signal <- ifelse(fast_sma > slow_sma, 1, 0)

Now, here is the part where most beginners fail: the lag. If the SMA crosses today, you cannot buy at today's closing price because you only know the cross after the market closes. You must execute the trade at the next available price. In R, we handle this by lagging our signal vector using lag().

Calculating the Strategy Equity Curve

Once you have a lagged signal, you can calculate your returns. The most efficient way to do this in R is vectorization. Instead of looping through every day of the year, we multiply the daily percentage change of the asset by our signal vector. I prefer using log returns here because they are additive, which makes the math much cleaner when dealing with compounded growth.

# Calculate daily log returns
returns <- diff(log(prices))

# Lag the signal by 1 day to avoid look-ahead bias
# We align the signal from 'yesterday' with 'today's' return
strategy_returns <- lag(signal, k = 1) * returns

# Remove NA values caused by lagging and SMA warmup
strategy_returns <- na.omit(strategy_returns)

# Convert returns to a cumulative equity curve starting at 1
equity_curve <- exp(cumsum(strategy_returns))
plot(equity_curve, main = "Equity Curve: SMA Crossover", col = "blue", lwd = 2)

If you see a vertical line going straight up, don't celebrate yet. Check your lag. If the equity curve looks too good to be true, it almost always is.

Measuring the Pain via Maximum Drawdown

Raw returns are a vanity metric. I don't care if a strategy makes 50% a year if it drops 70% in the middle of the year—most investors (and their bosses) will panic and shut the strategy down long before the recovery happens. This is why we track Maximum Drawdown (MDD), which is the largest peak-to-trough decline in the equity curve.

You can calculate this by tracking the running maximum of your equity curve and finding the largest percentage difference between that peak and the current value. It's the "sleep at night" metric. A strategy with a 10% return and a 5% drawdown is infinitely more valuable to a professional firm than a strategy with a 30% return and a 40% drawdown.




📋 Practical Task

Exercise: Implementing an RSI Mean-Reversion Strategy for SPY

Your task is to build a backtest for a "Mean Reversion" strategy using the S&P 500 ETF (SPY). Instead of a trend-following SMA crossover, you will implement a strategy based on the Relative Strength Index (RSI).

Requirements:

  • Download historical daily data for "SPY" from 2018 to the present.
  • Calculate a 14-day RSI using the RSI() function from the TTR package.
  • The Strategy:
    • Enter a Long position (1) when the RSI drops below 30 (oversold).
    • Exit the position (0) when the RSI rises above 70 (overbought).
  • Ensure you apply a 1-day lag to your signals to prevent look-ahead bias.
  • Calculate the total cumulative return of the strategy and plot the resulting equity curve.
  • Print the final value of the equity curve to the console.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.