Strategy Study

Daily vs Weekly vs Month-End SPY 200DMA Backtest

A reproducible SPY 200-day moving average signal frequency backtest in Python comparing daily, weekly, and month-end signal checks with transaction costs, charts, CSV output, and code.

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

Quick Take

This is a small timing-detail study around the same SPY 200-day moving average rule. The moving average is always a 200-trading-day SMA. The only change is how often the signal is checked.

In this sample, checking less often did reduce position changes. Daily had 215 position changes, weekly had 109, and month-end had 43. That did not create one clean ordering across all metrics. Weekly had lower turnover than daily but a deeper drawdown. Month-end had the highest final equity of the three variants, but it is also a slower rule with different path risk.

I read the result as sensitivity evidence: the 200DMA rule is not just “the 200DMA rule.” The signal calendar matters.

Why Signal Frequency

Most descriptions of a SPY 200 day moving average backtest say something like “hold SPY above the 200-day moving average and hold cash below it.” That still leaves an implementation choice: how often do you check?

Checking every day reacts quickly, but it also sees every small cross around the moving average. Checking weekly or at month-end ignores some of that noise, but it also accepts more delay. This study isolates that choice while keeping the data, SMA length, benchmark, cost model, and execution approximation fixed.

Method

The backtest compares three versions of the same rule:

VariantSignal check
DailyEvery trading day
WeeklyLast available trading day of each week
Month-endLast available trading day of each month

All three variants compute the same daily SMA200 from adjusted close:

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

This is not a 200-week or 200-month moving average test. The SMA is always based on 200 trading days; only the signal-check calendar changes.

Data

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

The script uses a local data/SPY.csv cache when present. Running python3 backtest.py --refresh-data replaces the cache with a fresh yfinance download.

Signal Definition

The daily version checks this condition every trading day:

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

The weekly version checks the same condition only on the last trading day of each week. Other days carry forward the last checked signal:

raw_signal[t] = 1 if t is the week's last trading day and adjusted_close[t] > SMA200[t]
signal[t] = last non-missing raw_signal
position[t] = signal[t-1]

The month-end version does the same thing on the last trading day of each month:

raw_signal[t] = 1 if t is the month's last trading day and adjusted_close[t] > SMA200[t]
signal[t] = last non-missing raw_signal
position[t] = signal[t-1]

The first 199 trading days have no valid SMA200, so they cannot produce a risk-on signal.

Execution and Cost Assumptions

This study uses the same close-to-close same-close approximation as the earlier daily 200DMA study. A signal checked at the close of day t-1 determines the modeled position for the close t-1 to close t return interval.

The important timing point is that a signal update date is not used for that same close-to-close return. The code computes position = signal.shift(1) for all variants.

Results

At 5 bps per position change, month-end had the highest CAGR, highest final equity, and shallowest max drawdown in this sample. Daily traded the most, while weekly reduced turnover but had a deeper drawdown than daily.

MetricDailyWeeklyMonth-endSPY buy and hold
CAGR8.08%7.98%9.88%10.81%
Annualized volatility11.99%11.98%12.56%18.57%
Sharpe ratio, 0% rf0.710.700.810.65
Max drawdown-29.42%-34.65%-25.73%-55.19%
Calmar ratio0.270.230.380.20
Time in market75.35%75.09%75.63%100.00%
Position changes21510943Initial buy only
Final equity$134,111$130,145$232,962$308,867
SPY 200 day moving average signal frequency equity curves for daily weekly and month-end checks
Equity curves for the daily, weekly, and month-end signal-check variants. All strategy lines use the same 200-trading-day SMA and the 5 bps base cost.
Drawdowns for SPY 200 day moving average signal frequency variants
Drawdowns by signal-check frequency. Weekly checking reduced turnover compared with daily checking, but the worst drawdown was deeper in this sample.
Position changes for daily weekly and month-end SPY 200DMA signal checks
Position changes in the 5 bps base case. Less frequent checking reduced turnover, but turnover alone did not explain every performance difference.

Interpretation

The cleanest result is turnover. Daily checking caught many more small crosses around the SMA200. Weekly checking cut the number of position changes roughly in half, and month-end checking cut it much further.

The performance path is less clean. Weekly checking had fewer trades than daily, but it ended slightly lower and had a deeper max drawdown. Month-end checking looked better in this historical sample, but that does not establish a general ranking. It means this particular calendar avoided some daily whipsaw without being badly late in the major episodes that mattered most for this sample.

That is a useful distinction. Signal frequency changes both friction and timing. Lower turnover helps only if the ignored signals are mostly noise; it can hurt when the ignored signals contain useful information.

Robustness Checks

The table below reruns each variant at 0, 5, and 10 bps per position change.

VariantCostCAGRSharpeMax drawdownPosition changesFinal equity
Daily0 bps8.43%0.74-28.00%215$149,340
Daily5 bps8.08%0.71-29.42%215$134,111
Daily10 bps7.73%0.68-30.82%215$120,429
Weekly0 bps8.16%0.72-33.99%109$137,439
Weekly5 bps7.98%0.70-34.65%109$130,145
Weekly10 bps7.80%0.69-35.31%109$123,234
Month-end0 bps9.95%0.82-25.51%43$238,010
Month-end5 bps9.88%0.81-25.73%43$232,962
Month-end10 bps9.81%0.81-25.95%43$228,019

The cost sensitivity mostly follows turnover. Daily has the widest spread between 0 and 10 bps because it changes position 215 times. Month-end has the smallest cost drag because it changes position 43 times.

Comparison with the Daily 200DMA Study

The daily variant here is the same rule as the earlier SPY 200-day moving average backtest: adjusted close above daily SMA200, lagged one day, 5 bps base cost, cash at 0%.

That gives a useful anchor. The earlier study asked what the basic daily rule did. This study asks whether the result is sensitive to the signal calendar. The answer is yes. Using the same 200DMA, the base-case final equity ranged from $130,145 for weekly checks to $232,962 for month-end checks, while the daily version ended at $134,111.

Limitations

The execution model is still a simplification. A close-to-close approximation keeps the adjusted close return series internally consistent, but it is not a next-open fill model.

Cash earns 0%, which understates cash-period returns during higher-rate periods. Taxes, account constraints, bid/ask spreads, market impact, and intraday order behavior are not modeled.

The month-end 200 day moving average strategy variant also depends on the historical calendar path. A different asset, sample window, cash proxy, or execution model could change the ranking across daily, weekly, and month-end checks.

Reproducibility

Run the study from the research repository:

cd studies/spy-200-day-moving-average-signal-frequency
pip install -r requirements.txt
python3 -B -m unittest discover -s . -p "test_*.py"
python3 backtest.py
python3 plot.py

The generated files are:

FilePurpose
data/SPY.csvCached adjusted OHLCV from yfinance
outputs/spy-200dma-signal-frequency-summary.csvSummary metrics for all variants and 0/5/10 bps cost scenarios
outputs/spy-200dma-signal-frequency-equity.csvBase-case daily signal, position, returns, costs, equity, and drawdowns
outputs/spy-200dma-signal-frequency-trades.csvBase-case position-change log
charts/spy-200dma-signal-frequency-equity-curve.svgEquity curve comparison
charts/spy-200dma-signal-frequency-drawdowns.svgDrawdown comparison
charts/spy-200dma-signal-frequency-position-changes.svgPosition-change comparison

Only the summary CSV and SVG charts are copied into this site. The full equity and trade CSVs are generated by the code above.

FAQ

Is this a 200-week or 200-month moving average strategy?

No. It is always a 200-trading-day simple moving average. Weekly and month-end refer only to how often the daily SMA200 signal is checked.

How is the weekly signal calculated?

The script identifies the last available trading day in each week. On that date, it checks whether adjusted close is above the daily SMA200. The resulting signal is carried forward until the next weekly check, and the position is shifted by one trading day.

How is the month-end signal calculated?

The script identifies the last available trading day in each calendar month. On that date, it checks whether adjusted close is above the daily SMA200. The resulting signal is carried forward until the next month-end check, and the position is shifted by one trading day.

Does checking less often reduce whipsaw?

It reduced position changes in this sample: 215 for daily, 109 for weekly, and 43 for month-end. That is evidence of lower turnover, but it is not a guarantee of better results. Weekly had lower turnover than daily and still had a deeper max drawdown in this run.

How is look-ahead bias avoided?

The code separates signal and position. A signal checked on day t is not used for the return ending on day t. The modeled position is always signal.shift(1), so returns use only prior-day or older information.

More notes