High-performance tick-to-bar aggregator — 6.7M ticks/s, 1.4× pandas
Project description
tickbar
High-performance tick-to-bar aggregator for financial market data.
Converts raw trade/quote ticks into OHLCV bars with configurable time alignment, gap filling, VWAP, and corporate action adjustments. One-pass state machine — 6.7M ticks/s from Python.
Key features
- Fast — 6.7M ticks/s from Python (PEP 3118 buffer protocol, zero-copy)
- One-pass streaming — no windowing, no sorting
- VWAP per bar
- Gap filling — empty bars for periods with no activity
- Forward fill — propagate last close through empty bars
- Corporate actions — split/dividend backward adjustment
- Export — CSV, Arrow IPC, Polars DataFrame
Installation
pip install tickbar
Requires Python ≥ 3.11.
Quick start
from tickbar import TickAggregator, Tick
agg = TickAggregator(interval_secs=60)
agg.push_tick(Tick(0, 100.0, 1000.0))
agg.push_tick(Tick(1_000_000_000, 100.5, 500.0))
bars = agg.finalize()
print(f"{len(bars)} bars")
for record in bars.to_records():
# [ts_ns, open, high, low, close, volume, tick_count, vwap]
print(record)
API reference
The complete binding reference lives in
docs/handbook/03-python-binding-reference.md.
This README is API-complete enough for PyPI users and mirrors the Rust crate
model closely.
Public Python classes:
| Class | Purpose |
|---|---|
Tick |
Trade or quote tick |
TickAggregator |
Streaming bar aggregator |
BarSeries |
Finalized bar container |
TickFilter |
Validation thresholds |
TradingCalendar |
Session range filter |
TradeClassifier |
Lee-Ready classifier |
TradeDirection |
Buy, Sell, Neutral |
Tick
Represents a single market data tick.
from tickbar import Tick
# timestamp (int, nanoseconds), price (float), volume (float)
tick = Tick(0, 100.0, 1000.0)
repr(tick) # "Tick(ts=0, price=100.0, volume=1000.0)"
quote = Tick.from_quote(1, 99.0, 101.0)
quote.is_quote() # True
quote.bid() # 99
quote.ask() # 101
quote.spread() # 2
quote.mid_price() # 100
BarSeries
A collection of completed bars, returned by TickAggregator.finalize().
from tickbar import TickAggregator
agg = TickAggregator(60)
# ... push ticks ...
bars = agg.finalize()
len(bars) # Number of bars
repr(bars) # "BarSeries(1950 bars)"
# Extract as list of [ts, open, high, low, close, volume, tick_count, vwap]
records = bars.to_records()
TickAggregator
The main aggregation class. Built with a fixed interval in seconds.
from tickbar import TickAggregator, Tick
# Constructor
agg = TickAggregator(interval_secs=60)
# Push one tick
agg.push_tick(tick)
# Push a batch of Tick objects
agg.push_ticks([tick1, tick2, tick3])
# Push from three int64 arrays — zero-copy via PEP 3118 buffer protocol
# Supports numpy, memoryview, array.array, bytes, etc.
agg.push_from_buffer(timestamps, prices, volumes)
# Push from three numpy int64 arrays — zero-copy via __array_interface__
agg.push_from_numpy(timestamps, prices, volumes)
# Push from three Python lists — copied into Rust
agg.push_from_arrays(timestamps, prices, volumes)
# Push from packed bytes — 32 bytes per tick
agg.push_from_bytes(data)
# Finalize and get bars (consumes the aggregator)
bars = agg.finalize()
Gap filling
Enable gap filling to produce empty bars for periods with no activity. Each gap-filled bar carries the previous bar's close price as open/high/low, close, and VWAP with zero volume:
from tickbar import TickAggregator, Tick
# Python exposes the high-throughput runtime constructors.
# Rust exposes the full builder surface for fill_gaps configuration.
Error handling
If you push out-of-order ticks (older timestamp than the previous tick),
push_tick and push_ticks raise a ValueError:
agg = TickAggregator(interval_secs=60)
agg.push_tick(Tick(100, 100.0, 1000.0)) # ts=100
agg.push_tick(Tick(50, 99.0, 500.0)) # ts=50 — ValueError!
The zero-copy methods (push_from_buffer, push_from_numpy, push_from_bytes)
skip ordering validation for maximum throughput. Ensure your data is pre-sorted
by timestamp before using them.
After finalize(), the native aggregator is consumed. Any later ingestion call
raises ValueError("aggregator already finalized").
Fixed-point scale
Prices and volumes use fixed-point int64 values. Typical scales:
| Asset | Price scale | Example |
|---|---|---|
| Stocks | 8 decimals (100_000_000) |
$100.50 → 10_050_000_000 |
| Crypto | 8+ decimals | $0.00123 → 123_000 |
| FX | 5-6 decimals | 1.12345 → 112_345_000 |
The push_from_arrays, push_from_numpy, push_from_buffer, and
push_from_bytes methods all work directly with these int64 values.
push_from_bytes format
The bytes must contain tightly packed Tick structs (32 bytes each):
| Offset | Type | Field |
|---|---|---|
| 0 | i64 |
timestamp_nanos |
| 8 | i64 |
price (fixed-point) |
| 16 | i64 |
volume (fixed-point) |
| 24 | u64 |
flags (0=trade, 1=quote) |
import struct
import numpy as np
# Build 3 ticks manually
data = b"".join(
struct.pack("<qqqQ", ts, price, vol, 0)
for ts, price, vol in [(0, 100_000_000_00, 1000), (1_000_000_000, 100_500_000_00, 500)]
)
agg = TickAggregator(60)
agg.push_from_bytes(data)
bars = agg.finalize()
Push method comparison
| Method | Zero-copy? | ticks/s | When to use |
|---|---|---|---|
push_from_buffer |
yes (PEP 3118) | 6-7M | Data in numpy/memoryview/array.array — fastest |
push_from_numpy |
yes (__array_interface__) |
5-6.5M | Data already in numpy arrays |
push_from_bytes |
yes | 5-6.5M | Data in packed binary buffers |
push_from_arrays |
no (copied) | 2.5-3.5M | Data in Python lists |
push_ticks |
mixed | ~0.9M | Batch of Tick objects |
push_tick |
N/A | ~0.8M | Streaming one tick at a time |
Filtering and calendars
from tickbar import TickAggregator, TickFilter, TradingCalendar
agg = TickAggregator(interval_secs=60)
flt = TickFilter()
flt.min_price = 1
flt.max_price = 10_000_000
flt.max_volume = 250_000
flt.max_price_change = 50_000
flt.reject = True
agg.set_filter(flt)
cal = TradingCalendar([
(52_200_000_000_000, 75_600_000_000_000),
])
agg.set_calendar(cal)
Calendar sessions are explicit nanosecond ranges. The binding does not compute exchange holidays or daylight-saving transitions.
Trade classification
from tickbar import Tick, TradeClassifier, TradeDirection
classifier = TradeClassifier()
classifier.update_quote(Tick.from_quote(0, 99.0, 101.0))
direction = classifier.classify_trade(Tick(1, 101.0, 10.0))
assert direction == TradeDirection.Buy
Use one classifier per symbol/session or call reset() between streams.
Bar record contract
Python bar records always use this field order: timestamp_nanos, open,
high, low, close, volume, tick_count, vwap.
TickAggregator.current_bar() returns one record or None. Iterating over a
TickAggregator drains completed bars without finalizing the active partial
bar. BarSeries.to_records() returns all finalized bars.
Arrow IPC export
When the native extension is built with the arrow-export feature,
BarSeries.to_arrow_bytes() returns Arrow IPC stream bytes:
import io
import pyarrow.ipc as ipc
payload = bars.to_arrow_bytes()
table = ipc.open_stream(io.BytesIO(payload)).read_all()
Performance
| Path | Throughput | vs pandas |
|---|---|---|
| Python buffer (PEP 3118) | 6.7M ticks/s | 1.4× |
| pandas resample | 4.7M ticks/s | 1.0× |
Benchmarked on 70K ticks from 9 S&P tickers via yfinance, aggregated to 1-minute bars. Hardware: Linux x86_64, CPython 3.12.
Operational contracts
- Use integer nanosecond timestamps.
- Convert prices and volumes to a consistent fixed-point
int64scale before ingestion. - Sort by timestamp before using zero-copy methods.
- Keep numpy/buffer objects alive for the duration of ingestion calls.
- Use
Tickobject ingestion for safety and ergonomics, not maximum throughput. - Use array or bytes ingestion for replay and research-scale workloads.
- Treat quote ticks as midpoint price updates with zero effective volume.
For production/replay guidance, see
docs/handbook/05-operational-guide.md.
Real-world workflows
50M ticks from Parquet → pandas
Load a Parquet file, aggregate to 1-minute bars via numpy zero-copy, back to pandas for your backtester.
import numpy as np
import pandas as pd
from tickbar import TickAggregator
# Load from Parquet
df = pd.read_parquet("tick_data_2024.parquet")
print(f"Loaded {len(df):,} ticks")
# Prepare int64 arrays (zero-copy into tickbar)
PRICE_SCALE = 100_000_000
timestamps = df["timestamp_ns"].values.astype(np.int64)
prices = (df["price"].values * PRICE_SCALE).astype(np.int64)
volumes = df["volume"].values.astype(np.int64)
# Aggregate
agg = TickAggregator(interval_secs=60)
agg.push_from_buffer(timestamps, prices, volumes)
bars = agg.finalize()
print(f"Produced {len(bars)} bars")
# Back to pandas
records = np.array(bars.to_records())
df_bars = pd.DataFrame(
records,
columns=["ts_ns", "open", "high", "low", "close", "volume", "tick_count", "vwap"],
)
df_bars["ts"] = pd.to_datetime(df_bars["ts_ns"], unit="ns")
yfinance tickers → multi-timeframe
Download 10 tickers, aggregate to 1-minute bars.
import numpy as np
import yfinance as yf
from tickbar import TickAggregator
tickers = ["AAPL", "MSFT", "GOOG", "AMZN", "META", "TSLA", "NVDA", "JPM", "V", "WMT"]
data = yf.download(tickers, period="10d", interval="1m", group_by="ticker", progress=False)
all_ticks = []
for t in tickers:
df = data[t].dropna()
for idx, row in df.iterrows():
ts = int(idx.timestamp() * 1e9)
price = int(round(float(row["Open"]) * 100_000_000))
vol = int(float(row["Volume"]))
all_ticks.append((ts, price, vol))
all_ticks.sort(key=lambda x: x[0])
ts = np.array([x[0] for x in all_ticks], dtype=np.int64)
pr = np.array([x[1] for x in all_ticks], dtype=np.int64)
vo = np.array([x[2] for x in all_ticks], dtype=np.int64)
agg = TickAggregator(60)
agg.push_from_buffer(ts, pr, vo)
bars = agg.finalize()
print(f"1-min bars: {len(bars)}")
Kafka → gap-filled bars → database
Consume a Kafka trade stream, aggregate with gap filling, write completed bars to TimescaleDB.
from tickbar import TickAggregator, Tick
from kafka import KafkaConsumer
import asyncpg
import json
consumer = KafkaConsumer("market-trades", bootstrap_servers="localhost:9092")
agg = TickAggregator(60)
pool = await asyncpg.create_pool("postgresql://localhost/tickdb")
for msg in consumer:
trade = json.loads(msg.value)
tick = Tick(trade["ts"], trade["price"], trade["size"])
agg.push_tick(tick)
bars = agg.finalize()
# Write to database ...
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tickbar-0.2.0.tar.gz.
File metadata
- Download URL: tickbar-0.2.0.tar.gz
- Upload date:
- Size: 74.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5c85d0f62e1d27641d19a778e770475795c29527e32747d3732ec9eb40c05ed1
|
|
| MD5 |
1ed0ba77deeeb3e27a9267c2aac665ec
|
|
| BLAKE2b-256 |
3cbd633bd847cb4b63db4484793d295703f10afc4920548b4f50cf1b31de9fcf
|
File details
Details for the file tickbar-0.2.0-cp311-abi3-win_amd64.whl.
File metadata
- Download URL: tickbar-0.2.0-cp311-abi3-win_amd64.whl
- Upload date:
- Size: 526.5 kB
- Tags: CPython 3.11+, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a1be05c80ddd0e544fef1f0fbb016153edd152d802ece018a98fc93eb95a38af
|
|
| MD5 |
b73d95d1a319d38052cccc0aaef7f5ab
|
|
| BLAKE2b-256 |
478958ee77c2c2dab736ee3730faec73ae0f8005372e26cfe8b06ab0f38d5a11
|
File details
Details for the file tickbar-0.2.0-cp311-abi3-manylinux_2_34_x86_64.whl.
File metadata
- Download URL: tickbar-0.2.0-cp311-abi3-manylinux_2_34_x86_64.whl
- Upload date:
- Size: 711.4 kB
- Tags: CPython 3.11+, manylinux: glibc 2.34+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5941d6a478fd2ad9f8ead896ffc9ac38a62c4cb4a758aaf950544edc8d91b9c0
|
|
| MD5 |
1efb2e49f6c6da39d0b0c685d80bb8bd
|
|
| BLAKE2b-256 |
29df46e8ce694996be754dac48e97d47b2bad5d6aa761c7d75a0981db48be5ca
|
File details
Details for the file tickbar-0.2.0-cp311-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: tickbar-0.2.0-cp311-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 544.6 kB
- Tags: CPython 3.11+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e35c0c26817622fd33562ccf33a2bd6ad5a7ea3af005d1798e725d7352464887
|
|
| MD5 |
b629d29373130e77dcf48e77f1ed3c04
|
|
| BLAKE2b-256 |
a0f4439cda73340289b497634b76691f9a8fd3b81cb970e143c76b732c285561
|