Reproducible Backtest in Python, No API Key
A 20/50 moving-average crossover on SPY, year by year, against holdingranking ·
2026-08-06 · 9×4
The same 20/50 rule on five liquid names, 2021 through 2025ranking ·
2026-08-06 · 5×4
Same rule, prior-session signal against same-session signal, SPY by yearranking ·
2026-08-06 · 9×4
A 20/50 moving-average crossover on SPY, year by year, against holding
A 20/50 moving-average crossover on SPY, year by year, against holding
| year | rule_pct | hold_pct | crossover_count |
|---|---|---|---|
| 2017 | 16 | 19.4 | 4 |
| 2018 | 6.1 | -6.3 | 3 |
| 2019 | 7.6 | 28.7 | 5 |
| 2020 | 17.4 | 16.1 | 4 |
| 2021 | 18.7 | 27 | 2 |
| 2022 | -24.7 | -19.5 | 6 |
| 2023 | 9.4 | 24.3 | 6 |
| 2024 | 13 | 23.3 | 4 |
| 2025 | 10.4 | 16.4 | 4 |
the exact SQL behind every number
WITH daily AS
(
SELECT
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= '2016-01-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY d
),
averaged AS
(
SELECT
d,
px,
avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
d,
px,
if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
toYear(d) AS year,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
round((exp(sum(log(px / px_prior))) - 1) * 100, 1) AS hold_pct,
countIf(long_today != long_prior) AS crossover_count
FROM positioned
WHERE px_prior > 0
AND toYear(d) >= 2017
GROUP BY year
ORDER BY year
More from this analysisReproducible Backtest in Python, No API Key
Same rule, prior-session signal against same-session signal, SPY by year
ranking 9×4
→
The same 20/50 rule on five liquid names, 2021 through 2025
ranking 5×4
→
US tickers trading in a June week, and how many still traded in June 2026
ranking 11×4
→
Symbols that printed a final daily bar, by year
ranking 10×3
→
See all 2,170 queries →