Skip to main content

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 int64 scale before ingestion.
  • Sort by timestamp before using zero-copy methods.
  • Keep numpy/buffer objects alive for the duration of ingestion calls.
  • Use Tick object 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

Metadata

Release files for tickbar 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tickbar 0.2.0
File Size Uploaded
tickbar-0.2.0.tar.gz 74.6 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for tickbar 0.2.0
File Interpreter ABI Platform
tickbar-0.2.0-cp311-abi3-win_amd64.whl CPython 3.11 abi3 Windows x86-64 Details
tickbar-0.2.0-cp311-abi3-manylinux_2_34_x86_64.whl CPython 3.11 abi3 Linux glibc 2.34+ x86-64 Details
tickbar-0.2.0-cp311-abi3-macosx_11_0_arm64.whl CPython 3.11 abi3 macOS 11.0+ ARM64 Details

Total release size: 1.9 MB

Release files / tickbar-0.2.0.tar.gz

Download URL tickbar-0.2.0.tar.gz
Size 74.6 kB
Tags Source
SHA-256 checksum
How to use checksums
5c85d0f62e1d27641d19a778e770475795c29527e32747d3732ec9eb40c05ed1
BLAKE2b-256 checksum
How to use checksums
3cbd633bd847cb4b63db4484793d295703f10afc4920548b4f50cf1b31de9fcf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.14.1

Release files / tickbar-0.2.0-cp311-abi3-win_amd64.whl

Download URL tickbar-0.2.0-cp311-abi3-win_amd64.whl
Size 526.5 kB
Tags CPython 3.11 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
a1be05c80ddd0e544fef1f0fbb016153edd152d802ece018a98fc93eb95a38af
BLAKE2b-256 checksum
How to use checksums
478958ee77c2c2dab736ee3730faec73ae0f8005372e26cfe8b06ab0f38d5a11
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.14.1

Release files / tickbar-0.2.0-cp311-abi3-manylinux_2_34_x86_64.whl

Download URL tickbar-0.2.0-cp311-abi3-manylinux_2_34_x86_64.whl
Size 711.4 kB
Tags CPython 3.11 Linux glibc 2.34+ x86-64 abi3
SHA-256 checksum
How to use checksums
5941d6a478fd2ad9f8ead896ffc9ac38a62c4cb4a758aaf950544edc8d91b9c0
BLAKE2b-256 checksum
How to use checksums
29df46e8ce694996be754dac48e97d47b2bad5d6aa761c7d75a0981db48be5ca
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.14.1

Release files / tickbar-0.2.0-cp311-abi3-macosx_11_0_arm64.whl

Download URL tickbar-0.2.0-cp311-abi3-macosx_11_0_arm64.whl
Size 544.6 kB
Tags CPython 3.11 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
e35c0c26817622fd33562ccf33a2bd6ad5a7ea3af005d1798e725d7352464887
BLAKE2b-256 checksum
How to use checksums
a0f4439cda73340289b497634b76691f9a8fd3b81cb970e143c76b732c285561
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.14.1

Release history Release notifications | RSS feed

This release

0.2.0 This release

4 release files

0.1.0

4 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page