Your Data Is Wrong. How to Fix It.
Most backtests fail because the data is wrong, not because the strategy is wrong. Clean market data still has three traps that break almost everyone.
Most backtests lie. Not because the strategy is wrong, because the data is wrong.
A strategy that genuinely has a Sharpe of 1.5 looks like a Sharpe of 4.0 in a dirty backtest. You ship it. It loses money for six months. You debug for weeks. Eventually you discover that 30% of your trades were on stocks that got delisted in 2008, and your “current constituents” list quietly dropped them, so you never saw the losses. This happens to everyone, and it happens more often than you’d guess.
Three traps account for most dirty-data failures.
The four types of market data
Before cleaning, distinguish the four types of market data:
End-of-day OHLCV. Open, High, Low, Close, Volume. One bar per day. The workhorse for daily-frequency strategies. Easy to get, easy to store, easy to backtest on. Most strategies in this learning path run on EOD data.
Intraday (1-min, 5-min, tick). Higher frequency, larger datasets, more signal but more noise. Free sources are limited. Tick data is gigabytes per day per symbol and you usually pay for it.
Fundamental data. Earnings, revenue, balance sheet items, ratios. Released quarterly. Comes with reporting lag: a Q1 earnings report typically drops in mid-April, not March 31. Forgetting that lag causes lookahead bias (we’ll get to that).
Alternative data. Sentiment scores from news, satellite imagery of parking lots, credit card transactions, web traffic. Increasingly the place where ML-driven alpha lives. Also increasingly expensive and noisy.
For your first 6-12 months in quant, stick to EOD OHLCV. The other categories are real but they’re a distraction from getting the basics right.
Where to get it (and what it costs)
Honest rundown of the major sources:
| Source | Type | Cost | Quality | Watch out for |
|---|---|---|---|---|
| Yahoo Finance | EOD US equities | Free | OK | No delisting returns. Survivorship bias baked in. |
| Alpha Vantage | EOD + intraday US | Free tier, $50/mo premium | OK | 5 calls/min on free tier. Splits adjusted, dividends not always. |
| Polygon.io | EOD + tick US | 200/mo | Good | Better than Yahoo. Still no delisting returns. |
| Quandl (now Nasdaq Data Link) | EOD, fundamentals, some alt | Varies | Good | Premium datasets expensive. |
| CRSP | US equities (academic-grade) | $5k+/year academic | Best | Used in published research. Includes delisting returns. |
| Bloomberg | Everything | $25k+/year | Best | Worth it only if you have institutional access. |
For Arithmion users: arithmion.fetch_ohlcv() proxies through Polygon and applies post-hoc cleaning (delisting returns, corporate action adjustments, exchange code normalization). You don’t need to think about this for the work in this learning path.
If you do it yourself: pair pandas-datareader with CRSP if you can get academic access. Otherwise accept that your data has survivorship bias and size positions accordingly.
Trap #1: Survivorship bias (the #1 backtest lie)
This is the trap that breaks more retail backtests than any other. It deserves its own section.
The problem: You download a list of “S&P 500 constituents today.” You pull 20 years of price history for those 500 stocks. You backtest a strategy. The strategy looks great.
Why it’s wrong: The S&P 500 in 2025 is not the S&P 500 in 2005. Companies get delisted (Lehman Brothers, Enron, Sears, Toys “R” Us, Bed Bath & Beyond, etc.). When you use the current list and pull historical prices, you only get the survivors: the companies that didn’t go bankrupt, get acquired, or fall out of the index.
You never see the trades that would have lost money. Your backtest is a fantasy.
Concrete example: A “strategy” that buys every stock that gets added to the S&P 500 and shorts every stock that gets removed. Looked at naively using the current list, this strategy looks brilliant from 2005-2024: it caught every addition (which mostly went up) and avoided every removal (which mostly went down). Sharpe of 2.5.
Run it with the historical constituent changes (the actual additions and removals as they happened year by year) and the Sharpe drops to 0.4. Some of those removals were Enron, which dropped 99% in a year. You can’t avoid a 99% loss with a stock you’re short if you didn’t know it was getting removed until after the drop started.
The fix: Use a historical constituent database. CRSP does this for US equities. Arithmion’s fetch_ohlcv does this transparently if you pass survivorship_aware=True. Manually, this means tracking index changes over time (most data vendors provide this as a separate file).
This is the #1 backtest lie. If you remember one trap from this article, remember this one.
Trap #2: Lookahead bias
The second most common lie. Easier to fall into than survivorship bias because it can happen from a single misplaced .shift().
The problem: You use data that wasn’t actually available at the time you pretend to use it.
Three common sources:
-
Split adjustments applied backwards. Stock splits are recorded on a specific date. A 2-for-1 split on June 15 means prices before June 15 should be divided by 2 to make them comparable. But if your data vendor applies this adjustment to the raw file and you don’t notice, you might use the adjusted price for a backtest that starts before June 15, and the prices match what you see today, not what a trader in 2019 actually saw. Usually harmless, but a sign of messy data hygiene.
-
Fundamentals with reporting lag. A company’s Q1 earnings (Jan-Mar) are reported in mid-April. If your strategy uses Q1 earnings and you assume they were available on March 31, you’ve looked ahead by ~2 weeks. Most fundamental strategies need a
reporting_lagparameter set to at least 14 days. -
The shift() bug from the previous article. Using
prices.pct_change()instead ofprices.pct_change().shift(-1)to compute forward returns. You compute today’s return using tomorrow’s close. Backtest looks like alpha. Live trading loses money. We covered this in the pandas article.
The fix: For every data field you use, ask: “Could a real trader have known this value on the date I’m using it?” If the answer is no, your backtest is lying.
Arithmion’s SDK prevents this by enforcing date alignment on all factor calculations. You can override it, but you have to be explicit. That’s by design.
Trap #3: Corporate actions
Splits, dividends, mergers, spin-offs, delistings. Get any of these wrong and your price series has phantom jumps that aren’t real returns.
Splits. AAPL did a 4-for-1 split on August 31, 2020. If your data shows AAPL at $499 the day before and $125 the day after, you haven’t earned -75% in a day. You’ve had a 4:1 split. Adjust historical prices by dividing by 4.
Dividends. When AAPL pays a $0.20 dividend, its stock price drops by $0.20 on the ex-date. Total return to shareholders is unchanged (you got the cash), but price return is down $0.20. If you only track prices, your return series is wrong by the dividend amount. For total-return strategies, you must reinvest dividends.
Mergers and acquisitions. When Company A buys Company B for cash, B’s stock jumps to the deal price on the closing date and trading stops. If you don’t have the delisting return, your backtest shows an open trade on a stock that no longer trades.
Spin-offs. A company distributes shares of a subsidiary to existing shareholders. Your position in the parent company now includes shares of the new entity. Track both or your return calculation is incomplete.
Concrete example of corporate-action disaster: You backtest buying stocks on the day they announce a 2-for-1 split, expecting a post-split pop. But your data shows the stock price halving the next day (correct) and never recovering (wrong, because you forgot to apply the split adjustment backwards). Your strategy “loses 50% on every split announcement.” In reality, you’d have made the expected pop.
The fix: Use adjusted prices everywhere. Most vendors do this for you (Yahoo, Polygon, Arithmion). If you build a dataset manually, you need to track corporate actions and apply them yourself. The pandas-datareader + CRSP combination handles this correctly.
What clean data actually looks like
Run this checklist on your data before you backtest:
import pandas as pd
def data_health_check(df):
checks = {}
# Are dates sorted and continuous?
checks["date_range"] = (df.index[-1] - df.index[0]).days
checks["n_rows"] = len(df)
checks["expected_rows"] = len(pd.bdate_range(df.index[0], df.index[-1]))
checks["missing_days"] = checks["expected_rows"] - checks["n_rows"]
# Any zero or negative prices?
checks["zero_prices"] = (df["close"] <= 0).sum()
# Big single-day moves (likely corporate actions or errors)?
returns = df["close"].pct_change()
checks["extreme_moves"] = (returns.abs() > 0.5).sum()
# Suspicious volume patterns?
checks["zero_volume_days"] = (df["volume"] == 0).sum()
return checks
print(data_health_check(data))
PROMPT
Use pandas to write a data health check that flags missing dates, bad prices, extreme moves, and zero-volume bars.
{
"date_range": 1826,
"n_rows": 1305,
"expected_rows": 1305,
"missing_days": 0,
"zero_prices": 0,
"extreme_moves": 0,
"zero_volume_days": 0,
}
Zero missing days, zero extreme moves, zero zero-prices: that’s a healthy dataset. If any of those numbers are non-zero, dig in before you trust the backtest.
Don’t preach about data wrangling. Show the gotcha. Move on.
Most tutorials on data cleaning spend 30 pages on pandas syntax. That’s a waste of your time. The syntax is fine. The mental model is what matters:
- Is my universe survivorship-biased? → Use historical constituents.
- Is my data available when I claim it is? → Audit reporting lags and split adjustments.
- Are corporate actions handled correctly? → Use adjusted prices or track raw + adjust manually.
If yes to all three, you have data you can trust. If no to any of them, your backtest is a story, not evidence. Fix the data first, then worry about the strategy.
The next phase of this learning path covers factors and backtest design, building on the assumption that you now have clean data and a working Python environment.
Read this if you want to go deeper
- López de Prado, M. (2018). Advances in Financial Machine Learning. Wiley. ISBN 978-1119482086. Chapter 7 covers cross-validation in financial data, directly relevant to avoiding lookahead bias; the multiple-testing problem is developed in Chapters 11–12. (Note: Chapter 4 is “Sample Weights,” not cross-validation.)
- Campbell, J., Lo, A., MacKinlay, A. (1997). The Econometrics of Financial Markets. Princeton. ISBN 978-0691043012. Chapter 1 is the canonical reference on the non-random-walk properties of asset prices, with careful attention to data issues.
- Arithmion documentation on data cleaning. arithmion.io/docs/data. Specifically the page on corporate-action adjustment methodology.