
From Tick Data to Candlestick Charts
Learn how to convert tick data into OHLCV candlesticks with Python, handle timestamps, volume, missing intervals, and late ticks, and maintain consistent data for quantitative backtesting.
For quantitative traders, converting tick data into candlestick charts is more than a visualization task. The aggregation logic determines whether historical bars accurately reflect market activity and whether backtest signals match the data available during live trading.
A misplaced timestamp, incorrect candle boundary, or duplicated volume can distort a strategy's results. This guide explains how to convert tick data into OHLCV candles, provides a reusable Python implementation, and covers the edge cases that matter for reliable backtesting.
What Tick Data and Candlestick Data Represent
Tick data records individual market events. Depending on the feed, a tick may represent a trade, a quote update, or another market event. A typical record contains a timestamp, price, and sometimes volume or trade size.
A candlestick compresses these events into a fixed time interval. Each candle contains four price values:
- Open: The price of the first eligible tick in the interval.
- High: The highest eligible price in the interval.
- Low: The lowest eligible price in the interval.
- Close: The price of the last eligible tick in the interval.
- Volume: The sum of individual trade sizes, when the input represents trades and the feed provides per-trade volume.
For example, a one-minute candle summarizes the eligible events within a 60-second window. The resulting OHLCV data can support technical indicators, signal generation, and historical strategy testing.
The distinction between trades and quotes matters. If the feed contains bid and ask updates rather than transaction prices, define whether the strategy uses bid, ask, midpoint, or another reference price. Do not automatically treat quote updates as executed trades or sum quote fields as traded volume.
How to Aggregate Tick Data Correctly
A reliable tick-to-candlestick process follows four rules.
Define the time window. Set the interval in seconds: 60 for one-minute candles, 300 for five-minute candles, or 900 for 15-minute candles. Use consistent timestamp units throughout the calculation.
Assign each tick to a candle. For an interval of length (\Delta t), calculate the candle's start timestamp as:
[
T_{\text{start}}=
\left\lfloor\frac{T_{\text{tick}}}{\Delta t}\right\rfloor
\Delta t
]
This creates start-inclusive, end-exclusive windows. A tick timestamped exactly at 10:01:00 belongs to the 10:01 candle, not the candle ending at that time.
Initialize and update OHLCV. The first tick initializes all four price fields. Subsequent ticks update High, Low, Close, and Volume. Open remains unchanged.
Finalize the previous candle when the interval changes. When a tick belongs to a new interval, save the previous candle and initialize a new one using the incoming tick. Do not assume that only one interval has elapsed; incoming data may skip one or more periods.
For exchange-session strategies, also align candles with the relevant market calendar and session boundaries. Epoch-based interval alignment is suitable for common intraday windows, but daily and session-based bars may require exchange-specific rules.
Python Example for Generating Candlestick Data
The following Python 3.10+ implementation converts a collection of timestamped ticks into OHLCV candles. It assumes that timestamps are Unix seconds, prices are trade prices, and volume represents the size of each trade.
from dataclasses import dataclass
from typing import Iterable
@dataclass(frozen=True)
class Tick:
timestamp: int # Unix timestamp in seconds
price: float
volume: float # Per-trade size
@dataclass
class Candle:
timestamp: int # Start of the candle
open: float
high: float
low: float
close: float
volume: float
def ticks_to_candles(
ticks: Iterable[Tick],
interval_seconds: int
) -> list[Candle]:
if interval_seconds <= 0:
raise ValueError("Interval must be positive")
ordered_ticks = sorted(
ticks, key=lambda tick: tick.timestamp
)
candles = []
current = None
for tick in ordered_ticks:
bucket_start = (
tick.timestamp
- tick.timestamp % interval_seconds
)
if current is None or bucket_start != current.timestamp:
if current is not None:
candles.append(current)
current = Candle(
timestamp=bucket_start,
open=tick.price,
high=tick.price,
low=tick.price,
close=tick.price,
volume=tick.volume,
)
else:
current.high = max(current.high, tick.price)
current.low = min(current.low, tick.price)
current.close = tick.price
current.volume += tick.volume
if current is not None:
candles.append(current)
return candles
ticks = [
Tick(120, 100.0, 2),
Tick(135, 101.0, 3),
Tick(181, 99.0, 1),
]
candles = ticks_to_candles(ticks, interval_seconds=60)
for candle in candles:
print(candle)The example produces two candles:
| Start timestamp | Open | High | Low | Close | Volume |
|---|---|---|---|---|---|
| 120 | 100.0 | 101.0 | 100.0 | 101.0 | 5 |
| 180 | 99.0 | 99.0 | 99.0 | 99.0 | 1 |
The first two ticks fall within the same 60-second window, so their prices and trade sizes contribute to one candle. The third tick belongs to the next window and starts another candle.
The implementation also sorts out-of-order input and avoids generating artificial candles for intervals without ticks. For production trading systems, consider fixed-point or decimal arithmetic for price calculations and use exchange sequence numbers to resolve events sharing the same timestamp.
Handling Real-Time Data and Backtesting Edge Cases
Batch aggregation and live aggregation should follow the same candle boundaries and price-selection rules. Several details can otherwise create discrepancies.
Late or duplicated ticks: Deduplicate events where the feed provides unique trade IDs or sequence numbers. Decide how to handle late-arriving events that belong to a candle already finalized. Depending on the strategy, this may require correcting a historical candle or rejecting the late event.
Missing intervals: An interval without eligible trades does not necessarily require a candle. If a downstream strategy requires continuous time intervals, fill missing bars only under an explicit policy, such as carrying forward the previous close. Mark synthetic bars so they are not mistaken for actual trading activity.
Volume semantics: Sum per-trade sizes only when the source provides incremental trade volume. Cumulative volume and quote-update fields require different handling.
Market sessions: Account for exchange time zones, trading breaks, holidays, and opening or closing rules. A timestamp-based aggregation function alone cannot determine every market's official candle convention.
Incomplete candles: In a live stream, the current candle remains provisional until its interval closes. Finalizing and publishing a candle should be a separate decision from updating its latest values.
Applying the Method to a Quantitative Trading Workflow
For quantitative traders, reproducibility is the main objective. The same aggregation rules should be applied to historical data used in backtesting and incoming events used for live signals. Otherwise, a strategy may appear profitable because of differences in candle construction rather than a genuine trading advantage.
When sourcing market data through AllTick, consult the AllTick API documentation to identify the applicable REST or WebSocket interface and verify the available fields and their meanings. Keep data ingestion separate from candle aggregation so that timestamp normalization, symbol mapping, and OHLCV construction can be tested independently.
Before using generated candles in a strategy, compare selected bars against their underlying ticks. Verify the first and last eligible event, high and low prices, volume totals, and interval boundaries. These checks help identify aggregation errors before they affect indicators or backtest results.
Accurate candlestick construction does not guarantee a profitable strategy, but it gives quantitative traders a more reliable foundation for evaluating one.