The 50/200 moving-average crossover — the golden cross — is the systematic trend-following workhorse: one rule, two parameters, a century of folklore. That makes it the perfect vehicle for backtesting discipline 101. We run the same long-or-flat rule on QQQ and BTC-USD over 2015–2024 with next-day execution and 10bp per-side costs, then do what most crossover backtests skip: sweep the parameter grid, stress the costs, and admit what one in-sample decade can and cannot prove.

  1. 01Download QQQ + BTC-USD closes, 2015–2024 (yfinance), dropna per asset
  2. 02Signal: 50d SMA above 200d SMA → long, else flat
  3. 03Execute next day — signal.shift(1), 10bp per side
  4. 04Compare vs buy-and-hold; cross-check fills with vectorbt
  5. 05Sweep the 4×4 fast/slow Sharpe grid per asset
  6. 06Stress costs (0/10/25bp) and file the in-sample caveats

1.The rule: two moving averages

Compute a fast and a slow simple moving average of the close. When the fast SMA sits above the slow one, the market is trending up — be long. When it drops below, be flat. No shorting, no leverage, no discretion. With fast = 50 and slow = 200 this is the golden cross of financial television fame, and a one-asset special case of time-series momentum — the effect Moskowitz, Ooi and Pedersen documented across 58 futures markets.

We run it on two deliberately different animals: the Nasdaq-100 ETF (exchange calendar, ~252 bars a year) and Bitcoin (trades every day of the week, ~365 bars a year). BTC has prices on weekends where QQQ has none, so each asset is backtested on its own calendar — per-asset dropna, per-asset annualisation — never forward-filled onto a shared grid.

2.Next-day execution — the shift(1) that keeps you honest

The most important line in the backtest is not the signal. It is the shift. Today's SMA cross is computed on today's close— a price you cannot trade before you have seen it. So today's signal may only earn tomorrow's return. Drop the shift and the backtest buys every up-day one bar early: a lookahead bias that flatters almost any signal and quietly fabricates Sharpe.

backtest.py
def backtest(px, fast=50, slow=200, fee=0.001):
    signal   = (px.rolling(fast).mean() > px.rolling(slow).mean()).astype(float)
    position = signal.shift(1).fillna(0.0)   # <- no lookahead. The whole game.
    ret      = px.pct_change().fillna(0.0)
    cost     = position.diff().abs().fillna(0.0) * fee   # 10bp per side
    return (position * ret - cost).iloc[250:]   # SMA warm-up window

Two more discipline details. Every position change pays 10bp — entries and exits both. And the first 250 bars of each asset are reserved for SMA warm-up, so every parameter pair we test later is judged on the same evaluation window: 2015-12-302024-12-31 for QQQ, 2015-09-082024-12-31for BTC. The pandas core above is deliberately transparent; we rebuild the identical trades with vectorbt's Portfolio.from_signals as an independent referee — final value, max drawdown and trade count agree.

3.Same rule, two verdicts

Growth of $100, log scale (BTC would render every other line invisible otherwise). Watch where the strategy detaches from buy-and-hold: always in the big drawdowns. A slow trend filter is not an accelerator — it is a brake pedal.

$100$126$158$200$251$316$398'15'17'19'21'23
QQQ — growth of $100 (log scale), 2015-12-30 → 2024-12-3150/200 crossoverbuy & hold
$100$316$1,000$3,162$10,000$31,623'15'17'19'21'23
BTC-USD — growth of $100 (log scale), 2015-09-08 → 2024-12-3150/200 crossoverbuy & hold
Ann retAnn volSharpeMax DDTradesIn mkt
QQQ · 50/200 crossover17.9%18.9%0.97-28.6%582%
QQQ · buy & hold19.0%22.2%0.90-35.1%100%
BTC · 50/200 crossover79.3%59.3%1.28-69.3%964%
BTC · buy & hold89.6%69.1%1.27-83.4%100%

On QQQ the crossover ends at $439 versus $478 for buy-and-hold — it lost the return race — but it did so at 18.9% vol instead of 22.2% and cut the worst drawdown from -35.1% to -28.6%, for a slightly better Sharpe (0.97 vs 0.90). On BTC it kept pace with one of the great bull markets in modern data ($23,100 vs $38,954) while sitting out 36% of all days and trimming the max drawdown from -83.4% to -69.3%. Neither run is a money machine. Both are drawdown insurance — and the whole 10-year QQQ position changed hands just 5 times. Two conservatisms are baked into every number above and worth naming: flat days are credited nothing (the QQQ variant sits in cash 18%of the time — park that at the T-bill rate and the strategy line improves while buy-and-hold's cannot), and Sharpe is quoted on raw rather than excess returns. Both choices shade against the strategy, which is the right direction to be wrong in.

4.Discipline 101: the parameter grid

One backtest is an anecdote. Before believing 50/200, ask whether the neighbourhood agrees: we sweep fast ∈ {10, 20, 50, 100} × slow ∈ {100, 150, 200, 250}15 valid systems per asset, identical execution, identical costs — and heat-map the Sharpe. Both grids share one colour scale, so a glance tells you which asset rewarded trend.

100150200250100.880.960.860.81200.780.850.910.94500.840.890.970.941001.010.930.89
QQQ — Sharpe by fast (rows) × slow (cols), 10bp per side · buy-and-hold = 0.90
100150200250101.431.321.391.31201.361.341.311.26501.271.301.281.181001.281.301.37
BTC-USD — Sharpe by fast (rows) × slow (cols), 10bp per side · buy-and-hold = 1.27

This is the card's thesis in one picture. QQQ: every cell lands between 0.78 and 1.01, straddling buy-and-hold's 0.90 — only 7 of 15cells beat it, none decisively. Picking the best cell after the fact and calling it edge is selection bias, not alpha; on this asset the honest claim is "no return edge, a consistent drawdown brake". BTC: 12 of 15cells sit above buy-and-hold's 1.27, the whole grid stays in the 1.181.43 band, and every cell slashes the max drawdown. The same rule gets two different verdicts because trend behaves differently per asset— Bitcoin's decade delivered longer, cleaner trends and far deeper crashes for the filter to sidestep.

5.The costs dial: 0 / 10 / 25 bp

Costs are the second-biggest backtest killer after lookahead — but they bite in proportion to turnover. The 50/200 pair trades a handful of times a decade, so even 25bp per side barely dents it. Speed the system up to 10/100 and the toll booth opens:

TradesSharpe @ 0bp@ 10bp@ 25bpFinal $100 @ 10bp
QQQ · 50/20050.970.970.96$439
QQQ · 10/100180.900.880.84$311
BTC · 50/20091.291.281.28$23,100
BTC · 10/100211.441.431.42$40,381

Slow trend is nearly free to run. Fast trend pays a visible tax — QQQ's 10/100 variant loses 0.06 Sharpe going from free execution to 25bp, on 18 round-trip entries. Any strategy whose backtest only works at 0bp does not work.

6.One sample, no walk-forward

Everything above is one ten-year window, evaluated in-sample. There is no walk-forward, no out-of-sample holdout, and no multiple-testing haircut for the 15 variants we just eyeballed per asset. 2015–2024 handed both assets two of the strongest trend decades they have ever printed — a regime gift the next decade owes nobody. Before promoting any cell of that grid, run the standard honesty checklist: point-in-time data, walk-forward splits, pessimistic costs, and a deflated Sharpe— Bailey & López de Prado's correction for exactly the selection bias a 15-cell grid search manufactures — on whatever looked best.

References

  1. 1.Brock, W., Lakonishok, J. & LeBaron, B. (1992). Simple Technical Trading Rules and the Stochastic Properties of Stock Returns. Journal of Finance 47(5).
  2. 2.Moskowitz, T., Ooi, Y.H. & Pedersen, L.H. (2012). Time Series Momentum. Journal of Financial Economics 104(2).
  3. 3.Bailey, D.H. & López de Prado, M. (2014). The Deflated Sharpe Ratio: Correcting for Selection Bias, Backtest Overfitting and Non-Normality. Journal of Portfolio Management 40(5).
  4. 4.vectorbt documentation — Portfolio.from_signals, the signal-based backtesting API used as the fill cross-check.
  5. 5.Companion notebook: sma-crossover-backtest.ipynb — reproduces every figure from raw data (deterministic, no simulation).