1 Introduction

In the world of quantitative trading, trend-following strategies have attracted practitioners for decades on the premise that price momentum contains predictive information about future returns. One of the best-known trend-following signals is the death cross, which occurs when a short-term moving average falls below a long-term moving average, commonly interpreted as a bearish signal indicating downward momentum. Its mirror counterpart, the golden cross, occurs when the short-term average rises above the long-term average, signaling bullish momentum. This project explores algorithmic backtesting through the lens of the death-cross and golden-cross trading signal.

The goal here isn’t just to see if the rule works, but to ask the “what if” questions. If we had systematically followed the death-cross and golden-cross signal on Tesla (TSLA) over recent years, would we have captured its explosive trends, or would whipsaws and false signals have eroded our returns?

1.1 The Death-Cross / Golden-Cross Signal

The signal is built from two simple moving averages (SMAs). A short-term SMA, which by convention uses a 50-day window, captures recent price momentum, while a long-term SMA, typically 200 days, represents the broader trend. When the 50-day SMA crosses above the 200-day SMA (a golden cross), the strategy enters a long position, betting that upward momentum will persist. When the 50-day SMA crosses below the 200-day SMA (a death cross), the strategy switches to a short position, anticipating continued downside. In this way, the trader always holds a directional position: long when the trend is up, short when the trend is down.

2 Load Libraries

library(yfR)
library(TTR)
library(xts)
library(ggplot2)
library(knitr)
library(kableExtra)
# Download daily adjusted prices via yfR and convert to xts
get_prices_xts <- function(ticker, first_date, last_date) {
  df <- yf_get(tickers = ticker,
               first_date = first_date,
               last_date = last_date,
               be_quiet = TRUE)
  price_xts <- xts(df$price_adjusted, order.by = df$ref_date)
  colnames(price_xts) <- "Close"
  return(price_xts)
}

3 Function 1: Base-Level Back Test

We ran the first simulation on TSLA from January 2020 through the end of March 2026 using the standard 50-day and 200-day moving-average windows.

backtest_ma <- function(ticker, begin, end, short_ma = 50, long_ma = 200,
                        prices_xts = NULL) {

  begin_date <- as.Date(begin, format = "%Y%m%d")
  end_date <- as.Date(end,   format = "%Y%m%d")

  # Download with warm-up buffer, or use pre-loaded xts from Functions 2/3
  if (is.null(prices_xts)) {
    buffer_start <- begin_date - ceiling(long_ma * 1.5)
    close_prices_full <- get_prices_xts(ticker, buffer_start, end_date)
  } else {
    close_prices_full <- prices_xts
  }

  # Compute SMAs on full dataset so values are valid from begin_date
  sma_short_full <- SMA(close_prices_full, n = short_ma)
  sma_long_full <- SMA(close_prices_full, n = long_ma)

  # Subset to the requested test window after SMA computation
  date_range_str <- paste0(begin_date, "/", end_date)
  close_prices <- close_prices_full[date_range_str]
  sma_short <- sma_short_full[date_range_str]
  sma_long <- sma_long_full[date_range_str]

  # Daily arithmetic returns: (P_t - P_{t-1}) / P_{t-1}
  daily_returns <- diff(close_prices) / lag(close_prices, 1)
  colnames(daily_returns) <- "returns"

  # Align returns and SMAs, drop leading NA
  combined <- merge(daily_returns, sma_short, sma_long)
  colnames(combined) <- c("returns", "sma_short", "sma_long")
  combined <- na.omit(combined)

  # Lagged signal avoids look-ahead bias: position at t uses SMAs from t-1
  combined$signal <- ifelse(lag(combined$sma_short, 1) >= lag(combined$sma_long, 1),
                            1, -1)
  combined <- na.omit(combined)

  # Strategy returns
  signal <- combined$signal
  strategy_returns <- combined$returns * signal

  # Count trades (each distinct directional position entered, including initial)
  positions <- as.numeric(coredata(signal))
  pos_changes <- c(positions[1], diff(positions))
  long_trades <- sum(pos_changes ==  2) + ifelse(positions[1] ==  1, 1, 0)
  short_trades <- sum(pos_changes == -2) + ifelse(positions[1] == -1, 1, 0)

  # Percent of time long and short
  n_days <- length(positions)
  pct_long <- sum(positions ==  1) / n_days * 100
  pct_short <- sum(positions == -1) / n_days * 100

  # Cumulative return via geometric compounding
  cum_return <- as.numeric(prod(1 + strategy_returns) - 1) * 100

  # Buy-and-hold benchmark over the same trading days
  bh_return <- as.numeric(prod(1 + combined$returns) - 1) * 100

  summary_df <- data.frame(
    Metric = c("Total Long Trades", "Total Short Trades",
               "Percent Time Long (%)", "Percent Time Short (%)",
               "Cumulative Return (%)", "Buy-and-Hold Return (%)"),
    Value  = round(c(long_trades, short_trades, pct_long, pct_short,
                     cum_return, bh_return), 2)
  )

  results <- list(
    summary = summary_df,
    long_trades = long_trades,
    short_trades = short_trades,
    pct_long = pct_long,
    pct_short = pct_short,
    cum_return = cum_return,
    bh_return = bh_return,
    strategy_ret = strategy_returns
  )

  return(results)
}

3.1 Function 1 Results: TSLA, 2020-01-01 to 2026-03-31, SMA(50, 200)

f1 <- backtest_ma("TSLA", "20200101", "20260331", 50, 200)

kable(f1$summary,
      caption = "Death-Cross / Golden-Cross Back Test Summary for TSLA (2020–2026)",
      align = "lr") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Death-Cross / Golden-Cross Back Test Summary for TSLA (2020–2026)
Metric Value
Total Long Trades 6.00
Total Short Trades 5.00
Percent Time Long (%) 67.50
Percent Time Short (%) 32.50
Cumulative Return (%) 1.75
Buy-and-Hold Return (%) 1102.95

Interpretation: The strategy produces a cumulative return of 1.75% over the six-year window, spending 67.5% of the time long and 32.5% short across 6 long and 5 short trades. Critically, the strategy is technically profitable in absolute terms, it did not lose money, but it falls dramatically short of the buy-and-hold benchmark of 1102.95%, a gap of 1101.2 percentage points. A strategy does not need to produce negative returns to be a poor one; massively underperforming a passive position represents a real opportunity cost.

The underperformance in the full 2020–2026 window is not primarily explained by the COVID crash and recovery period. As the sub-period analysis in Function 2 will show, the strategy actually performed extremely well in 2020–2022, capturing TSLA’s strong directional move. Rather, the six-year result is driven largely by the 2022–2026 period, when TSLA’s price action became more volatile and range-bound, triggering repeated crossover signals that put the strategy on the wrong side of significant moves. With 32.5% of the test period spent short, and TSLA exhibiting a strong long-run upward trend over the full window, each day the strategy held a short position on a rising stock compounded losses against the buy-and-hold benchmark. Additionally, each crossover represents a full position reversal that would incur transaction costs — bid-ask spreads and brokerage commissions — in practice, further eroding returns beyond the figures reported here.

4 Function 2: Simulate Multiple Back-Test Periods

It’s easy to get a “lucky” or “unlucky” result by picking one specific window. To assess whether the death-cross signal is robust, we broke the 2018–2026 period into rolling 3-year sub-periods. Each sub-period spans exactly three calendar years (e.g., 2018–2020, 2019–2021, …, 2024–2026). To ensure valid SMA values from the first day of each sub-period, the initial data download includes a one-year buffer before the overall start year.

backtest_multi_period <- function(ticker, test_years, date_range,
                                  short_ma = 50, long_ma = 200) {

  start_year <- as.numeric(date_range[1])
  end_year <- as.numeric(date_range[2])

  # Download with one-year buffer for SMA warm-up
  buffer_start <- as.Date(paste0(start_year - 1, "-01-01"))
  all_prices <- get_prices_xts(ticker,
                                 buffer_start,
                                 as.Date(paste0(end_year, "-12-31")))

  # Roll test_years-year windows; last valid start = end_year - test_years + 1
  results_list <- list()
  period_labels <- c()
  cum_returns <- c()

  for (yr in start_year:(end_year - test_years + 1)) {
    begin <- paste0(yr, "0101")
    end <- paste0(yr + test_years - 1, "1231")
    label <- paste0(yr, "-", yr + test_years - 1)

    res <- backtest_ma(ticker, begin, end, short_ma, long_ma,
                       prices_xts = all_prices)

    results_list[[label]] <- res
    period_labels <- c(period_labels, label)
    cum_returns <- c(cum_returns, res$cum_return)
  }

  # Compute means across all iterations
  mean_long_trades <- mean(sapply(results_list, function(x) x$long_trades))
  mean_short_trades <- mean(sapply(results_list, function(x) x$short_trades))
  mean_pct_long <- mean(sapply(results_list, function(x) x$pct_long))
  mean_pct_short <- mean(sapply(results_list, function(x) x$pct_short))
  mean_cum_return <- mean(sapply(results_list, function(x) x$cum_return))

  summary_df <- data.frame(
    Metric = c("Mean Long Trades", "Mean Short Trades",
               "Mean Percent Time Long (%)", "Mean Percent Time Short (%)",
               "Mean Cumulative Return (%)"),
    Value  = round(c(mean_long_trades, mean_short_trades,
                     mean_pct_long, mean_pct_short, mean_cum_return), 2)
  )

  # Bar chart of cumulative return by sub-period
  plot_df <- data.frame(Period = period_labels, Cumulative_Return = cum_returns)
  plot_df$Period <- factor(plot_df$Period, levels = period_labels)

  p <- ggplot(plot_df, aes(x = Period, y = Cumulative_Return, fill = Period)) +
    geom_bar(stat = "identity", width = 0.6) +
    geom_text(aes(label = paste0(round(Cumulative_Return, 1), "%")),
              vjust = -0.5, size = 3.5) +
    labs(title = paste("Cumulative Return by Sub-Period:", ticker),
         subtitle = paste(test_years, "-Year Testing Windows  |  SMA(",
                          short_ma, ",", long_ma, ")", sep = ""),
         x = "Period", y = "Cumulative Return (%)") +
    theme_minimal() +
    theme(legend.position = "none",
          plot.title = element_text(face = "bold"))

  return(list(summary = summary_df, plot = p, details = results_list))
}

4.1 Function 2 Results: TSLA, 3-Year Windows, 2018–2026, SMA(50, 200)

f2 <- backtest_multi_period("TSLA", 3, c("2018", "2026"), 50, 200)

kable(f2$summary,
      caption = "Mean Results Across 3-Year Sub-Periods for TSLA",
      align = "lr") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Mean Results Across 3-Year Sub-Periods for TSLA
Metric Value
Mean Long Trades 3.14
Mean Short Trades 2.71
Mean Percent Time Long (%) 60.75
Mean Percent Time Short (%) 39.25
Mean Cumulative Return (%) 274.96
print(f2$plot)

Interpretation: The bar chart reveals a strikingly regime-dependent strategy. Three of the seven 3-year windows — 2018–2020, 2019–2021, and 2020–2022 — produced enormous positive returns, coinciding with TSLA’s sustained directional bull market. During this phase, the 50/200-day crossover worked as intended: persistent trends gave the moving averages enough time to align with price momentum, and the strategy captured large directional moves while positioned correctly. The four most recent windows — 2021–2023, 2022–2024, 2023–2025, and 2024–2026 — are all deeply negative, corresponding to TSLA’s post-peak period of elevated volatility and more range-bound price action. In this regime, frequent reversals generated a series of whipsaw losses as the strategy repeatedly switched direction without durable trends to exploit.

The mean cumulative return of 274.96% should be interpreted with caution: it is substantially elevated by the three strongly positive early windows and does not represent a typical outcome across the sample. The median result would tell a notably bleaker story. The stark contrast between the two halves of the sample — three consecutive profitable windows followed by four consecutive losing ones — is the most informative finding here. It demonstrates that the strategy’s performance is highly dependent on the market regime it encounters. When TSLA trended persistently in one direction, the death-cross signal was a powerful tool; when price action became choppy and mean-reverting, the same signal became a reliable source of losses. A strategy this sensitive to regime is not reliably deployable without a mechanism to identify favorable market conditions in advance.

5 Function 3: Simulate Multiple Short Moving-Average Windows

Finally, we wanted to see how sensitive the strategy was to the choice of the short moving-average window. Holding the long moving average fixed at 200 days, we varied the short window from 20 to 80 days in increments of 20.

backtest_multi_short_ma <- function(ticker, begin, end, low_short, high_short,
                                     ma_increment, long_ma = 200) {

  begin_date <- as.Date(begin, format = "%Y%m%d")
  end_date <- as.Date(end,   format = "%Y%m%d")

  # Download once with warm-up buffer, reused across all short MA iterations
  buffer_start <- begin_date - ceiling(long_ma * 1.5)
  all_prices <- get_prices_xts(ticker, buffer_start, end_date)

  short_windows <- seq(low_short, high_short, by = ma_increment)
  long_trades <- numeric(length(short_windows))
  short_trades <- numeric(length(short_windows))
  cum_returns <- numeric(length(short_windows))

  for (i in seq_along(short_windows)) {
    current_short <- short_windows[i]
    res <- backtest_ma(ticker, begin, end, current_short, long_ma,
                       prices_xts = all_prices)
    long_trades[i] <- res$long_trades
    short_trades[i] <- res$short_trades
    cum_returns[i] <- res$cum_return
  }

  results_df <- data.frame(
    Short_MA_Window = short_windows,
    Long_Trades = long_trades,
    Short_Trades = short_trades,
    Cumulative_Return = round(cum_returns, 2)
  )

  # Line plot of cumulative return vs short MA window
  p <- ggplot(results_df, aes(x = Short_MA_Window, y = Cumulative_Return)) +
    geom_line(color = "steelblue", linewidth = 1) +
    geom_point(color = "steelblue", size = 2.5) +
    geom_text(aes(label = paste0(round(Cumulative_Return, 1), "%")),
              vjust = -1, size = 3.2) +
    geom_hline(yintercept = 0, linetype = "dashed", color = "red") +
    labs(title = paste("Cumulative Return vs. Short MA Window:", ticker),
         subtitle = paste("Long MA fixed at", long_ma,
                          "days  |  Back Test:", begin, "to", end),
         x = "Short Moving-Average Window (days)",
         y = "Cumulative Return (%)") +
    theme_minimal() +
    theme(plot.title = element_text(face = "bold"))

  return(list(table = results_df, plot = p))
}

5.1 Function 3 Results: TSLA, 2020–2026, Short MA 20–80 (step 20), Long MA 200

f3 <- backtest_multi_short_ma("TSLA", "20200101", "20260331", 20, 80, 20, 200)

kable(f3$table,
      caption = "Strategy Performance Across Short MA Windows (Long MA = 200)",
      row.names = FALSE, align = "cccc") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Strategy Performance Across Short MA Windows (Long MA = 200)
Short_MA_Window Long_Trades Short_Trades Cumulative_Return
20 8 8 -51.39
40 7 6 -43.70
60 6 5 55.75
80 5 4 136.64
print(f3$plot)

Interpretation: The table and plot reveal how sensitive the death-cross / golden-cross strategy is to the choice of the short moving-average window. Performance improves monotonically as the short window increases: the 20-day window produces the worst result, and the 80-day window the best. A shorter window (20 days) makes the strategy more reactive, generating more frequent crossover signals — 16 total trades at 20 days versus 9 at 80 days. While this increases the potential to capture short-lived trends, it also amplifies exposure to whipsaw losses, situations where the averages cross back and forth without establishing a durable trend. A longer short window (80 days) smooths out noise and reduces trading frequency, keeping the strategy aligned with only the most sustained directional moves.

Notably, the improvement from 40-day to 60-day is non-linear — a swing of nearly 100 percentage points — suggesting a threshold effect rather than a smooth noise-reduction trade-off. The crossover from negative to positive cumulative returns appears to occur somewhere between the 40-day and 60-day windows for this stock and period.

However, even the best-performing parameter setting (80-day short window) still trails the buy-and-hold benchmark of 1102.95% by approximately 966.31 percentage points. Parameter optimization improves the strategy meaningfully but does not resolve its fundamental challenge of keeping pace with a strongly trending stock using a passive position. This result also raises a concern about in-sample overfitting: selecting the 80-day window based on these observed results would constitute optimization on the same data used for evaluation, and out-of-sample performance may differ considerably.

6 Discussion

After examining the results from all three functions, several key findings emerge.

Function 1 provided the baseline result for the standard 50/200-day crossover on Tesla over a six-year window. The strategy’s cumulative return of 1.75% compares poorly against a buy-and-hold return of 1102.95% over the same period — a gap of 1101.2 percentage points. Importantly, the strategy was not a money-loser in absolute terms; it simply failed to capture the outsized gains available to a passive investor in one of the decade’s strongest-trending stocks. The underperformance is concentrated in the 2022–2026 period, when TSLA’s price became more volatile and mean-reverting, rather than in the 2020–2022 period where the strategy actually performed well. Transaction costs are not included in this figure; each crossover represents a full position reversal that would incur bid-ask spreads and brokerage commissions in practice, reducing the reported cumulative return further.

Function 2 demonstrated the importance of not relying on a single time window and revealed that the strategy is fundamentally regime-dependent. The three earliest sub-periods (2018–2022) were all strongly positive, coinciding with TSLA’s sustained bull market phase during which the trend-following signal worked as designed. The four most recent sub-periods (2021–2026) were all deeply negative, corresponding to a post-peak environment of elevated volatility and range-bound price action that repeatedly triggered costly whipsaw signals. The mean cumulative return of 274.96% is dominated by the enormous early-period gains and overstates the typical outcome; the pattern of results — three wins followed by four consecutive losses — is the most informative feature of the data. A strategy that performs this inconsistently across market environments requires a regime-identification mechanism before it can be considered reliably deployable.

Function 3 addressed parameter sensitivity. The choice of 50 days for the short moving average is a widely adopted convention, not an optimization. By sweeping across 20, 40, 60, and 80 days, we can assess whether the strategy’s performance is robust to this design choice. Performance improved monotonically with window length, and longer windows (60–80 days) produced positive cumulative returns while shorter windows remained negative. Despite this improvement, even the best parameter (80-day) fell far short of buy-and-hold, and selecting it on the basis of in-sample results introduces overfitting risk. Parameter choice matters, but it cannot compensate for the strategy’s underlying regime sensitivity.

From a behavioral finance perspective, the death-cross and golden-cross signals are interesting because they are so widely followed. When a large proportion of market participants watch the same 50-day and 200-day moving averages, crossover events can become self-fulfilling: a golden cross triggers buying, which pushes prices higher, which reinforces the signal. This herding dynamic is distinct from the signal capturing fundamental value — it captures a coordination mechanism among technical traders. This distinction matters for interpreting the results: a strategy that profits from investor behavior rather than economic fundamentals is inherently fragile, and its effectiveness depends on the continued participation of traders acting on the same indicators. The 2018–2022 success of the strategy may partly reflect this self-fulfilling dynamic during a period of strong consensus bullishness on TSLA.

On the question of economic meaningfulness: the strategy’s cumulative return of 1.75% versus a buy-and-hold return of 1102.95% represents deeply negative alpha. Accounting for turnover and execution lag would only worsen that figure. Each crossover involves a full position reversal, meaning slippage on both the close and the open. Lagging execution — where the signal is confirmed only after the market close and the trade is executed at the following day’s open — introduces additional gap risk not captured in this backtest. A more complete evaluation would also consider risk-adjusted performance metrics such as the Sharpe ratio or volatility of returns, as a strategy that underperforms in absolute terms may still offer diversification or downside protection benefits under certain conditions.

7 Conclusion

This backtest demonstrates how a widely followed technical indicator, the 50/200-day moving-average crossover, performs when applied systematically to Tesla. The death-cross and golden-cross signals capture the essence of trend-following: ride momentum when it confirms, and reverse when the trend breaks. The three functions we built provide a complete analytical toolkit, examining a single base case, testing robustness across time periods, and assessing parameter sensitivity.

The key takeaway is not that the death-cross strategy universally fails on TSLA, but that it is highly regime-dependent. During TSLA’s sustained bull market from 2018 to 2022, the strategy performed remarkably well — returning over 1,000% in the 2019–2021 window — because persistent directional trends gave the 200-day SMA enough time to align with price momentum, and the strategy captured large moves while positioned correctly. The strategy’s breakdown occurred specifically in the 2021–2026 period, when TSLA’s price action became more volatile and range-bound, generating repeated false crossover signals that produced a series of costly whipsaw losses. The single six-year result from Function 1 (+1.75% versus +1,102.95% buy-and-hold) is therefore best understood as the compounded result of four consecutive difficult years, not as evidence that the strategy was fundamentally misaligned with TSLA’s price behavior throughout the entire period.

The Function 3 results further reinforce that parameter choice matters significantly. While longer short windows (60–80 days) substantially outperform shorter ones by reducing trading frequency and filtering noise, even the best parameter choice (+136.64% at 80 days) trails buy-and-hold by nearly 966 percentage points. Selecting parameters based on observed performance also introduces in-sample overfitting risk, meaning out-of-sample results may be considerably weaker. Moving forward, potential improvements could include adding a confirmation filter (requiring the crossover to persist for several days before acting), incorporating a cash or exit option during ambiguous regimes, using exponential rather than simple moving averages to reduce lag, or combining the crossover signal with volatility-based position sizing to limit downside during uncertain market conditions.

So what did we learn from this? Beyond the results themselves, this assignment helped to reinforce several lessons about rigorous quantitative analysis. First, backtesting is deceptively easy to do incorrectly: subtle mistakes like computing moving averages only over the test window (rather than a buffered history) or failing to lag the signal can introduce look-ahead bias that makes a strategy appear far better than it truly is. Second, a single backtest window can be deeply misleading — the rolling sub-period analysis in Function 2 made clear that the same strategy can look like a winner or a loser depending entirely on which years are examined, with returns ranging from over +1,000% to nearly −90% across the seven windows. This is a stark warning against cherry-picking results. Third, the buy-and-hold benchmark reframes the entire discussion: a strategy does not need to lose money to be a bad strategy; it only needs to underperform the passive alternative after accounting for its costs and risks — and with a positive but minimal return of +1.75% against a benchmark of +1,102.95%, this point is illustrated vividly here. Finally, connecting the mechanics of the signal to behavioral finance concepts (anchoring, herding, underreaction) gave us a framework for thinking about why such rules might work in some environments and fail in others, rather than treating the backtest result as a black box. These lessons apply well beyond the death-cross signal to quantitative research more broadly.

8 References

  • Murphy, J. J. (1999). Technical Analysis of the Financial Markets. New York Institute of Finance.
  • Brock, W., Lakonishok, J., & LeBaron, B. (1992). Simple Technical Trading Rules and the Stochastic Properties of Stock Returns. Journal of Finance, 47(5), 1731–1764.
  • Shiller, R. J. (2000). Irrational Exuberance. Princeton University Press.
  • De Bondt, W. F. M., & Thaler, R. (1985). Does the Stock Market Overreact? Journal of Finance, 40(3), 793–805.
  • TTR: Technical Trading Rules. R Package. CRAN.