Case studies

All cases
Cases · Backtest · US stock market, daily, 1926–2026

A century of the 200-day rule: where the edge came from

Stay in the stock market while it is above its 200-day average, move to Treasury bills when it falls below: one of the oldest trend-following rules. Does it beat simply holding the market — and when?

DataFama/French research factors, daily — The daily return of the whole US stock market (all NYSE, AMEX and NASDAQ stocks, weighted by value, from CRSP) in excess of the one-month Treasury bill, and the T-bill return itself, since July 1926.
SourceKenneth R. French Data Library, Tuck School of Business
LicenceFree to download for research and teaching; not redistributed here — the code below fetches it from the library, and this page shows only results computed from it
Get itThe code in step 1 downloads the file (≈0.2 MB) directly. The page has no Run buttons, because a browser page cannot fetch from that site; use the notebook below.

01A hundred years of daily returns

Load the file, turn percentages into fractions, and add the T-bill rate back to get the market’s total return.

import numpy as np
import pandas as pd

url = ('https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/ftp/'
       'F-F_Research_Data_Factors_daily_CSV.zip')
ff = pd.read_csv(url, skiprows=4, index_col=0)          # pandas unzips it
ff = ff[ff.index.astype(str).str.strip().str.fullmatch(r'\d{8}')]   # drop the copyright line
ff.index = pd.to_datetime(ff.index.astype(str).str.strip(), format='%Y%m%d')
ff = ff.astype(float) / 100
r = ff['Mkt-RF'] + ff['RF']          # the market's daily total return
rf = ff['RF']                        # the T-bill's
print(len(r), 'days,', r.index[0].date(), 'to', r.index[-1].date())
print('five worst days:', {d.date().isoformat(): round(v, 3) for d, v in r.nsmallest(5).items()})
26317 days, 1926-07-01 to 2026-08-31
five worst days: {'1987-10-19': -0.174, '2020-03-16': -0.12, '1929-10-29': -0.115, '1929-10-28': -0.112, '1929-11-06': -0.096}

The worst day, 19 October 1987, lost 17.4% — 16 times the standard deviation of a day’s return. Three of the five worst came in the crash of autumn 1929, one in March 2020.

02The rule, without peeking

Build the market’s value from its returns and its 200-day average. The decision for each day is taken from the previous day’s close — the shift(1) — because that is all an investor would have known. Out of the market, the money earns the T-bill rate. Each switch costs 0.1%.

price = (1 + r).cumprod()
above = (price > price.rolling(200).mean()).shift(1)   # yesterday's close vs its average
start = price.rolling(200).mean().first_valid_index() + pd.Timedelta(days=1)
r, rf, above = r[start:], rf[start:], above[start:].astype(bool)
switch = above != above.shift(1, fill_value=True)
rule = pd.Series(np.where(above, r, rf), index=r.index) - 0.001 * switch

def summary(x, name):
    years = len(x) / 252
    growth = (1 + x).prod() ** (1 / years) - 1
    wealth = (1 + x).cumprod()
    drawdown = (wealth / wealth.cummax() - 1).min()
    sharpe = (x - rf.loc[x.index]).mean() / x.std() * np.sqrt(252)
    print(f'{name:16} {growth:6.2%} a year | volatility {x.std() * np.sqrt(252):6.2%} | '
          f'worst drawdown {drawdown:7.1%} | Sharpe {sharpe:.2f}')
    return growth, drawdown

hold_all = summary(r, 'buy and hold')
rule_all = summary(rule, '200-day rule')
print('switches per year:', round(switch.sum() / (len(r) / 252), 1))
buy and hold      9.78% a year | volatility 17.15% | worst drawdown  -84.1% | Sharpe 0.45
200-day rule     10.46% a year | volatility 11.25% | worst drawdown  -34.0% | Sharpe 0.67
switches per year: 5.4

Over the whole period the rule wins on every line: 10.5% a year against 9.8% for buy-and-hold, at two thirds of the volatility, and a worst drawdown of 34% instead of 84%. That is the headline result.

03Split the century

The same comparison, era by era.

eras = {'1927-1975': ('1927', '1975'),
        '1976-2000': ('1976', '2000'),
        '2001-2026': ('2001', '2026')}
by_era = {}
for era, (a, b) in eras.items():
    print(era)
    by_era[era] = (summary(r[a:b], '  buy and hold'), summary(rule[a:b], '  200-day rule'))
1927-1975
  buy and hold    7.41% a year | volatility 17.24% | worst drawdown  -84.1% | Sharpe 0.39
  200-day rule   10.51% a year | volatility 11.12% | worst drawdown  -34.0% | Sharpe 0.78
1976-2000
  buy and hold   15.30% a year | volatility 14.38% | worst drawdown  -33.1% | Sharpe 0.61
  200-day rule   13.85% a year | volatility 11.42% | worst drawdown  -23.9% | Sharpe 0.62
2001-2026
  buy and hold    9.47% a year | volatility 19.31% | worst drawdown  -54.6% | Sharpe 0.47
  200-day rule    7.13% a year | volatility 11.38% | worst drawdown  -22.9% | Sharpe 0.51

The whole-sample edge comes from 1927–1975, when the rule sidestepped most of the 1929–32 crash: 10.5% a year against 7.4%. Since 1976 it has earned less than simply holding the market — 13.8% against 15.3%, then 7.1% against 9.5% — while still cutting the worst drawdown, from 33% to 24% and from 55% to 23%. In the recent eras it behaved like insurance, not like an engine of return. Sullivan, Timmermann and White found the same pattern on the Dow Jones index: the best rules over 1897–1986 survived a correction for data snooping, but their performance did not repeat in the decade that followed.

buy and hold200-day rule1927-19757.4%10.5%1976-200015.3%13.8%2001-20269.5%7.1%
Compound annual return, with 0.1% per switch for the rule. Daily data, US market (CRSP value-weighted).

04The bug that nearly doubles the result

Remove the shift(1): decide each day using that same day’s close — which is only known once the day’s return has already happened.

peek = (price > price.rolling(200).mean())[start:]
cheat = pd.Series(np.where(peek, r, rf), index=r.index)
cheat -= 0.001 * (peek != peek.shift(1, fill_value=True))
cheat_all = summary(cheat, 'with look-ahead')
with look-ahead  18.69% a year | volatility 11.33% | worst drawdown  -25.0% | Sharpe 1.30

One missing shift, and the rule earns 18.7% a year — 1.8 times the honest 10.5%. A day’s own return is what pushes the price across its average, so the bugged rule is in the market for the rise that crosses it upward and out of it for the fall that crosses it downward. Bugs in backtests are rarely this visible; most leak a little information and inflate a little, which is why an implausibly good backtest should be checked line by line before anything else.

$1$100$10,000$1,000,000$100,000,00019301950197019902010buy and hold200-day rulewith look-ahead
Value of $1 invested in May 1927, year-end values, log scale. The look-ahead line is the bug, not a strategy.

What this case shows

The patterns behind it

Run it yourself

Sources

Last reviewed: 2 October 2026 · How this site checks its content · Report a mistake