Prices come from Fruit Stand’s Historical Stock Prices listing and T-bill
rates from our FRED listing, through November 6, 2026. Every query here
was run on November 8, 2026. A backtest describes the past, and this isn’t
investment advice.
The strategy
The 50/200-day crossover is the classic trend rule, often called the golden cross and the death cross:- Signal: in the market when the 50-day simple moving average of the price is above the 200-day average, out when it’s below.
- Prices:
adjusted_close, which includes splits and dividends, so the returns are total returns. See Adjusted Close vs. Close for why the raw close breaks here. - Timing: the signal is calculated at a day’s close and acted on the next day, so the strategy never trades on information it didn’t have yet.
- Cash: when out of the market, the money earns the 3-month Treasury
bill rate (FRED series
DTB3). - Test: SPY (S&P 500) and QQQ (Nasdaq-100), each from its 201st trading day, when the 200-day average first exists.
Step 1: Build the daily positions
One query computes both averages, the position, and each day’s strategy return. Saving it as a table keeps the next queries short:rows between 199 preceding and current rowmakes the window a fixed 200 trading days. Skipping the first 200 days (day_number > 200) drops the rows where the average covers fewer days.lag(...)on the signal is what prevents look-ahead bias: today’s return is earned by yesterday’s decision.asof joinmatches each trading day to the latest T-bill rate on or before it, so holidays and missing rate days don’t drop rows.
Step 2: Score it against buy-and-hold
Compound the daily returns withexp(sum(ln(1 + r))), track each curve’s
running peak for drawdowns, and annualize:
The results
Through November 6, 2026:
On SPY, the crossover gave up 0.4 points a year of return for a much
smoother ride: it was out for most of the 2008 crash, and its worst loss was
a third rather than more than half. $10,000 grew to $251,169 with the
strategy and $284,394 buying and holding.
On QQQ, it beat buy-and-hold outright, but mostly because of when the test
starts. QQQ’s 200th trading day fell in December 1999, three months before
the dot-com peak, and buy-and-hold then lost 83%. A backtest that starts at
a bubble’s top flatters any rule that sidesteps the crash.
The last 10 years tell a different story. From November 7, 2016, in a
mostly rising market:
A trend rule pays for crash protection by being late, and in a decade with
fast recoveries it was late in both directions.
The trades it made
The 2020 trades show the weakness. The S&P 500 bottomed on March 23, 2020,
but the 50-day average only crossed below the 200-day on April 1, so the
strategy sold after the crash and bought back in July, higher. 2022 went
better: it sold in March and missed most of that year’s decline.
Mistakes that inflate a backtest
- Look-ahead bias. Trading on the same day’s signal, which uses that
day’s close, lifts SPY’s result from 9.79% to 10.15% a year before cash
interest. Always
lagthe signal. - Ignoring cash. Leaving cash at 0% understates a strategy that’s out of the market a quarter of the time. T-bill interest added about half a point a year: SPY’s crossover returned 9.79% without it and 10.27% with it.
- Raw prices. The raw
closedrops on every split and every dividend, so moving averages built on it trigger false sell signals. Useadjusted_close. - Costs. At 0.1% per trade, 30 trades cost SPY’s crossover about 0.1 points a year (10.27% to 10.17%). Rules that trade weekly pay far more.
- The start date. As QQQ shows, the first day of the test can decide the answer. Rerun from a few different starts before trusting a result.
- Survivorship bias. Index ETFs like SPY and QQQ avoid it, but a stock backtest on today’s companies doesn’t include the ones that failed. The Historical Stock Prices listing doesn’t include delisted securities.
- Overfitting. Testing many average lengths and keeping the best is easy in SQL, which makes it easy to fool yourself. Pick the rule first, or keep a period you don’t look at until the end.
Testing other tickers and rules
Change thecode in (...) list to test any US stock, ETF or mutual fund in
the listing; the queries handle many tickers at once because every window is
partition by code. To test a different rule, change the iff(...) in the
positions step: for example, iff(adjusted_close > sma_200, 1, 0) for the
simpler price-above-200-day rule (add adjusted_close to the prices
step’s columns first).
For more SQL on the same data, see How to Calculate Inflation-Adjusted
Returns in SQL, which joins the same
two listings.
F.A.Q.
Can Snowflake run a backtest without Python?
Can Snowflake run a backtest without Python?
Yes. A rule built from moving averages, price levels or other window calculations is plain SQL, as above. Python in Snowflake (Snowpark) helps for strategies with complex state, such as stop-losses that depend on the entry price.
Does the golden cross beat buy-and-hold?
Does the golden cross beat buy-and-hold?
Not on SPY since 1993: 10.27% a year against 10.68%, though with a 34% maximum drawdown instead of 55%. On QQQ from December 1999 it did beat buy-and-hold, largely because the test started just before the dot-com crash.
How do I avoid look-ahead bias in SQL?
How do I avoid look-ahead bias in SQL?
Calculate the signal from data up to a day’s close, then use
lag() so the position applies from the next day’s return. Never let a row’s own return decide its position.Why use adjusted prices for a backtest?
Why use adjusted prices for a backtest?
Adjusted prices include splits and dividends, so returns are total returns and moving averages don’t jump on split days. The raw close would trigger false signals and leave out dividends.
How much compute does this use?
How much compute does this use?
Very little. The backtest above covers about 15,000 rows, and window functions over the full price history of thousands of tickers run in seconds on a small warehouse.