Lesson 2 · 25 min

Market data fundamentals

Prices, bars, adjusted data, and the panels your strategies consume every day.

OHLCVAdjusted closePanelsBias

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.

LayerWhat you getIn Quant Buffet
Exchange / vendorTick-by-tick trades, quotesNot used in lab
Yahoo Finance (yfinance)Daily OHLCVDownloaded via load_daily_prices
Quant Buffet cacheCSV per symbol + date rangebacktest/data_cache/*.csv
Your strategypd.DataFrame panelColumns = ASSETS, index = dates
IndicatorsRolling SMA, returns, z-scoresComputed inside make_on_day

Core vocabulary

TermMeaningExample in lab
Ticker / symbolInstrument codeSPY, TLT, BTC-USD
OHLCV barOpen, High, Low, Close, Volume for one periodOne row per trading day
Adjusted closeClose corrected for splits & dividendsDefault in load_daily_prices
PanelTable: dates × symbolsprices DataFrame in make_on_day
LookbackHistory window for an indicator200-day SMA needs 200 rows
readyFirst date a signal is trustworthyReturned 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:

DateSPYTLTGLDXLC
2015-10-05185.42112.08108.31NaN
2015-10-06184.90112.55109.02NaN
2018-06-19268.41108.77116.9448.30
2018-06-20269.03108.32117.1048.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

python
# 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 None

The 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:

python
# 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_std

The 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.

ETFWhat happenedLesson for your backtest
USOIn 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.
UNGPersistent 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.
SVXYOn 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

SymbolData effectively beginsConsequence
SPY1993Safe anchor for long tests
XLREOctober 2015Real-estate sector tests are short
XLCJune 2018Cannot appear in a 2008 stress test
BTC-USD≈ September 2014No crypto strategy spans the GFC
ETH-USD≈ November 2017Only 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 ready when you add a recently launched ETF.