Quant from Scratch by QuantWright

Backtesting Essentials · Lesson 4

Data quality

Four errors hiding in a price file, how to find each, and which moves a result most.

Know the basics? Read the lesson notes

Ten years, as delivered

This lesson runs on SimMarket, the course's simulated market: made-up prices, no real market data. Here its Market 1 stands for the shares of one made-up company, over ten years (2,607 trading days). Each day has an open, its first price, and a close, its last.

Below is Market 1 as a careless data source might deliver it: raw data, the prices exactly as recorded, nothing corrected. A backtest replays a rule on past prices. The simplest rule is to hold: buy $10,000 of shares at the first day's open and keep them. Press Hold.

Holding Market 1 for ten years, raw data (simulated)

The account: what the shares are worth at each day's close. Dashed: the $10,000 start.

Bought at–
Last close–
Account$10,000

The backtest says holding lost money: $10,000 became $7,707, since 10,000 × 188.52 ÷ 244.60 = 7,707.

The file holds four errors. The next screens find them one at a time.

A split is not a crash

Tap the cliff in the line to look closer.

Holding Market 1, raw data or split-adjusted (simulated)

The account at each day's close. Orange: adjusted. Grey: as delivered. Dashed: the $10,000 start.

Bought at–
Account–

Adjusted, day 1,501 is an ordinary day: 211.54 ÷ (425.31 ÷ 2) − 1 = −0.52%. The first open becomes 244.60 ÷ 2 = 122.30, and holding turns $10,000 into 10,000 × 188.52 ÷ 122.30 = $15,415.

Dividends: the cash the prices forget

A cash dividend is money a company pays to whoever owns its shares. This one pays every 63 trading days (about every three months), 0.5% of the share price: 41 times in ten years. On the ex-dividend day, the first day a buyer no longer gets that payment, the price drops by about the dividend (that cash has left the company), on top of that day's ordinary market move, but the owner has the cash. The raw file holds prices only, so a backtest sees each drop and never the cash.

The fix is to count the total return: the price change plus the dividend. Credited means the cash buys more shares (it is reinvested). Switch dividends on.

Holding Market 1, split-adjusted, with and without dividends (simulated)

Coloured: the setting you picked. Grey: the other one. Dashed: the $10,000 start.

Dividends
Bought at–
Account–

The first ex-dividend day was day 41. The close fell from $118.75 to $111.21, and the dividend was $0.59. Price only, the day moved (111.21 − 118.75) ÷ 118.75 = −6.35%; with the dividend, (111.21 − 118.75 + 0.59) ÷ 118.75 = −5.85%.

Many data sources credit dividends by adjusting prices: each price before an ex-dividend day is multiplied by (1 − the dividend ÷ the close before the ex-day), here 1 − 0.59 ÷ 118.75 = 0.995 for the first. Why this stands in for the cash: reinvesting the 0.59 buys about 0.5% more shares, and making every earlier price 0.5% lower lifts your return by about the same amount, with no shares to count. The 41 factors multiplied together come to 0.8151, so the first open becomes 122.30 × 0.8151 = $99.69, and holding ends at 10,000 × 188.52 ÷ 99.69 = $18,911.

"Bought at $99.69" is the first price rewritten in today's terms. You still paid $244.60 a share; only the ratio between prices matters.

A day that isn't there

A gap in data is a stretch with no prices (not the overnight gap between a close and the next open). SimMarket trades every weekday, with no holidays. Here are eleven rows around day 900, as the file has them, each with its close. Tap the first day after the hole.

Market 1's rows around day 900 (simulated)

Each tile is one row of the file: the weekday and its close.

Monday, day 901, is missing: the file jumps from Friday to Tuesday.

Most backtests read the file row by row (one row per day) without checking the calendar. So Tuesday's row shows the move since Friday's close, 144.56 ÷ 147.22 − 1 = −1.81%: two days of moves, read as one. A rule that counts rows ("hold 20 days") holds for 21 days when it spans the hole. Real markets do close on holidays, so check the exchange's calendar before calling a missing day an error.

A price that never was

A bad print is a price recorded wrong: a typing slip, a misplaced decimal point, a stray trade. The chart shows each day's move from one close to the next, with the split and dividends already fixed. Tap the move that looks wrong.

Market 1's daily moves, close to close, ten years (simulated)

One thin bar per day: up above the line, down below it. Orange band, once drawn: 20 times the usual size of a day's move (the middle value, or median, of the last 20 sizes), above and below, so it widens after wild days and narrows after calm ones; cut off at the chart's edge, ±25%.

Days checked–
Flagged–

Day 1,201's close reads $218.18 (prices here have the split and dividends already fixed, so they differ from the raw file). The true close was $181.82, between $181.01 the day before and $184.08 the day after. A price that jumps and comes straight back is the mark of a bad print.

The band compares each day's move with the usual size of the 20 moves before it (their median; a later screen shows how it is found), and flags any move more than 20 times that size. 20 clears every real day here: the biggest was 14.6 times its usual size. 2,585 days checked (the first 20 have nothing before them). It flagged two days: the bad print, and the day after, when the price comes back.

Which error costs more?

Now a rule that reacts to prices. The 10% exit rule: hold the shares, but when a day closes 10% or more below the day before, sell everything at the next open, and buy back at the open 20 rows later. On clean data no day falls 10%, so the rule just holds and ends at $18,911, like holding.

On the raw data, which error costs this rule most over ten years?

Keep your guess; next, you fix each error yourself.

Fix them one at a time

The raw data again. Switch each fix on, in any order, and watch both results. Filled and Corrected take that day's true prices from a second source (here, SimMarket's clean data).

Two rules on Market 1, with the fixes you choose (simulated)

Coloured: the 10% exit rule. Grey, thicker, underneath: holding; where you see one line, the two are the same. Dashed: the $10,000 start.

Split
Dividends
Missing day
Bad print
Holding–
Exit rule–
Exits fired–

Each error alone, with the other three fixed, against the exit rule's $18,911 on clean data:

Error left inExit rule ends atCost

The missing day cost nothing here because the exit rule never fired near it. The split costs most: a fake crash in the account, then a false exit. The missing dividends, 0.5% at a time, cost about twice as much as the bad print, 20% at once. The bad print's cost is also luck: its false exit sat out a rise. Holding never reacts to one day's move, so the bad print cost it nothing.

Theory

Adjusting prices for splits and dividends

Raw prices change units at a split and drop at each dividend. Adjusted prices rewrite the earlier prices so that every day's move is the real one. For a split, split adjustment divides every price before it by the split ratio. For a dividend, the day's total return counts the cash with the price change; the dividend factor F does the same by rewriting earlier prices: multiply every price before the ex-dividend day by F. The page's figures use the factor; with several dividends, multiply their factors together.

Padj = P ÷ s
r = (Pt − Pt−1 + D) ÷ Pt−1
F = 1 − D ÷ Pt−1
P
a price from before the split, as recorded
s
the split ratio: new shares per old share (a 4-for-1 split means s = 4; a reverse split, which merges shares, has s below 1)
Padj
that price in today's shares
Pt, Pt−1
day t's close and the previous day's close
D
the dividend per share, on its ex-dividend day (0 on other days)
r
the day's total return
F
the dividend factor, a little below 1
Worked example: your data

Split: day 1,500's close becomes 425.31 ÷ 2 = 212.655, so day 1,501 moved 211.54 ÷ 212.655 − 1 = −0.52%, not −50.26%.

Dividend: on day 41, r = (111.21 − 118.75 + 0.59) ÷ 118.75 = −5.85%, against −6.35% from prices alone. With the factor, F = 1 − 0.59 ÷ 118.75 = 0.995, so the earlier close becomes 118.75 × 0.995 = 118.16 and the day moved 111.21 ÷ 118.16 − 1 = −5.88%. The page's figures use this; the formula for r is a close approximation (−5.85% against −5.88%).

Holds when
  • the split ratios, dividend amounts and ex-dividend dates come from a reliable record;
  • the dividend is cash (shares handed out instead need their own adjustment).
Breaks when

A rule reads price levels ("buy under $150"): adjusted prices before a split are prices nobody traded at. Or the record is wrong or missing (lesson D6, Cleaning and adjusting data).

Try it on new numbers. A share closes at $80.00. The next day is its ex-dividend day: it closes at $79.70 and pays a dividend of $0.60. What is that day's total return, in %?

r = (79.70 − 80.00 + 0.60) ÷ 80.00 = 0.30 ÷ 80.00 = 0.375%. From prices alone it would read −0.375%.
Same working: 0.30 ÷ 80.00 = 0.375%. The price fell 0.30, but the owner got 0.60 in cash.
Theory

Flagging a bad print: the usual range

A bad print is an outlier: a value far outside the usual range. To say what is usual, use the median: sort the numbers and take the middle one (with an even count, halfway between the middle two). One huge error drags an average a long way but barely moves a median, so the median is called robust. The check:

flag day t if |rt| > k × mt
rt
day t's move from the close before, in %
|rt|
its size, with any minus sign dropped
mt
the usual size: the median of the sizes of the 20 moves before day t
k
how many times the usual size counts as suspicious (here 20, which clears the biggest real day, 14.6 times its usual size)
Worked example: day 1,201

The 20 moves before it, in %: 0.25, −0.19, 0.68, −0.99, 0.49, −0.58, −0.35, 1.00, 0.85, 0.51, 0.40, 0.55, 0.09, −0.76, −0.18, −0.08, 0.25, 0.33, −0.03, 0.40. Sorted by size, the 10th and 11th are both 0.40, so m = 0.40% and the line is 20 × 0.40% = 8.0%. Day 1,201 moved +20.53%: flagged.

Next day, the window drops the first 0.25 and takes in the 20.53. The median only moves to (0.40 + 0.49) ÷ 2 = 0.445%, so the line is 8.9% and the −15.63% is flagged too. Their average would jump from 0.448% to 1.462%, putting the line at 29.24% and letting the −15.63% through.

Holds when
  • errors are rare, so most of the 20 days in each window are normal;
  • an error is big next to a normal day's move.
Breaks when

A small error (2% off) hides among normal moves. A real crash is flagged too, so a flag is a question for a second data source, not a verdict (lesson A6, Fat tails). A missing day is no move at all: only the calendar finds it, by comparing the file's dates with the trading calendar (weekdays minus holidays).

Try it on new numbers, with a five-day window. The last five moves had sizes 0.3%, 0.5%, 0.4%, 0.9% and 0.6%. With k = 20, above what size is the next move flagged, in %?

Sorted: 0.3, 0.4, 0.5, 0.6, 0.9. The middle one is 0.5%, so the line is 20 × 0.5% = 10%.
The middle one is 0.5%, so the line is 20 × 0.5% = 10%. (Their average, 0.54%, would give 10.8%.)

What this means for your backtest

Four checks before you trust a result. Turn them over one at a time.

  1. Read returns from adjusted prices.

    Split-adjusted, with dividends credited, unless the rule needs the price as traded.

Practice 1 of 3

A Friday that vanished

The situationYour daily data jumps from Thursday to Monday; that Friday was not a holiday. Your rule sells when a day closes 3% or more below the day before. Monday's row is 3.4% below Thursday's close, so the rule sells.

What should you do?

Monday's row holds two days of moves. Split into Friday and Monday, neither need fall 3%, and the sale may never happen.
This row holds Friday's and Monday's moves together, so the sale may be a false signal.
Deleting Monday makes a bigger hole: Tuesday's row would hold three days of moves.
Practice 2 of 3

A 75% crash

The situationA share closes at $412.00. Overnight it splits 4-for-1, and the next day it closes at $101.50. Your raw data shows a 75% crash.

What was the day's real move, in %?

Hint: first put the earlier close in today's shares (divide by the split ratio), then compare.

A 4-for-1 split means s = 4: 412.00 ÷ 4 = 103.00, and 101.50 ÷ 103.00 − 1 = −1.46%: a small down day.
101.50 ÷ 103.00 − 1 = −1.46%, after 412.00 ÷ 4 = 103.00. Don't forget the minus sign.
Practice 3 of 3

Three flags

The situationA usual-range check on your data has a usual size of 0.5% and k = 20, so its line is about 10%. It flags three days: +18%; −15.3% the next day (which undoes it: 1.18 × 0.847 = 1.00); and weeks later −11%, after which the price stays down.

What next?

The −11% that stays down looks real, and deleting real crashes makes a rule look safer than it is.
The +18% that comes straight back looks like a bad print (the −15.3% is its undo). Check them before you keep them.
A move that jumps and comes straight back is the mark of a bad print, and one wrong close flags two days, as here. The −11% that stays looks real. A second data source settles all three.
✓

Lesson done

Raw price data has traps. An unadjusted split looks like a crash, missing dividends make every holding return too low, a missing day turns two days into one row, and a bad print triggers trades on a price nobody traded. Which error matters most depends on the rule: here a quiet 0.5%, 41 times, cost the exit rule more than one close 20% too high, whose cost was partly luck.

Key ideas

Raw data
prices exactly as recorded, nothing corrected
Stock split
each share becomes s shares, each worth 1/s; nothing is lost
Split adjustment
divide every price before the split by the split ratio s
Ex-dividend day
the first day a buyer no longer gets the dividend; the price drops by about the dividend
Total return
the price change plus any dividend
Gap (missing day)
a stretch with no prices; the next row holds the moves of several days
Bad print
a price recorded wrong; it usually jumps and comes straight back
Median, robust
the middle value once sorted; robust because one huge error barely moves it
Outlier
a value far outside the usual range; a question for a second source

Formulas

Padj = P ÷ s  ·  r = (Pt − Pt−1 + D) ÷ Pt−1  ·  F = 1 − D ÷ Pt−1 (multiply every earlier price by F)  ·  flag if |rt| > k × median of the last 20 sizes

Lesson notes

A price file can be wrong in ways that look like real market events: a stock split, dividends left out, a missing day, a price recorded wrong. Each can change a result by more than the rule does.

Raw data can lose money on paper

The lesson uses SimMarket, the course's simulated market, where Market 1 is one made-up company's shares over 2,607 trading days (day numbers count trading days from 1, so a missing day keeps its number). Each day has an open, its first price, and a close, its last. Raw data is prices as recorded, nothing corrected. A backtest replays a rule on past prices; the simplest, holding, buys $10,000 of shares at the first open and keeps them. On the raw file it ends at $7,707.28 (10,000 × 188.52 ÷ 244.60). Corrected, it ends at $18,910.62. The file has one error of each kind.

A split is not a crash

A stock split gives each owner more shares, each worth less; nobody loses anything. On day 1,501 the company made a 2-for-1 split (each share became two): 100 shares at $425.31 became 200 at about $212.66. The raw file keeps the old prices, so a backtest sees −50.26% in one day. Split adjustment divides every earlier price by the ratio, here 2. Adjusted, that day moved −0.52%, the first open becomes 244.60 ÷ 2 = 122.30, and holding ends at 10,000 × 188.52 ÷ 122.30 = $15,414.55.

Dividends: the cash the prices forget

A cash dividend is money a company pays its shareholders; this one pays 0.5% of the share price every 63 trading days, 41 times. On the ex-dividend day, the first day a buyer no longer gets the payment, the price drops by about the dividend, but the owner has the cash. A raw file holds prices only, so a backtest sees every drop and never the cash; the total return adds it back. On the first ex-dividend day, day 41, the close fell from $118.75 to $111.21, which is −6.35% on price alone, and −5.85% with the $0.59 dividend.

Many sources instead multiply each earlier price by 1 − (the dividend ÷ the close before the ex-dividend day), here 0.995 for the first. Each dividend is rounded to the cent, so the 41 factors differ a little; their product is 0.8151 (0.995 multiplied 41 times would give 0.8142), so the first open becomes 122.30 × 0.8151 = $99.69 and holding ends at $18,910.62.

A day that isn't there

A gap is a stretch of days with no prices. SimMarket trades every weekday, so day 901, a Monday, is missing: the file jumps from Friday to Tuesday. Backtests read one row per day, so Tuesday's row shows the move since Friday's close, 144.56 ÷ 147.22 − 1 = −1.81%: two days of moves read as one. A rule that counts rows ("hold 20 days") then holds for 21. Deleting the next row as well only makes a bigger hole. Real markets close on holidays, so check the calendar first.

A price that never was

A bad print is a price recorded wrong, such as a typing slip. Day 1,201's close reads $218.18; the true close was $181.82, between $181.01 the day before and $184.08 the day after. A price that jumps and comes straight back is the mark of a bad print.

To find one, flag any day whose move is over 20 times the usual size, the median (middle value once sorted) of the 20 moves before it. Day 1,201 moved +20.53% and the next day, as the price came back, −15.63%; both were flagged. The file has 2,606 rows, so 2,605 moves; the first 20 lack 20 moves before them, leaving 2,585 checked. 20 clears every real day: the biggest was 14.6 times its usual size. A flag is a question for a second source, not a verdict: a real crash is flagged too.

Which error costs more?

The 10% exit rule holds, but when a day closes 10% or more below the day before it sells at the next open and buys back at the open 20 rows later. On corrected data it never fires and ends at $18,910.62. With one error left in, in the lesson's simulation, it ends at:

The split costs most: a fake crash, then a false exit. The dividends, 0.5% at a time, cost about twice the bad print's 20% at once (3,496.07 ÷ 1,767.96 ≈ 2); the bad print's cost is partly luck, since its false exit sat out a rise. Holding never reacts to one day's move, so the bad print cost it nothing. The ranking holds for these two rules on this market only. The fixes used SimMarket's clean prices as a stand-in for a second source.

Formula: adjusting prices for splits and dividends

Padj = P ÷ s
r = (Pt − Pt−1 + D) ÷ Pt−1
F = 1 − D ÷ Pt−1
P
a price from before the split, as recorded
s
the split ratio: new shares per old share (a 4-for-1 split means s = 4; a reverse split, which merges shares, has s below 1)
Padj
that price in today's shares
Pt, Pt−1
day t's close and the previous day's close
D
the dividend per share, on its ex-dividend day (0 on other days)
r
the day's total return
F
the dividend factor, a little below 1: multiply every price before the ex-dividend day by it
Worked example: the lesson's data

Split: day 1,500's close becomes 425.31 ÷ 2 = 212.655, so day 1,501 moved 211.54 ÷ 212.655 − 1 = −0.52%, not −50.26%.

Dividend: on day 41, r = (111.21 − 118.75 + 0.59) ÷ 118.75 = −5.85%, against −6.35% from prices alone. With the factor, F = 1 − 0.59 ÷ 118.75 = 0.995, so the earlier close becomes 118.75 × 0.995 = 118.16 and the day moved 111.21 ÷ 118.16 − 1 = −5.88%. The formula for r (the cash added back to the day's return) is a close approximation of the factor method (earlier prices rewritten): −5.85% against −5.88%. The lesson's figures use the factor; with several dividends, multiply their factors together.

Holds whenBreaks when

A rule reads price levels ("buy under $150"): adjusted prices before a split are prices nobody traded at. Or the record is wrong or missing (lesson D6, Cleaning and adjusting data).

Formula: flagging a bad print

flag day t if |rt| > k × mt
rt
day t's move from the close before, in %
|rt|
its size, with any minus sign dropped
mt
the usual size: the median of the sizes of the 20 moves before day t
k
how many times the usual size counts as suspicious (here 20, which clears the biggest real day, 14.6 times its usual size)
Worked example: day 1,201

The 20 moves before it, in %: 0.25, −0.19, 0.68, −0.99, 0.49, −0.58, −0.35, 1.00, 0.85, 0.51, 0.40, 0.55, 0.09, −0.76, −0.18, −0.08, 0.25, 0.33, −0.03, 0.40. Sorted by size, the 10th and 11th are both 0.40, so m = 0.40% and the line is 20 × 0.40% = 8.0%. Day 1,201 moved +20.53%: flagged.

Next day, the window drops the first 0.25 and takes in the 20.53. The median only moves to (0.40 + 0.49) ÷ 2 = 0.445%, so the line is 8.9% and the −15.63% is flagged too. Their average would jump from 0.448% to 1.462%, putting the line at 29.24% and letting the −15.63% through.

Holds whenBreaks when

A small error (2% off) hides among normal moves. A real crash is flagged too, so a flag is a question for a second data source, not a verdict (lesson A6, Fat tails). A missing day is no move at all: only the calendar finds it, by comparing the file's dates with the trading calendar (weekdays minus holidays).

One more worked example

Made-up round numbers

A split. A share closes at $90.00, splits 3-for-1 overnight, and closes at $29.70. In today's shares the earlier close is 90.00 ÷ 3 = 30.00, so the real move is 29.70 ÷ 30.00 − 1 = −1.00%.

A dividend. A share closes at $50.00, then on its ex-dividend day at $49.50, paying $1.00. Price alone: 49.50 ÷ 50.00 − 1 = −1.00%. Total return: (49.50 − 50.00 + 1.00) ÷ 50.00 = 0.50 ÷ 50.00 = +1.00%.

Common mistakes

Check yourself

  1. A share closes at $200.00. Overnight it splits 5-for-1, and the next day it closes at $39.20. What was the day's real move?
    Answer

    Earlier close in today's shares: 200.00 ÷ 5 = 40.00. Real move: 39.20 ÷ 40.00 − 1 = 0.98 − 1 = −2.00%. The raw file would show 39.20 ÷ 200.00 − 1 = −80.4%.

  2. A share closes at $40.00. The next day is its ex-dividend day: it closes at $39.60 and pays $0.80. What is that day's total return?
    Answer

    Total return = (39.60 − 40.00 + 0.80) ÷ 40.00 = 0.40 ÷ 40.00 = +1.00%. On prices alone it would read −1.00%.

  3. The last five moves had sizes 0.2%, 0.6%, 0.3%, 0.9% and 0.4%, and the multiplier is 20 (the lesson's check uses 20 moves; this question uses 5 to keep it short). Above what size is the next move flagged? Is a 9% move flagged?
    Answer

    Sorted: 0.2, 0.3, 0.4, 0.6, 0.9. The middle one is 0.4%, so the line is 20 × 0.4% = 8.0%. A 9% move is flagged, as a question for a second source. (The average, 0.48%, would give a line of 9.6% and let it through.)

  4. Your daily data jumps from Thursday to Monday, and that Friday was not a holiday. Your rule sells when a day closes 3% or more below the day before, and Monday's row is 3.4% below Thursday's close. What should you do?
    Answer

    Get Friday's prices from a second source and run it again. Monday's row holds two days of moves, so split into Friday and Monday neither need fall 3%, and the sale may never happen. Deleting Monday's row as well only makes a bigger hole: Tuesday's row would hold three days of moves.

  5. Why did the missing dividends cost the exit rule more than the bad print, when the bad print was a 20% error?
    Answer

    The exit rule never fired on the dividends' error, so it ended where holding did, $3,496.07 below the corrected $18,910.62: the 0.5% missing 41 times. The bad print's cost was $1,767.96, about half as much, and partly luck, because its false exit happened to sit out a rise. The ranking holds for these two rules on this market, not in general.

Try it yourself

Run the script. Every number in this lesson comes from one Python script (below). It imports SimMarket (course/sim/simmarket.py, seed 20261109), whose switches split, dividends, missing_day and bad_print put each error into Market 1. Switch them on in other mixes, change the exit rule's 10% or the check's 20 times, and see what moves. The page rebuilds all 16 mixes from the same prices and checks its numbers against the script's.

Check a file by hand. In a spreadsheet with one row per day: put each day's move in a column (close ÷ previous close − 1); next to it, the median of the 20 sizes above it (=MEDIAN(ABS(…)) over the 20 rows above); flag rows whose size is more than 20 times that. Add a column with each row's weekday and look for jumps. Look up every flag in a second source.

About QuantWright

QuantWright, which publishes this course, sells backtesting software. Its US stock and ETF data is daily bars, split-adjusted, with cash dividends credited on each ex-date (total returns), and every result names any stretch with no bars. Neither replaces checking data you bring yourself. quantwright.com

The script needs simmarket.py, the course's simulated market, in the same folder.

The code behind every number (Python, SimMarket seed 20261109)
Loading…

Going further: adjusted prices change after the fact

Each new dividend or split lowers every earlier adjusted price, so an adjusted price file downloaded today has different numbers from the same file downloaded last year. The moves (the returns) stay the same; the price levels don't. Two consequences. A rule that reads levels ("buy under $50") gives different answers depending on the download date. And mixing adjusted prices from one source with raw prices from another puts a false jump wherever they meet. Store the raw prices with their split and dividend records, and adjust them yourself when you need returns.

For the preciseDividend adjustment. SimMarket's clean series multiplies every price before an ex-dividend day by 1 − D ÷ Pt−1, with Pt−1 the raw close before the ex-day, the common convention for adjusted prices. A day's move on that series equals the total-return formula only to a close approximation: on day 41 it is −5.88% against the formula's −5.85%. The two agree exactly when the price drops by exactly the dividend. The usual-range check. On the page's data (split and dividends fixed, missing day and bad print in), 2,585 days are checked (the first 21 rows have no full window). With unrounded moves, day 1,201's usual size is 0.403% and its move 50.9 times that; day 1,202's 0.447% and 35.0 times. The largest real move is 14.6 times its usual size. With k = 10, six real days would be flagged as well. k = 20 was set after looking at these numbers. A textbook version, the Hampel filter, uses the median absolute deviation of a centred window and a smaller k (often 3 × 1.4826); daily returns have fat tails and calm and wild spells, so a fixed small k flags many real days.
For the precise: every numberMarket 1 (SimMarket seed 20261109, planted effect at its default), 2,607 trading days, day numbers counted from 1 on the page, also after the missing day removes a row. Hooks: 2-for-1 split before day 1,501; 41 cash dividends of 0.5% of the previous raw close, from day 41 every 63 days; day 901 (a Monday) missing; day 1,201's close 20% too high. Both rules buy $10,000 at the first open (fractional shares, no costs) and value the shares at each close. The exit rule: on a close at or below 90% of the previous row's close (exact in cents), sell at the next row's open and buy back at the open 20 rows later. Holding ends at $7,707.28 (raw), $15,414.55 (split fixed), $9,454.84 (dividends fixed), $18,910.62 (both); the missing day and the bad print don't change it. The exit rule: clean $18,910.62; raw $7,081.04 (exits at days 1,202 and 1,501); split alone $9,583.42; dividends alone $15,414.55; bad print alone $17,142.66; missing day alone $18,910.62. Costs on the page are differences of the dollar-rounded ends.

Honest notes

Where this comes from