Getting market data: OHLCV, sources & resolution
Understand what a bar actually contains, where to get it for stocks and crypto, how timeframe changes everything, and why every timestamp you store should be UTC.
Every strategy is downstream of its data. Before we manipulate a single price we need to know exactly what a "bar" is, where honest data comes from, and the two silent killers of beginner datasets: resolution and timezones.
What OHLCV actually is
Market data is almost always delivered as OHLCV bars — one row summarising a fixed slice of time. Each bar carries five numbers:
- Open — the first traded price in the interval.
- High — the highest price reached.
- Low — the lowest price reached.
- Close — the last traded price. The most-used number in all of trading.
- Volume — how much was traded. Your window into conviction and liquidity.
A candlestick draws all five at once: the body spans open-to-close (green if it closed up, red if down), and the thin wicks reach out to the high and low. One candle tells you not just where price ended but the *fight* that got it there.
Where the data comes from
Different markets have different plumbing. Three sources cover almost everything you will do as a retail algo trader:
- Stocks / ETFs / indices / FX —
yfinancewraps Yahoo Finance for free daily and (limited) intraday history. Great for learning; for serious work you graduate to a paid vendor. - Crypto —
ccxtspeaks to Binance, Kraken, Coinbase and 100+ others with one interface. Crypto trades 24/7 and exchanges hand you clean OHLCV directly. - Live / your broker — the account you actually trade through (Interactive Brokers, Alpaca, OANDA, an MT5 bridge) has its own API. This is your source of truth once you go live.
import yfinance as yf
class="c"># Daily bars for class="n">2 years. auto_adjust=True gives split/dividend-
class="c"># adjusted prices — the correct default for backtesting (Lesson class="n">5).
spy = yf.download(
class="s">"SPY",
start=class="s">"class="n">2022-class="n">01-class="n">01",
end=class="s">"class="n">2024-class="n">01-class="n">01",
interval=class="s">"1d",
auto_adjust=True,
progress=False,
)
print(spy.shape) class="c"># (rows, columns)
print(spy.columns.tolist())
print(spy.tail(class="n">3).round(class="n">2))import ccxt
import pandas as pd
exchange = ccxt.binance() class="c"># public data needs no API key
raw = exchange.fetch_ohlcv(
class="s">"BTC/USDT",
timeframe=class="s">"1h", class="c"># hourly bars
limit=class="n">500, class="c"># last class="n">500 hours
)
class="c"># ccxt returns [timestamp_ms, open, high, low, close, volume]
df = pd.DataFrame(raw, columns=[class="s">"ts", class="s">"open", class="s">"high", class="s">"low", class="s">"close", class="s">"volume"])
class="c"># Timestamps are epoch milliseconds in UTC — convert and set as index.
df[class="s">"ts"] = pd.to_datetime(df[class="s">"ts"], unit=class="s">"ms", utc=True)
df = df.set_index(class="s">"ts")
print(df.tail(class="n">3))Resolution changes the game
The timeframe (also called resolution or bar size) is not a cosmetic choice — it defines what strategy is even possible. A daily bar hides everything that happened inside the day; a 1-minute bar exposes noise that a swing strategy should ignore. As you go finer, you get more bars (more statistical power) but also more noise, more cost, and stricter data-quality demands.
The UTC rule
The single most common data bug is a timezone mistake. Yahoo returns exchange-local time; ccxt returns UTC; your laptop is on its own clock; daylight-saving shifts move sessions twice a year. Mix these and your "9:30 open" bar lands an hour off — quietly corrupting every intraday signal.
import pandas as pd
class="c"># A naive index (no timezone) — dangerous.
naive = pd.to_datetime([class="s">"class="n">2024-class="n">03-class="n">10 class="n">09:class="n">30", class="s">"class="n">2024-class="n">03-class="n">10 class="n">09:class="n">31"])
class="c"># Localize to the exchange zone, THEN convert to UTC for storage.
aware_utc = naive.tz_localize(class="s">"America/New_York").tz_convert(class="s">"UTC")
print(aware_utc)
class="c"># DatetimeIndex([class="s">'class="n">2024-class="n">03-class="n">10 class="n">13:class="n">30:class="n">00+class="n">00:class="n">00', ...], tz=class="s">'UTC')
class="c"># For display only, convert back at the edge:
print(aware_utc.tz_convert(class="s">"America/New_York"))With clean OHLCV in hand and every timestamp anchored to UTC, we can start actually working with the data. That is pandas — the next lesson.