OHLCV data: what each bar means and how it is built
OHLCV is five numbers per symbol per interval: open, high, low, close, volume. The definitions fit on one line. The details that break backtests do not, so this page measures them on real files from this archive: AAPL on 2024-01-03, and every US stock and the main index futures on 2026-10-28.
The five numbers
For any interval (a minute, an hour, a trading day):
- open: price of the first trade in the interval
- high / low: highest and lowest trade prices
- close: price of the last trade
- volume: shares (or contracts) traded
Two real bars for AAPL on 2024-01-03, one daily and the first regular-session minute:
bar open high low close volume daily 184.22 185.88 183.43 184.25 58,414,500 1-min 09:30 184.29 184.74 183.86 184.69 1,477,331
Note that the day opened at 184.22 while the first minute opened at 184.29. The daily bar is not built from the minute bars, and the next section shows how far apart they are.
A daily bar is not the sum of its minutes
On 2026-10-28 we compared each US stock's daily bar with its own 1-minute bars, for the 2,108 stocks that traded in at least 300 of the 390 regular-session minutes:
| daily field | matches | share of stocks |
|---|---|---|
| open | open of the 09:30 minute | 89% |
| high | highest high, 09:30-15:59 minutes | 99.4% |
| high | highest high, all minutes 04:00-19:59 | 64% |
| close | open of the 16:00 minute | 83% |
| close | close of the 15:59 minute | 30% |
| volume | sum of all 1-minute volume | none: always larger, median 1.30x |
What the numbers say:
- The daily open and close match the auction prints. The listing exchange runs an opening and a closing auction, and the daily bar takes their prices, not the first and last trades. The closing auction prints at 16:00, so it lands in the bar stamped 16:00, after the 15:59 minute has closed. For AAPL on 2024-01-03 the 16:00 bar opened at 184.25, the daily close, on 7,984,020 shares.
- The daily high and low ignore extended hours. 720 of those stocks traded above their regular-session high before 09:30 or after 16:00. The daily high matched the regular-session high for 99% of them.
- Daily volume is the consolidated total and is larger than the intraday bars. AAPL on 2024-01-03: 37,331,553 shares in the regular-session minutes, 46,105,155 in all 741 minutes, 58,414,500 in the daily bar. Across the 2,108 stocks the daily figure was 1.07 to 1.53 times the minute total (10th to 90th percentile). The intraday bars count fewer trade types than the consolidated tape. Which conditions are left out is the source's bar-building rule, and it is not published.
Three practical consequences:
- A backtest that "buys the close" from 1-minute data should use the 16:00 bar's open (the auction), not the 15:59 close. The 16:00 open matched the daily close for 83% of liquid stocks; the 15:59 close differed from it for 70%.
- Do not rebuild daily bars from intraday bars, and do not reconcile the two on volume. They are separate measurements.
- Volume-based filters such as "average daily volume above 1M" should use the daily file. Intraday sums run low.
Timestamps: bar open, US/Eastern, no time zone attached
Intraday files store timestamp as a timezone-naive
TIMESTAMP (Parquet isAdjustedToUTC=false, microseconds)
holding the bar open time in New York. The 09:30 bar holds trades
from 09:30:00 to 09:30:59. Stock and ETF files run from the 04:00 bar to the 19:59
bar, so the regular session is 09:30 <= time < 16:00. Because the
clock is New York wall time, 09:30 is 09:30 in both summer and winter, and no
daylight-saving conversion is needed.
The common bug is look-ahead. The bar stamped 10:00 is not complete until 10:01. If a signal fires at 10:00, the newest bar it may use is the one stamped 09:59.
import pandas as pd
bars = pd.read_parquet("stock_1min_2024-01-03.parquet")
bars["available_at"] = bars["timestamp"] + pd.Timedelta(minutes=1)
# join signals on available_at, never on timestamp
Longer bars align to the clock, not to the session. 30-minute bars start at :00 and :30, so 09:30 begins a bar. 1-hour bars start on the hour, so the 09:00 bar mixes 30 minutes of pre-market with the first 30 minutes of the session (AAPL, 2026-10-28: 2,611,313 shares in the 09:00 bar). If you need 09:30-aligned hourly bars, build them from the 1-minute file (see resampling below).
A missing minute means nothing traded
A 1-minute bar exists only if the symbol traded in that minute. Nothing is forward-filled: AAPL on 2024-01-03 has 741 bars out of 960 possible minutes from 04:00 to 19:59. Thin names have far fewer. On 2026-10-28 the median US stock traded in 167 of the 390 regular-session minutes, and the 10th percentile in 7.
If your code needs a dense grid, build it explicitly and decide what an empty minute means. Usually that is: price carried forward from the last close, volume zero.
import pandas as pd
one = bars[bars.symbol == "AAPL"].set_index("timestamp")
grid = pd.date_range("2024-01-03 09:30", "2024-01-03 15:59", freq="1min")
dense = one.reindex(grid)
dense["close"] = dense["close"].ffill()
for col in ("open", "high", "low"):
dense[col] = dense[col].fillna(dense["close"])
dense["volume"] = dense["volume"].fillna(0)
Resampling OHLCV correctly
The rules are first open, max high, min low, last close, summed volume, over buckets that are closed on the left and labelled by their start. Those are the rules this archive's own files follow: on 2026-10-28, resampling every US stock's 1-minute bars this way reproduced 410,351 of the 410,354 bars in the 5-minute file exactly.
agg = {"open": "first", "high": "max", "low": "min",
"close": "last", "volume": "sum"}
five = (bars.set_index("timestamp")
.groupby("symbol")
.resample("5min", label="left", closed="left")
.agg(agg)
.dropna(subset=["open"])) # drop buckets with no trades
# hourly bars aligned to the 09:30 open instead of the clock
hourly = (bars.set_index("timestamp").between_time("09:30", "15:59")
.groupby("symbol")
.resample("60min", offset="30min").agg(agg)
.dropna(subset=["open"]))
The same in DuckDB, which is faster on a whole day of symbols:
SELECT symbol,
time_bucket(INTERVAL 5 MINUTE, timestamp) AS timestamp,
arg_min(open, timestamp) AS open,
max(high) AS high,
min(low) AS low,
arg_max(close, timestamp) AS close,
sum(volume) AS volume
FROM 'stock_1min_2024-01-03.parquet'
GROUP BY ALL ORDER BY symbol, timestamp;
Without dropna, pandas emits a row of NaNs for every empty bucket, and
a later ffill turns them into fake flat bars.
Futures bars follow the exchange's trading day
CME index futures trade from 18:00 to 17:00 New York time. The daily bar covers that session, not the calendar day. NQ's daily bar for 2026-10-28 opened at 29,814.25, with a high of 30,178.00 and a low of 29,674.25. Those match the 1-minute bars from 18:00 on 10-27 through 16:59 on 10-28 exactly. The calendar day 00:00-23:59 gives a different open (29,935.00) and low (29,636.00).
The daily close is the exchange settlement price, not the last trade. NQ settled at 29,979.50. The last minute of the session, 16:59, closed at 29,756.25. The settlement sits in the exchange's settlement window: around 16:00 ET for the equity-index contracts, 14:30 for crude oil (CL: 102.78; its 14:28 minute opened at 102.92 and closed at 102.77) and 13:30 for gold (GC: 4,051.60, the close of the 13:30 minute). A futures backtest that marks positions at the daily close is marking to settlement.
Continuous futures add one more layer: contracts are stitched together at each roll and earlier history is ratio-adjusted, so old price levels differ from what traded. Details and the published roll table: continuous futures.
Adjusted prices are still OHLCV, on a different basis
Stock and ETF bars here are split-adjusted: every bar before a split is divided by the split factor, and volume is multiplied by it. NVDA split 10-for-1 effective 2024-06-10. Its 2024-06-07 daily bar reads close 120.888 on 412,386,000 shares. The tape that day said about 1,208.88 on about 41.2 million. Dividends are not embedded, so the prices are the traded levels on today's share basis. More on this: data quality and methodology.
Checklist for any OHLCV source
- Is the timestamp the bar's open or its close, and in which time zone?
- Which sessions are included: regular only, or pre- and post-market too?
- Are the daily open and close auction prints, or the first and last trades?
- How is a minute with no trades represented: no row, a zero-volume row, or a forward-filled row?
- Are prices raw, adjusted for splits only, or for splits and dividends? Is volume adjusted to match?
- Are delisted symbols in the files, or only today's listings?
- For futures: whose trading day, settlement or last trade, and how are rolls stitched?
Every answer for this archive is on this page or on data quality. A vendor that cannot answer them has not looked.
Related
historical OHLCV data archive · browse stock 1-minute files · DuckDB guide · Polars guide · continuous futures · all guides