Skip to main content

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 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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tickbar-0.2.0.tar.gz (74.6 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

tickbar-0.2.0-cp311-abi3-win_amd64.whl (526.5 kB view details)

Uploaded CPython 3.11+Windows x86-64

tickbar-0.2.0-cp311-abi3-manylinux_2_34_x86_64.whl (711.4 kB view details)

Uploaded CPython 3.11+manylinux: glibc 2.34+ x86-64

tickbar-0.2.0-cp311-abi3-macosx_11_0_arm64.whl (544.6 kB view details)

Uploaded CPython 3.11+macOS 11.0+ ARM64

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

Hashes for tickbar-0.2.0.tar.gz
Algorithm Hash digest
SHA256 5c85d0f62e1d27641d19a778e770475795c29527e32747d3732ec9eb40c05ed1
MD5 1ed0ba77deeeb3e27a9267c2aac665ec
BLAKE2b-256 3cbd633bd847cb4b63db4484793d295703f10afc4920548b4f50cf1b31de9fcf

See more details on using hashes here.

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

Hashes for tickbar-0.2.0-cp311-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 a1be05c80ddd0e544fef1f0fbb016153edd152d802ece018a98fc93eb95a38af
MD5 b73d95d1a319d38052cccc0aaef7f5ab
BLAKE2b-256 478958ee77c2c2dab736ee3730faec73ae0f8005372e26cfe8b06ab0f38d5a11

See more details on using hashes here.

File details

Details for the file tickbar-0.2.0-cp311-abi3-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for tickbar-0.2.0-cp311-abi3-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 5941d6a478fd2ad9f8ead896ffc9ac38a62c4cb4a758aaf950544edc8d91b9c0
MD5 1efb2e49f6c6da39d0b0c685d80bb8bd
BLAKE2b-256 29df46e8ce694996be754dac48e97d47b2bad5d6aa761c7d75a0981db48be5ca

See more details on using hashes here.

File details

Details for the file tickbar-0.2.0-cp311-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for tickbar-0.2.0-cp311-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 e35c0c26817622fd33562ccf33a2bd6ad5a7ea3af005d1798e725d7352464887
MD5 b629d29373130e77dcf48e77f1ed3c04
BLAKE2b-256 a0f4439cda73340289b497634b76691f9a8fd3b81cb970e143c76b732c285561

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page