Fetch Market Data with yfinance in Python
Before you can write a single backtest, you need data — clean, correctly adjusted, and stored in a format your strategy code can consume. This walkthrough uses yfinance to download ten years of SPY daily OHLCV, explains the difference between adjusted and unadjusted closes (it matters more than most tutorials admit), checks data quality, and saves to CSV. Every printed number is the real output of running the script on 2026-06-05.
"""AlgoDrill — fetch and inspect OHLCV market data with yfinance.
Download SPY daily bars, compare adjusted vs unadjusted closes,
run a data-quality check, and save to CSV. Backtest only — no broker.
Versions: yfinance 1.4.1 · pandas 2.3.3 (run 2026-06-05)
Install: pip install yfinance pandas
"""
import yfinance as yf
import pandas as pd
# 1. Download 10 years of SPY daily OHLCV (fixed range = reproducible)
# auto_adjust=True (default): Close is dividend + split-adjusted
ticker = yf.Ticker("SPY")
adj = ticker.history(start="2015-01-01", end="2024-12-31", auto_adjust=True)
print("=== SPY daily OHLCV — first 3 rows (adjusted closes) ===")
print(adj[["Open", "High", "Low", "Close", "Volume"]].head(3).to_string())
print()
# 2. Adjusted vs unadjusted close — the difference matters for backtesting
# auto_adjust=False returns unadjusted Close + separate Adj Close column
raw = ticker.history(start="2015-01-01", end="2024-12-31", auto_adjust=False)
cmp = pd.DataFrame(
{
"Adj Close": adj["Close"].values[:3].round(4),
"Raw Close": raw["Close"].values[:3].round(4),
},
index=adj.index[:3].date,
)
print("=== Adjusted vs unadjusted close (first 3 rows) ===")
print(cmp.to_string())
print()
# 3. Data quality snapshot
missing = adj["Close"].isna().sum()
print(f"Rows: {len(adj):>6}")
print(f"Missing closes: {missing:>6}")
print(f"Date range: {adj.index[0].date()} to {adj.index[-1].date()}")
# 4. Save to CSV for offline analysis
# (Parquet is faster for large files; needs: pip install pyarrow)
adj.to_csv("/tmp/spy_daily.csv")
print(f"Saved: /tmp/spy_daily.csv ({len(adj)} rows)")Script Output
=== SPY daily OHLCV — first 3 rows (adjusted closes) ===
Open High Low Close Volume
Date
2015-01-02 00:00:00-05:00 170.911759 171.325830 169.089839 170.125015 121465900
2015-01-05 00:00:00-05:00 169.081524 169.247150 166.746174 167.052582 169632600
2015-01-06 00:00:00-05:00 167.359043 167.880776 164.684151 165.479172 209151400
=== Adjusted vs unadjusted close (first 3 rows) ===
Adj Close Raw Close
2015-01-02 170.1250 205.43
2015-01-05 167.0526 201.72
2015-01-06 165.4792 199.82
Rows: 2515
Missing closes: 0
Date range: 2015-01-02 to 2024-12-30
Saved: /tmp/spy_daily.csv (2515 rows)
Line-by-Line Walkthrough
Lines 10–13: Download with auto_adjust=True
yf.Ticker("SPY") creates a Ticker object. .history(start=..., end=..., auto_adjust=True) downloads daily OHLCV and returns a pandas DataFrame with a DatetimeIndex. auto_adjust=True is the default and means the Close column is already dividend- and split-adjusted — the number to use for backtesting. The DataFrame also includes Dividends and Stock Splits columns by default; the script explicitly selects OHLCV to avoid showing those extra columns in the first-3-rows preview.
Lines 21–31: Adjusted vs unadjusted close
On 2015-01-02, SPY's adjusted close was $170.13 while the raw (unadjusted) close was $205.43. That $35 gap represents years of accumulated dividends that have been subtracted backward from historical prices. If you used raw closes to compute a return from 2015 to 2024, a large portion of total return would appear to come from price appreciation when it actually came from dividends paid and reinvested — producing an incorrect picture of the strategy's performance.
The rule: always use adjusted closes for backtesting. The only exception is strategies that specifically model dividend capture or ex-dividend price drops — but even those usually start from adjusted closes and add dividend income separately.
Lines 33–36: Data quality check
The quality check prints three numbers: row count (2,515 trading days over ten years), missing close values (0 for SPY — a major liquid ETF), and the date range (2015-01-02 to 2024-12-30, since the range is exclusive of the end date). For most liquid US equity ETFs, missing values are rare. For individual stocks — especially smaller-cap names or names with trading halts — missing values can appear and must be forward-filled or dropped before backtesting. adj["Close"].isna().sum() is the fast check.
Lines 39–40: Save to CSV
The script saves to CSV via adj.to_csv(). CSV is fine for a single ticker over years of daily data. For multi-ticker or higher-frequency data, Parquet is substantially faster to read and write — it requires pip install pyarrow and then adj.to_parquet("spy_daily.parquet"). Parquet also preserves column dtypes exactly, so you do not need to parse date strings on re-load.
Rate Limit Gotchas
yfinance accesses Yahoo Finance's unofficial endpoints with no published rate limit. In practice, downloading dozens of tickers in a tight loop frequently triggers HTTP 429 (Too Many Requests) or silent data gaps. Three mitigations:
- Batch download:
yf.download(["SPY", "QQQ", "GLD"], start=..., end=...)fetches multiple tickers in a single call, which is more polite than individual loops. - Add sleep:
time.sleep(0.5)between loop iterations is enough to avoid most 429s. - Cache locally: download once to CSV/Parquet, then read the file for subsequent backtests. The data does not change for historical dates.
Survivorship Bias Is Not a yfinance Problem
yfinance returns data for tickers you explicitly request. It does not maintain historical index membership lists. If you backtest a strategy by pulling current S&P 500 constituents, you automatically exclude companies that were removed (bankrupt, delisted, merged) — this is survivorship bias and inflates returns. yfinance cannot solve this; it does not have the delisted-ticker data. For survivorship-bias-free equity universes, see Market Data Providers (Norgate is the only provider here specifically built for this; QuantConnect/LEAN includes historical constituent data in its data library).
Frequently Asked Questions
- Is yfinance free to use?
- yfinance is a free, open-source Python library (MIT license, v1.4.1 as of May 2026). It scrapes Yahoo Finance's public endpoints and requires no API key for personal and research use. Yahoo's Terms of Service restrict commercial redistribution of the data, so for production systems serving third parties or large-scale data collection, a paid data provider (Polygon/Massive, Databento, Tiingo) is appropriate. For backtesting research and personal algorithmic trading on your own account, yfinance is the standard free starting point.
- What is the difference between adjusted and unadjusted close?
- An unadjusted close is the raw market price at the end of the trading day. An adjusted close modifies historical prices backward to account for dividends and stock splits. For example, if SPY paid a dividend of $0.50, the adjusted close for all prior dates is reduced by $0.50 (approximately), so that a return calculation across the ex-dividend date does not show a phantom drop. For backtesting, always use adjusted closes unless your strategy specifically models dividend capture. yfinance's auto_adjust=True (the default in Ticker.history()) returns adjusted closes; auto_adjust=False returns unadjusted closes with a separate Adj Close column.
- What are yfinance's rate limits?
- yfinance does not publish official rate limits, because it accesses Yahoo Finance's unofficial endpoints. In practice, downloading dozens of tickers in a tight loop often triggers HTTP 429 (Too Many Requests) or silent data gaps. Common mitigations: add a time.sleep(0.5) between downloads in a loop, use yf.download() with a list of tickers in a single call (batched internally), or cache downloaded data to disk so re-runs do not re-hit the endpoint. For programmatic bulk downloads, a paid provider with documented rate limits (Polygon/Massive: 5 req/min on free tier; Databento: pay-per-GB) is more reliable.
- Does yfinance have survivorship bias?
- yfinance shows data for tickers you request, but it does not maintain a historical index membership list. If you backtest an S&P 500 strategy by pulling data for the current 500 constituents, you will only see companies that survived to be in the index today — companies that were delisted, merged, or bankrupt are absent. This is survivorship bias and inflates backtest returns. yfinance itself is not the cause; it simply cannot give you historical constituent data it does not have. To avoid survivorship bias, use a data provider with historical index membership data (Norgate, QuantConnect/LEAN data library) or explicitly include delisted tickers.
Now that you have clean data, see how a backtest uses it — and what the parameter sweep reveals about overfitting.
SMA Backtest in Depth → Market Data Providers →