Lesson 2 · 25 min
Market data fundamentals
Prices, bars, adjusted data, and the panels your strategies consume every day.
Every strategy starts with market data — a time series of prices. Quant Buffet's lab uses daily adjusted close for liquid ETFs, loaded through load_daily_prices() and cached on disk so re-runs are fast and reproducible.
| Layer | What you get | In Quant Buffet |
|---|---|---|
| Exchange / vendor | Tick-by-tick trades, quotes | Not used in lab |
| Yahoo Finance (yfinance) | Daily OHLCV | Downloaded via load_daily_prices |
| Quant Buffet cache | CSV per symbol + date range | backtest/data_cache/*.csv |
| Your strategy | pd.DataFrame panel | Columns = ASSETS, index = dates |
| Indicators | Rolling SMA, returns, z-scores | Computed inside make_on_day |
Core vocabulary
| Term | Meaning | Example in lab |
|---|---|---|
| Ticker / symbol | Instrument code | SPY, TLT, BTC-USD |
| OHLCV bar | Open, High, Low, Close, Volume for one period | One row per trading day |
| Adjusted close | Close corrected for splits & dividends | Default in load_daily_prices |
| Panel | Table: dates × symbols | prices DataFrame in make_on_day |
| Lookback | History window for an indicator | 200-day SMA needs 200 rows |
ready | First date a signal is trustworthy | Returned from make_on_day |
What a panel actually looks like
A panel is just a spreadsheet with dates down the side and tickers across the top. Illustrative shape:
| Date | SPY | TLT | GLD | XLC |
|---|---|---|---|---|
| 2015-10-05 | 185.42 | 112.08 | 108.31 | NaN |
| 2015-10-06 | 184.90 | 112.55 | 109.02 | NaN |
| 2018-06-19 | 268.41 | 108.77 | 116.94 | 48.30 |
| 2018-06-20 | 269.03 | 108.32 | 117.10 | 48.55 |
Notice the NaN cells. XLC did not exist before June 2018, so no amount of data cleaning can invent a price for it in 2015. This is not a defect in the data — it is a real constraint on any strategy that wants to trade communication-services stocks over a long history.
Reading a price panel in code
# Inside make_on_day — prices is already loaded for your ASSETS
cols = [c for c in ASSETS if c in prices.columns]
close = prices[cols]
daily_return = close.pct_change() # simple returns
sma_200 = close.rolling(200, min_periods=200).mean() # trend filter
mom_12m = close.pct_change(252) # 12-month momentum
vol_20d = daily_return.rolling(20).std() * (252 ** 0.5) # annualised vol
# Never start trading before every input exists for every asset
ready = close.dropna().index[0] if not close.dropna().empty else NoneThe three biases that ruin beginner backtests
1. Survivorship bias
If you build a universe from *today's* winners, you have quietly deleted every failure. Enron, Lehman Brothers, Washington Mutual, Kodak and Bear Stearns were all respectable S&P 500 members before they were not. A strategy tested only on companies that survived to 2026 inherits a tailwind that no real investor ever received. Broad ETFs like SPY reduce this problem because the index itself replaces failing members — the ETF survives even when its constituents do not.
2. Look-ahead bias
This is the most common beginner error and it is nearly invisible. Any statistic computed over the *whole* sample leaks the future into the past:
# WRONG — full-sample mean includes prices from the future
z = (close - close.mean()) / close.std()
# RIGHT — only data available up to each date
roll_mean = close.rolling(60).mean()
roll_std = close.rolling(60).std()
z = (close - roll_mean) / roll_stdThe wrong version routinely produces Sharpe ratios above 3 and equity curves that look like a straight line. If your first backtest looks that good, suspect leakage before celebrating.
3. Corporate actions and unadjusted prices
Apple split 4-for-1 in August 2020; Nvidia split 10-for-1 in June 2024; Tesla split 3-for-1 in August 2022. On raw price data, each of those looks like a catastrophic overnight crash of 75%, 90%, and 67% — a momentum strategy on unadjusted prices would sell every one of them at exactly the wrong moment. Dividends matter just as much in aggregate: SPY distributes roughly 1.3–2% per year, and ignoring that over a 20-year test is the difference between compounding at about 8% and about 6.5% — on $10,000 that is roughly $46,600 versus $35,200.
Real example: when the price series is not the asset
Two whitelisted commodity ETFs are famous cautionary tales, and both are worth loading in the IDE precisely because they misbehave.
| ETF | What happened | Lesson for your backtest |
|---|---|---|
USO | In April 2020 the front WTI futures contract settled negative (about -$37). USO was forced to restructure its holdings and later did a reverse split. | The fund's mandate can change mid-history; "oil price" and "USO price" are different series. |
UNG | Persistent contango means each futures roll sells cheap and buys expensive, grinding value away regardless of spot gas. | A long-term short-side result here may be structural cost, not a tradable edge. |
SVXY | On 5 Feb 2018 ("volmageddon") it fell roughly 90% in a day; its sister note XIV was terminated outright. | Daily-close backtests cannot see intraday gap risk of this size. |
History start dates you will actually hit
| Symbol | Data effectively begins | Consequence |
|---|---|---|
SPY | 1993 | Safe anchor for long tests |
XLRE | October 2015 | Real-estate sector tests are short |
XLC | June 2018 | Cannot appear in a 2008 stress test |
BTC-USD | ≈ September 2014 | No crypto strategy spans the GFC |
ETH-USD | ≈ November 2017 | Only two full cycles of history |
Before Lesson 3 — you should be able to
- Explain adjusted close and why raw prices break momentum signals.
- Spot look-ahead bias in a line of pandas code.
- Say why an ETF universe reduces but does not eliminate survivorship bias.
- Predict what happens to
readywhen you add a recently launched ETF.