Strategy Study

SPY Golden Cross Backtest in Python

A reproducible SPY golden cross backtest in Python using the 50/200 SMA crossover, lagged signals, transaction costs, charts, CSV output, and code.

Updated Jul 05, 2026 / Data: Yahoo Finance via yfinance, adjusted OHLCV / US ETF

Quick Take

31 trades over 33 years. Compared to buy and hold, the golden cross reduced volatility and drawdown but finished behind on ending wealth. Compared to a single 200-day SMA filter, it traded far less and earned more. It reads as a slow, low-maintenance risk filter, not a return enhancer.

Why the Golden Cross

The golden cross — when a shorter moving average crosses above a longer one — is one of the most quoted signals in technical analysis. I wanted to see what the classic 50-day / 200-day version actually does on SPY once the timing, costs, and benchmark are pinned down.

Method

The backtest computes two simple moving averages from the daily adjusted close series. The strategy holds SPY when the prior day’s SMA50 is above the prior day’s SMA200, and holds cash otherwise.

I kept the same close-to-close model used in the single-SMA study, so the comparison is not mixing execution assumptions.

Data

FieldValue
SourceYahoo Finance via yfinance
TickerSPY
Price seriesAdjusted close
Start date1993-01-29
End date2026-07-02
First valid SMA date1993-11-11
Metric windowIncludes the SMA warmup period
Initial capital$10,000
Base transaction cost5 bps per position change
Cash return0%

The data/SPY.csv file is a local cache for this study. Running python3 backtest.py --refresh-data replaces it with fresh yfinance data.

Signal Definition

For each trading day t, the script computes:

SMA50[t]  = mean(adjusted_close[t-49] ... adjusted_close[t])
SMA200[t] = mean(adjusted_close[t-199] ... adjusted_close[t])

The position for day t uses only information available at the close of day t-1:

signal[t-1] = 1 if SMA50[t-1] > SMA200[t-1], else 0
position[t] = signal[t-1]

When SMA50[t-1] crosses above SMA200[t-1], it is a golden cross. When it crosses below, it is a death cross. The strategy does not trade only on crossover dates; the daily position is a state rule: hold SPY while SMA50 stays above SMA200, and switch to cash when the relationship flips. The first 200 trading days have no valid SMA200 and therefore produce no signal.

Execution and Cost Assumptions

The execution model is the same close-to-close approximation used in the single-SMA study. A signal based on the adjusted close of day t-1 determines the position for the interval from close t-1 to close t. This keeps the adjusted price series internally consistent, but it is not a next-open fill model.

The base case deducts 5 bps of portfolio equity each time the position changes, from cash to SPY or SPY to cash. At 31 trades across the full sample, the cost drag is far smaller than in the single-SMA study, where 215 position changes accumulated more friction.

Results

The golden cross earned a 9.64% CAGR, ahead of the single-SMA strategy’s 8.08% but still behind buy and hold’s 10.81%. Volatility and drawdown were both lower than the benchmark, though the strategy’s max drawdown of -33.72% was deeper than the single-SMA filter’s -29.42%.

The standout number is turnover. With only 31 position changes across 33 years, the golden cross is a very slow system. The trade count is close to one entry and one exit every two years.

Base case results, 5 bps per position change. Metrics include the SMA warmup period.
Metric Strategy Benchmark Note
CAGR 9.64% 10.81% 1993-01-29 to 2026-07-02
Annualized volatility 13.66% 18.57% Daily returns annualized with 252 trading days
Sharpe ratio 0.74 0.65 0% risk-free rate
Max drawdown -33.72% -55.19% Peak-to-trough equity drawdown
Calmar ratio 0.29 0.20 CAGR divided by absolute max drawdown
Time in market 75.15% 100.00% Daily position average
Position changes 31 Initial buy only Cash-to-SPY and SPY-to-cash changes
Final equity $216,554 $308,867 $10,000 initial capital
SPY golden cross 50/200 SMA crossover strategy equity curve compared with SPY buy and hold
Strategy versus buy-and-hold equity curve. The y-axis is log scale and the chart is generated from the base-case equity CSV.
Drawdown chart comparing the SPY golden cross strategy with SPY buy and hold
Drawdowns for the strategy and benchmark. The crossover rule reduced the largest drawdown but left about one quarter of trading days in cash.
SPY adjusted close with SMA50, SMA200, and shaded risk-on periods
Adjusted close, SMA50, SMA200, and risk-on periods. Shading indicates days when the lagged signal holds SPY. Golden crosses are where the blue SMA50 line crosses above the orange SMA200 line.

Interpretation

The golden cross trade count tells the story. 31 position changes over 33 years is a very different system from the single-SMA rule’s 215 changes. The slower crossover smooths out the whipsaw that can plague faster filters, and that shows up in the final wealth numbers. At $216,554, the golden cross strategy ends about 61% ahead of the single-SMA rule’s $134,111.

The cost side of that tradeoff is drawdown depth. The golden cross is slower to react to a reversal than a single moving-average filter. Because SMA50 must cross all the way through SMA200 before the rule exits, the strategy can stay invested through the start of a downturn. This is visible in the cost sensitivity table too: at only 31 trades, the spread between 0 bps and 10 bps costs is only about $6,700. Friction is not the binding constraint for this strategy. Signal lag is.

Robustness Checks

The main result uses 5 bps per position change. The table below reruns the same signal with 0 bps and 10 bps costs.

Cost per position changeCAGRSharpeMax drawdownFinal equity
0 bps9.69%0.75-33.72%$219,940
5 bps9.64%0.74-33.72%$216,554
10 bps9.59%0.74-33.72%$213,219

The cost sensitivity is small because the strategy rarely trades. At 31 total position changes, even doubling the assumed cost per trade does not change the story.

Comparison with the Single 200-Day SMA Rule

The single 200-day SMA study used a simpler filter: hold SPY when price is above SMA200, go to cash when it is below. That rule traded 215 times. The golden cross, with its second slower-moving SMA, traded 31 times. Here is a side-by-side comparison at the 5 bps base case.

MetricGolden cross (50/200)Single SMA200Buy and hold
CAGR9.64%8.08%10.81%
Annualized volatility13.66%11.99%18.57%
Sharpe ratio0.740.710.65
Max drawdown-33.72%-29.42%-55.19%
Position changes31215-
Final equity$216,554$134,111$308,867

The difference in final wealth — $216,554 versus $134,111 — comes primarily from the golden cross staying invested during pullbacks that the single-SMA filter exited and re-entered. The price of that patience was a slightly deeper max drawdown. Whether patience or reactivity is the better trait depends on the market environment and the user’s objective.

Limitations

The execution model is the largest simplification. A close-to-close approximation is useful for a first test, but it overstates the quality of fills at the exact moment the crossover signal is generated. A next-open or one-day-delayed next-close model would be a more conservative follow-up.

Cash return at 0% also understates performance while the strategy is in cash during periods when short-term Treasury rates are high, as in the 2022-2024 window. Adding a Treasury bill proxy would close one of the larger gaps between this model and a real portfolio.

Reproducibility

Run the study from the research repository:

cd studies/spy-golden-cross
pip install -r requirements.txt
python3 backtest.py
python3 plot.py
python3 -m unittest discover -s . -p "test_*.py"

The generated files are:

FilePurpose
data/SPY.csvCached adjusted OHLCV from yfinance
outputs/spy-golden-cross-summary.csvSummary metrics for 0, 5, and 10 bps costs
outputs/spy-golden-cross-equity.csvDaily signal, position, returns, costs, equity, and drawdowns
outputs/spy-golden-cross-trades.csvPosition-change log
charts/spy-golden-cross-equity-curve.svgEquity curve chart
charts/spy-golden-cross-drawdowns.svgDrawdown chart
charts/spy-golden-cross-price-sma.svgAdjusted close, SMA50, and SMA200 chart

FAQ

What is a 50/200 SMA crossover strategy?

A 50/200 SMA crossover strategy compares the 50-day simple moving average with the 200-day simple moving average. In this test, SPY is held while SMA50 is above SMA200 and cash is held otherwise. The upward crossover is commonly called a golden cross; the downward crossover is a death cross.

Does the golden cross strategy beat buy and hold on SPY?

Not by ending wealth or CAGR in this version. Under the base assumptions, the strategy ended at $216,554 versus $308,867 for buy and hold. The tradeoff was lower volatility and a meaningfully smaller maximum drawdown.

How does the golden cross compare to a single 200-day moving average strategy?

In this sample, the golden cross earned a higher CAGR (9.64% vs 8.08%), traded far less (31 vs 215 position changes), and finished with much higher ending wealth ($216,554 vs $134,111). However, the single-SMA filter had a shallower max drawdown (-29.42% vs -33.72%). The golden cross is a slower, lower-turnover system; the single-SMA filter is more reactive.

Why use adjusted close data for SPY?

SPY pays distributions. Adjusted close accounts for dividends and stock splits, keeping the strategy and benchmark return series consistent.

How is look-ahead bias avoided in this backtest?

The signal is shifted before returns are applied. The position for day t uses SMA50[t-1] and SMA200[t-1], not any information from day t.

More notes