Explainer
OHLC and OHLCV explained: how price bars are built from ticks
A bar is a lossy summary of every trade in a window. Knowing exactly what it keeps and what it throws away is the difference between a chart and a bug.
On this page
Key takeaways
- OHLC stands for open, high, low and close: the first, highest, lowest and last traded price in a fixed window. OHLCV adds volume, the quantity traded in that window.
- Bars are built from trades, not quotes: put each trade in the window its event time falls in, then take the first, maximum, minimum, last and sum.
- A TickerLayer bar is stamped with the start of its window: the one-minute bar at 13:30:00 UTC covers 13:30:00.000 up to 13:31:00.000.
- Resampling must keep the source's anchor: US hourly bars start at 09:30, so pandas needs `resample("1h", offset="30min")` to match them.
- Bars hide the bid-ask spread, the order of trades inside the window, and anything that traded outside the session they cover.
OHLC stands for open, high, low and close: the first, highest, lowest and last price traded in a fixed window of time. OHLCV adds volume, the total quantity traded in that window. Together they make a bar, the unit behind every candlestick chart and most historical stock data. This is the first one-minute bar of Coca-Cola's session on Friday 2026-09-25:
One OHLCV bar
{
"o": 88.16,1
"h": 88.3183,2
"l": 87.935,3
"c": 88.06,4
"v": 268550,5
"t": 17903430000006
}
oOpen: the first trade in the minute, here the regular-session open.hHigh: the highest trade. Four decimals are real; stocks can print below a cent.lLow: the lowest trade in the minute.cClose: the last trade before 09:31.vVolume: 268,550 shares. The first minute of a session is usually one of the busiest.tThe window start: 1790343000000 is 13:30:00 UTC, 09:30 in New York.
Four prices and a number sound simple, and they are, until you build bars yourself, merge two sources, or turn one interval into another. This explainer covers the details that break charts in production: which events feed a bar, which clock assigns them to a window, and which time the bar carries.
Quote vs trade vs bar
Market data comes in three shapes, and mixing them up is a common source of confusing charts:
| Feature | Quote | Trade (tick) | Bar |
|---|---|---|---|
| What it is | The best bid and ask standing now | One deal that happened | A summary of every trade in a window |
| Key fields | bid, ask, sizes | price, size | o, h, l, c, v |
| Time field | timestamp on REST, ts on the stream | timestamp on REST, ts on the stream | t, the window start |
| Stock channel | stocks.quotes | stocks.trades | stocks.agg |
| Good for | Spreads, fill estimates | Tapes, building your own bars | Charts, indicators, backtests |
Tick data usually means the raw trades, one record per event, and sometimes every quote change as well. Bars are built from the trades alone. A quote can change many times without anyone trading, and none of that touches the bar, which is why the bid-ask spread is invisible on a candlestick chart.
How a bar is built from ticks
- Trades arriveprice, size, ts
- Bucket by tsfloor to the window start
- Aggregatefirst, max, min, last, sum
- Window closesthe bar is final
- Emit the bart = window start
The rules are short. For every trade, find the window its event time falls in: floor(ts / width) × width. Within a window, the open is the earliest trade, the high and low are the extremes, the close is the latest trade, and the volume is the sum of sizes. A window with no trades produces no bar at all, which is why minute charts of quiet stocks have gaps. Here are five real trades from the crypto.trades stream, all inside one half second:
| Arrived | Event time (ts) | Price | Size, BTC |
|---|---|---|---|
| 1st | 10:52:40.079 | 82,996.15 | 0.000119 |
| 2nd | 10:52:40.159 | 82,976.77 | 0.00023 |
| 3rd | 10:52:40.298 | 82,980.10 | 0.000118 |
| 4th | 10:52:40.442 | 82,941.38 | 0.00000005 |
| 5th | 10:52:40.396 | 82,976.77 | 0.00015 |
The bar for that half second is open 82,996.15, high 82,996.15, low 82,941.38, close 82,941.38, volume 0.00061705 BTC. Look at the close. The last frame to arrive carried 82,976.77, but the last trade to happen was 82,941.38 at .442. An aggregated stream collects prints from several sources, so a frame can land a few milliseconds after a later one. Sort by ts before you take the close, or your closes will depend on network timing.
The range is worth a second look too: $54.77 between high and low inside half a second, about 6.6 basis points. Prints from different venues can differ by a few basis points at the same instant, so a bar built from an aggregated tape can span a wider range than any single venue's chart. Here is the same bar-building applied to a whole session of Coca-Cola, in 15-minute windows:
US:KO in 15-minute bars, 2026-09-25
The timestamp is the start of the bar
Every bar carries one time, and TickerLayer uses the start of its window: the one-minute bar with t at 13:30:00 UTC covers trades from 13:30:00.000 up to, but not including, 13:31:00.000. Daily bars are labelled at midnight UTC of the session date. Some tools label bars by their close instead, and mixing the two conventions shifts every bar by one interval. That bug survives code review, because the chart still looks plausible.
The latest settled one-minute bar
{
"symbol": "US:KO",
"interval": "1m",
"bar_start": 1790366340000,1
"bar_end": 1790366400000,2
"as_of": 1790591117557,3
"result": { "o": 87.865, "h": 87.865, "l": 87.785, "c": 87.81, "v": 322781, "t": 1790366340000 }
}
bar_start19:59:00 UTC, the last regular-session minute on Friday. Always equal tot.bar_end20:00:00 UTC, the 16:00 close in New York. The window includes its start and excludes its end.as_ofWhen the answer was built: Monday morning, market closed, so the last real bar came back.
Streaming bars follow the same rule. A stocks.agg frame's ts is the bar start, and the frame is sent only after the window closes, never while the bar is forming (see bar messages). To draw a live candle, build it from stocks.trades and replace it with the settled bar when that arrives. The Unix timestamp guide covers converting these values without time-zone surprises.
How to read candlestick charts and OHLC charts
A candlestick draws one bar. The body spans the open and the close, and the thin wicks reach up to the high and down to the low. When the close is above the open, the body is usually hollow or green; when below, filled or red. An OHLC chart shows the same four prices as a vertical line from low to high, with a small tick on the left for the open and on the right for the close.
- Long bodyPrice travelled a long way from open to close. The window had a clear direction.
- Long upper wickPrice traded much higher inside the window but fell back before the close.
- DojiOpen and close almost equal: the window ended about where it began, whatever happened in between.
- GapA bar opens away from the previous close: news, an auction, or a stretch with no trades.
The 09:30 bar above shows several at once: it opened at 88.16, spiked to the day's high of 88.3183, fell to 87.61 and closed at 87.71, a long red body under a clear upper wick. To draw charts like this yourself, build a candlestick chart in JavaScript from the same bars.
Resample ticks into OHLCV bars with pandas
pandas builds bars in a few lines once trades sit in a time-indexed frame. This example uses thirteen real crypto.trades frames, trimmed to the fields that matter, so it runs without a key:
import pandas as pd
# Thirteen crypto.trades frames for BTCUSD, as they arrived (trimmed to the
# fields that matter). Prices and sizes come as strings on this channel.
frames = [
{"price": "82996.15", "size": "0.000119", "ts": 1790592760079},
{"price": "82976.77", "size": "0.00023", "ts": 1790592760159},
{"price": "82980.1", "size": "0.000118", "ts": 1790592760298},
{"price": "82941.38", "size": "5e-8", "ts": 1790592760442},
{"price": "82976.77", "size": "0.00015", "ts": 1790592760396},
{"price": "82941.39", "size": "0.00001027", "ts": 1790592760810},
{"price": "82976.78", "size": "0.002", "ts": 1790592760823},
{"price": "82976.78", "size": "0.01692", "ts": 1790592760838},
{"price": "82976.78", "size": "0.001", "ts": 1790592760854},
{"price": "82941.38", "size": "2e-8", "ts": 1790592760943},
{"price": "82976.78", "size": "0.00086", "ts": 1790592760941},
{"price": "82980.2", "size": "0.001299", "ts": 1790592760986},
{"price": "82980.2", "size": "0.00701", "ts": 1790592760986},
]
ticks = pd.DataFrame(frames)
ticks["price"] = ticks["price"].astype(float)
ticks["size"] = ticks["size"].astype(float) # handles "5e-8" too
ticks["time"] = pd.to_datetime(ticks.pop("ts"), unit="ms", utc=True)
# Order by event time, not arrival: two frames above arrived out of order.
ticks = ticks.sort_values("time", kind="stable").set_index("time")
bars = ticks["price"].resample("500ms").ohlc()
bars["volume"] = ticks["size"].resample("500ms").sum()
bars["trades"] = ticks["price"].resample("500ms").count()
print(bars.round(6).to_string()) open high low close volume trades
time
2026-09-28 10:52:40+00:00 82996.15 82996.15 82941.38 82941.38 0.000617 5
2026-09-28 10:52:40.500000+00:00 82941.39 82980.20 82941.38 82980.20 0.029099 8Change "500ms" to "1min" and feed it a live list of frames, and you have a minute-bar builder. Two habits keep it honest: convert prices and sizes explicitly, since the crypto, forex and stocks channels send them as strings, and sort by event time before resampling.
From small bars to big bars
The same aggregation turns small bars into big ones: first open, highest high, lowest low, last close, summed volume. The trap is the anchor. US stock bars start at the 09:30 open, but pandas starts hourly buckets on the hour:
import os
import pandas as pd
import requests
BASE_URL = "https://api.tickerlayer.com"
HEADERS = {"x-api-key": os.environ["TICKERLAYER_API_KEY"]}
resp = requests.get(
f"{BASE_URL}/stocks/agg/US:KO/1/minute/2026-09-25/2026-09-25",
params={"sort": "asc", "limit": 5000},
headers=HEADERS,
timeout=15,
)
resp.raise_for_status()
minutes = pd.DataFrame(resp.json()["results"])
minutes.index = pd.to_datetime(minutes.pop("t"), unit="ms", utc=True).dt.tz_convert("America/New_York").rename("time")
RULES = {"o": "first", "h": "max", "l": "min", "c": "last", "v": "sum"}
# Wrong for US stocks: pandas starts hourly buckets on the hour (09:00, 10:00 ...).
naive = minutes.resample("1h").agg(RULES)
# Right: anchor buckets at the 09:30 open, the way the API's hourly bars are built.
hourly = minutes.resample("1h", offset="30min").agg(RULES)
print(naive.head(2))
print(hourly.head(2))
print(len(minutes), "minute bars ->", len(hourly), "hourly bars") o h l c v
time
2026-09-25 09:00:00-04:00 88.16 88.3183 87.580 87.940 1429879
2026-09-25 10:00:00-04:00 87.93 88.0600 87.675 87.765 1297770
o h l c v
time
2026-09-25 09:30:00-04:00 88.16 88.3183 87.580 87.9001 2204492
2026-09-25 10:30:00-04:00 87.90 87.9300 87.675 87.8400 897008
390 minute bars -> 7 hourly barsThe naive first bar is labelled 09:00 and holds only 30 minutes of trading, and every bar after it straddles two of the API's hours. With offset="30min" the result matches GET /stocks/agg/US:KO/1/hour/... exactly, down to the 2,204,492 shares in the first hour. The 15-minute case checks out the same way: all 26 bars resampled from minutes matched the API's own 15-minute bars field for field.
What a bar cannot tell you
- The spread. Bars come from trades, and quotes never touch them.
- The order of events inside the window. A bar does not say whether the high or the low came first, so a backtest that needs to know which was hit first cannot tell from bars alone.
- Where the volume traded. For a volume-weighted price you need the trades, or VWAP.
- Anything outside the session it covers. US intraday bars over REST run 09:30 to 16:00, so the closing auction and extended-hours trading show up only in the daily bar.
- Whether the newest bar is final. During the session, a range request includes the bar in progress.
- Whether a print was bad. One erroneous trade becomes the high or low of its bar, so filter bad ticks before you aggregate.
Questions
What does OHLC stand for?
Open, high, low, close: the first, highest, lowest and last traded price in a time window such as one minute or one day.
What is the difference between OHLC and OHLCV?
OHLCV adds volume, the total quantity traded in the window. Volume shows how much trading stood behind a move, which the four prices alone cannot.
What is tick data?
Tick data is the raw record of individual market events, usually every trade and sometimes every quote change, each with its own timestamp. Bars are built by summarizing ticks over fixed windows.
Is a candlestick timestamp the open time or the close time?
It depends on the source. TickerLayer stamps each bar with the start of its window, so a one-minute bar at 13:30:00 UTC covers 13:30:00 to 13:30:59.999. Check the convention before merging data from two sources.
How do you read an OHLC chart?
Each bar is a vertical line from the low to the high, with a small tick on the left for the open and one on the right for the close. If the right tick sits higher than the left, the price rose during the window.
Why does my resampled hourly bar not match the API?
Your buckets probably start on the hour. US stock bars are anchored at the 09:30 open, so pass offset="30min" to pandas resample("1h").