Metadata-Version: 2.4
Name: tickbar
Version: 0.2.0
License-File: LICENSE
Summary: High-performance tick-to-bar aggregator — 6.7M ticks/s, 1.4× pandas
Author: tickbar contributors
License-Expression: MIT
Requires-Python: >=3.11
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# tickbar

[![Crates.io][crates-badge]][crates-url]
[![PyPI][pypi-badge]][pypi-url]
![Rust 2024](https://img.shields.io/badge/rust-2024-edition)

**High-performance tick-to-bar aggregator for financial market data.**

Converts raw trade/quote ticks into OHLCV bars. One-pass state machine — **119M ticks/s** native Rust, **6.7M ticks/s** from Python via zero-copy PEP 3118 buffer protocol.

---

## Features

| Category | Capabilities |
|----------|-------------|
| **Bar types** | Time, volume, tick-count, dollar bars with configurable thresholds |
| **Throughput** | 119M ticks/s (Rust), 6.7M ticks/s (Python zero-copy) |
| **Streaming** | One-pass state machine — no windowing, no sorting, no backfill |
| **Quote support** | Bid, ask, spread, mid price for quote ticks |
| **Trade classification** | Lee-Ready algorithm (quote rule + tick rule fallback) |
| **Tick filtering** | Min/max price, max volume, max price jump (reject or skip) |
| **Trading calendar** | Session-aware aggregation, cross-session bar finalization |
| **Time alignment** | UTC or custom-offset interval boundaries (`:00`, `:15`, etc.) |
| **Gap filling** | Empty bars for inactive periods using previous-close OHLC |
| **VWAP** | Per-bar volume-weighted average price |
| **In-flight access** | Peek at the current partial bar at any time |
| **Streaming iterator** | Python `for bar in agg:` drains completed bars |
| **Batch API** | `process_batch()` for one-shot aggregation |
| **Multi-symbol** | Rayon-based parallel aggregation |
| **Corporate actions** | Split/dividend backward adjustment on completed series |
| **Export** | CSV, Arrow IPC stream, Polars DataFrame |
| **Memory-mapped I/O** | `MmapTickReader` for binary tick files |
| **Custom computation** | `BarComputation` trait for non-standard OHLCV logic |
| **Python bindings** | Full API via maturin; zero-copy numpy/buffer protocol |

---

## Documentation

Start here:

- **[Handbook Home](./docs/handbook/README.md)** - detailed map for Rust,
  Python, Orderflow integration, and operations.
- **[Core Model](./docs/handbook/01-core-model.md)** - tick layout,
  fixed-point values, bar semantics, filtering, sessions, and adjustments.
- **[Rust API Reference](./docs/handbook/02-rust-api-reference.md)** -
  public types, feature gates, ingestion contracts, and error behavior.
- **[Python Binding Reference](./docs/handbook/03-python-binding-reference.md)** -
  Python classes, zero-copy paths, lifecycle rules, and exceptions.
- **[Orderflow Integration](./docs/handbook/04-orderflow-integration.md)** -
  how tickbar fits under `of_core` without leaking implementation details.
- **[Operational Guide](./docs/handbook/05-operational-guide.md)** -
  replay, mmap files, validation, performance paths, and release checks.

The README is intentionally complete enough for crates.io and PyPI users. The
handbook is the exhaustive reference for public API contracts, examples,
operational notes, and integration boundaries.

---

## Installation

### Rust

```toml
[dependencies]
tickbar = "0.2"
```

Optional feature flags:

```toml
# Core only (no Python bindings)
tickbar = { version = "0.2", default-features = false }

# Arrow IPC export
tickbar = { version = "0.2", features = ["arrow-export"] }

# Polars DataFrame export (+ arrow)
tickbar = { version = "0.2", features = ["polars-export"] }
```

### Python

```bash
pip install tickbar
```

Requires Python 3.11+. Ships pre-built wheels for Linux, macOS, Windows (x86_64, aarch64).

---

## Quick start

### Rust

```rust
use std::time::Duration;
use tickbar::{
    BarType, Tick, TickAggregator, TickFilter, TradeClassifier, TradeDirection, TradingCalendar,
};

// 1-minute time bars
let mut agg = TickAggregator::builder()
    .interval(Duration::from_secs(60))
    .symbol("AAPL")
    .build()?;

// Configure a filter
let filter = TickFilter { max_price: 1_000_000, max_price_change: 10_000, ..Default::default() };
agg.set_filter(filter);

// Push ticks
agg.push_tick(Tick::from_trade(0, 100.0, 1000.0))?;
agg.push_tick(Tick::from_trade(30_000_000_000, 100.5, 500.0))?;

// Peek at in-progress bar
if let Some(bar) = agg.current_bar() {
    println!("Partial bar: {}/{}", bar.open, bar.close);
}

// Finalize
let bars = agg.finalize();

// Classify trades
let mut clf = TradeClassifier::new();
clf.update_quote(Tick::from_quote(0, 99.0, 101.0, 1000.0, 500.0));
let dir = clf.classify_trade(&Tick::from_trade(1, 100.5, 100.0));
assert_eq!(dir, TradeDirection::Buy);
# Ok::<_, tickbar::Error>(())
```

### Python

```python
from tickbar import (
    TickAggregator, Tick, TickFilter, TickBuffer,
    TradingCalendar, TradeClassifier, TradeDirection,
)

# ---- Bar types ----

# Time bars (60-second intervals)
agg = TickAggregator(interval_secs=60)

# Volume bars (bar closes after 10K volume)
agg = TickAggregator.volume_bar(threshold=10000)

# Tick bars (bar closes after 100 trades)
agg = TickAggregator.tick_bar(threshold=100)

# Dollar bars (bar closes after $1M traded)
agg = TickAggregator.dollar_bar(threshold=1_000_000)

# ---- Tick filtering ----

flt = TickFilter()
flt.min_price = 10_000          # reject trades below $1.00
flt.max_price = 10_000_000      # reject trades above $1,000.00
flt.max_volume = 100_000        # reject trades above 10K shares
flt.max_price_change = 50_000   # reject 5%+ price jumps
flt.reject = True               # raise error on violation
agg.set_filter(flt)

# ---- Trading calendar ----

# NYSE regular session: 9:30-16:00 ET = 14:30-21:00 UTC
cal = TradingCalendar([
    (52200_000_000_000, 75600_000_000_000),  # 14:30-21:00 UTC
])
agg.set_calendar(cal)

# ---- Ingest ticks ----

agg.push_tick(Tick(0, 100.0, 1000.0))
agg.push_tick(Tick(30_000_000_000, 100.5, 500.0))

# ---- Streaming iterator ----

for bar in agg:
    print(bar)  # [ts, open, high, low, close, volume, tick_count, vwap]

# ---- In-flight bar ----

partial = agg.current_bar()
if partial:
    print(f"In progress: open={partial[1]}, close={partial[4]}")

# ---- Finalize ----

bars = agg.finalize()
print(f"Total bars: {len(bars)}")
print(bars.to_records())

# ---- Trade classification ----

clf = TradeClassifier()
clf.update_quote(Tick.from_quote(0, 99.0, 101.0))
direction = clf.classify_trade(Tick(1, 100.5, 100.0))
print(direction)  # TradeDirection.Buy
```

---

## Architecture

tickbar processes market data through five stages:

```mermaid
flowchart TD
    raw["Raw ticks<br/>trade and quote events"]
    ingest["Tick ingestion<br/>Tick, TickBuffer, MmapTickReader"]
    preprocess["Pre-processing<br/>TickFilter, TradeClassifier, TradingCalendar"]
    aggregate["Bar aggregation<br/>Time, volume, tick-count, dollar bars<br/>Gap filling, VWAP, custom computation"]
    output["Output<br/>BarSeries, CSV, Arrow, Polars, resample"]
    adjust["Adjustments<br/>Splits and dividends"]

    raw --> ingest --> preprocess --> aggregate --> output --> adjust
```

### Tick data model

All ticks are 32 bytes regardless of type:

```rust
#[repr(C)]
pub struct Tick {
    pub timestamp_nanos: i64,   // UTC nanoseconds since epoch
    pub price: i64,             // trade price or bid price (fixed-point)
    pub volume: i64,            // trade volume or ask price (fixed-point)
    pub flags: u64,             // bit 0: 0 = trade, 1 = quote
}
```

**Trade ticks**: `price` = execution price, `volume` = shares traded.

**Quote ticks**: `price` = bid price, `volume` = ask price. Access via `.bid()`, `.ask()`, `.spread()`, `.mid_price()`. Bid/ask sizes are not stored (saves 8 bytes per tick).

---

## API Reference

### Public contract summary

| Contract | Status |
|----------|--------|
| `Tick` layout | 32-byte `repr(C)` record: `i64, i64, i64, u64` |
| `Bar` layout | 48-byte `repr(C)` OHLCV record |
| Checked ingestion | rejects timestamps older than the previous accepted tick |
| Unchecked ingestion | caller guarantees sorting and validation |
| Quote ticks | bid in `price`, ask in `volume`, zero effective volume |
| Gap bars | previous close is carried into OHLC/VWAP with zero volume |
| Feature gates | Arrow, Polars, and Python are optional integrations |

### Rust — Core types

| Type | Purpose |
|------|---------|
| [`Tick`](https://docs.rs/tickbar/latest/tickbar/struct.Tick.html) | 32-byte market data tick (trade or quote) |
| [`TickBuffer`](https://docs.rs/tickbar/latest/tickbar/struct.TickBuffer.html) | Batched tick ingestion with ordering/duplicate policies |
| [`MmapTickReader`](https://docs.rs/tickbar/latest/tickbar/struct.MmapTickReader.html) | Memory-mapped iterator over binary tick files |
| [`BarAggregator`](https://docs.rs/tickbar/latest/tickbar/struct.BarAggregator.html) | Low-level aggregation engine |
| [`TickAggregator`](https://docs.rs/tickbar/latest/tickbar/struct.TickAggregator.html) | High-level aggregator with builder API |
| [`TickAggregatorBuilder`](https://docs.rs/tickbar/latest/tickbar/struct.TickAggregatorBuilder.html) | Builder for `TickAggregator` |
| [`BarSeries`](https://docs.rs/tickbar/latest/tickbar/struct.BarSeries.html) | Completed OHLCV bar series for one symbol |
| [`Bar`](https://docs.rs/tickbar/latest/tickbar/struct.Bar.html) | Single OHLCV bar (48 bytes, `repr(C)`) |
| [`BarBuilder`](https://docs.rs/tickbar/latest/tickbar/struct.BarBuilder.html) | Mutable bar accumulator with VWAP tracking |

### Rust — Pre-processing

| Type | Purpose |
|------|---------|
| [`TickFilter`](https://docs.rs/tickbar/latest/tickbar/struct.TickFilter.html) | Outlier rejection: price bounds, volume cap, max jump |
| [`TradeClassifier`](https://docs.rs/tickbar/latest/tickbar/struct.TradeClassifier.html) | Lee-Ready buy/sell classification |
| [`TradeDirection`](https://docs.rs/tickbar/latest/tickbar/enum.TradeDirection.html) | Classified trade direction (Buy/Sell/Neutral) |
| [`TradingCalendar`](https://docs.rs/tickbar/latest/tickbar/struct.TradingCalendar.html) | Market session definition with binary search |
| [`TimeAlignment`](https://docs.rs/tickbar/latest/tickbar/enum.TimeAlignment.html) | UTC or custom-offset bar boundary alignment |
| [`BarType`](https://docs.rs/tickbar/latest/tickbar/enum.BarType.html) | Bar completion strategy (Time/Volume/TickCount/Dollar) |
| [`BarComputation`](https://docs.rs/tickbar/latest/tickbar/trait.BarComputation.html) | Custom OHLCV computation strategy trait |

### Rust — Output

| Type | Purpose |
|------|---------|
| [`BarSeries::to_csv()`](https://docs.rs/tickbar/latest/tickbar/struct.BarSeries.html#method.to_csv) | CSV export (via `csv` crate) |
| [`BarSeries::to_arrow()`](https://docs.rs/tickbar/latest/tickbar/struct.BarSeries.html#method.to_arrow) | Arrow `RecordBatch` (feature: `arrow-export`) |
| [`BarSeries::to_polars()`](https://docs.rs/tickbar/latest/tickbar/struct.BarSeries.html#method.to_polars) | Polars `DataFrame` (feature: `polars-export`) |
| [`BarSeries::resample()`](https://docs.rs/tickbar/latest/tickbar/struct.BarSeries.html#method.resample) | Resample to wider interval (integer multiple) |
| [`BarSeries::apply_adjustments()`](https://docs.rs/tickbar/latest/tickbar/struct.BarSeries.html#method.apply_adjustments) | Split/dividend backward adjustment |
| [`AggregatorConfig`](https://docs.rs/tickbar/latest/tickbar/struct.AggregatorConfig.html) | Batch processing configuration |
| [`aggregate_parallel()`](https://docs.rs/tickbar/latest/tickbar/fn.aggregate_parallel.html) | Multi-symbol parallel aggregation |

### Python — Classes

| Class | Purpose |
|-------|---------|
| `Tick` | Market data tick (trade or quote) |
| `TradeDirection` | Buy, Sell, Neutral |
| `TradeClassifier` | Lee-Ready buy/sell classifier |
| `TickFilter` | Outlier rejection configuration |
| `TradingCalendar` | Market session definition |
| `TickAggregator` | Streaming tick-to-bar aggregator |
| `BarSeries` | Completed OHLCV bar container |

---

## Advanced usage

### Multi-type bars

```rust
use std::time::Duration;
use tickbar::{BarAggregator, BarType, TimeAlignment};

// Volume bars: close after 10,000 shares
let mut agg = BarAggregator::with_bar_type(
    BarType::Volume { threshold: 10_000 },
    TimeAlignment::UTC, 8, 0, false, false, 0,
);

// Tick bars: close after 100 trades
let mut agg = BarAggregator::with_bar_type(
    BarType::TickCount { threshold: 100 },
    TimeAlignment::UTC, 8, 0, false, false, 0,
);

// Dollar bars: close after $1,000,000 traded
let mut agg = BarAggregator::with_bar_type(
    BarType::Dollar { threshold: 1_000_000 },
    TimeAlignment::UTC, 8, 0, false, false, 0,
);
```

### Custom bar computation

```rust
use tickbar::{BarBuilder, BarComputation};

/// Median-price bars: use (H+L)/2 instead of close
#[derive(Debug)]
struct MedianBar;

impl BarComputation for MedianBar {
    fn update(&self, bar: &mut BarBuilder, price: i64, volume: i64) {
        // Delegate to standard update, then override close
        bar.update(price, volume);
    }
}
```

### Gap filling and forward fill

When `fill_gaps(true)` is enabled for time bars, missing windows are emitted as
zero-volume bars. The current implementation carries the previous completed
close into open, high, low, close, and VWAP for generated gap bars.

```rust
let mut agg = TickAggregator::builder()
    .interval(Duration::from_secs(60))
    .fill_gaps(true)
    .forward_fill(true)   // retained config switch; gap bars carry last close
    .symbol("SPY")
    .build()?;
```

### Exchange-aligned time boundaries

```rust
use tickbar::{TickAggregatorBuilder, TimeAlignment};

// Align to NYSE: UTC-5 (300 min = 5 hours)
let mut agg = TickAggregator::builder()
    .interval(Duration::from_secs(900))   // 15 minutes
    .align_to_exchange(-5 * 3600)          // UTC → ET offset
    .symbol("AAPL")
    .build()?;
```

### Process quotes with trade classification

```rust
use tickbar::{Tick, TradeClassifier, TradeDirection};

let mut clf = TradeClassifier::new();

// Feed quote updates
clf.update_quote(Tick::from_quote(0, 99.50, 100.50, 1000.0, 500.0));  // mid = 100.00

// Trades are classified against the most recent quote
let trade1 = Tick::from_trade(1, 100.50, 1000.0);   // at ask → Buy
assert_eq!(clf.classify_trade(&trade1), TradeDirection::Buy);

let trade2 = Tick::from_trade(2, 99.50, 500.0);      // at bid → Sell
assert_eq!(clf.classify_trade(&trade2), TradeDirection::Sell);
```

### Batch processing from parallel arrays

```rust
let mut agg = BarAggregator::new(60_000_000_000, TimeAlignment::UTC, 8, 0, false, false, 0);

let timestamps = vec![0i64, 30_000_000_000, 60_000_000_000];
let prices = vec![100_000_000i64, 100_500_000, 101_000_000];
let volumes = vec![1000i64, 500, 750];

agg.ingest_from_arrays(&timestamps, &prices, &volumes);
```

### Python zero-copy with numpy

```python
import numpy as np
from tickbar import TickAggregator

agg = TickAggregator(interval_secs=60)

ts = np.array([0, 30_000_000_000, 60_000_000_000], dtype=np.int64)
price = np.array([100_000_000, 100_500_000, 101_000_000], dtype=np.int64)
volume = np.array([1000, 500, 750], dtype=np.int64)

agg.push_from_numpy(ts, price, volume)
```

### Python Arrow IPC export

```python
import pyarrow.ipc as ipc
import io
from tickbar import TickAggregator, Tick

agg = TickAggregator(interval_secs=60)
agg.push_tick(Tick(0, 100.0, 1000.0))
agg.push_tick(Tick(30_000_000_000, 100.5, 500.0))

bars = agg.finalize()
bytes = bars.to_arrow_bytes()

# Read back with PyArrow
reader = ipc.open_stream(io.BytesIO(bytes))
table = reader.read_all()
print(table.to_pandas())
```

---

## Error handling

### Rust errors

```rust
pub enum Error {
    /// Ticks must be ingested in chronological order.
    OutOfOrderTick { current: i64, previous: i64 },

    /// Invalid configuration (bad interval, missing symbol, filtered tick).
    InvalidConfiguration(String),
}
```

All fallible methods return `Result<_, tickbar::Error>`. Use `?` to propagate:

```rust
agg.push_tick(tick)?;  // returns OutOfOrderTick on sequence violation
```

### Python errors

| Exception | When |
|-----------|------|
| `ValueError` | Invalid configuration, out-of-order ticks |
| `RuntimeError` | Arrow export failure (requires `arrow-export` feature) |

---

## Performance

| Path | Throughput | vs pandas |
|------|-----------|-----------|
| Rust native (single-threaded) | **119M ticks/s** | 25× |
| Python PEP 3118 buffer | **6.7M ticks/s** | 1.4× |
| Python numpy `__array_interface__` | **6.5M ticks/s** | 1.4× |
| pandas `resample().agg()` baseline | 4.7M ticks/s | 1.0× |

Benchmarked on 70,000 synthetic ticks across 9 S&P 500 tickers via yfinance (Intel i7-12700H, DDR5-4800).

### Zero-copy data paths

| Method | Copy count | Best for |
|--------|-----------|----------|
| `push_from_buffer()` / `push_from_numpy()` | 0 copies | Production pipelines with numpy/pandas |
| `push_from_bytes()` | 0 copies | Binary file replay |
| `push_from_arrays()` | 1 copy (into Vec) | Ad-hoc scripts |
| `push_tick()` / `push_ticks()` | N copies | Interactive use |

---

## Orderflow integration

`tickbar` is designed to be used by the Orderflow Rust project under `of_core`.
The intended boundary is narrow:

- `tickbar` owns tick-to-bar mechanics, compact tick layout, bar completion
  policies, and high-throughput ingestion.
- `of_core` owns `SymbolId`, canonical trade/book schemas, data-quality flags,
  strategy-facing analytics, and any `CompletedBar` wrapper type.
- Orderflow should keep the dependency feature-gated so core analytics users do
  not pay for tickbar unless they opt in.
- Arrow, Polars, Python, and exchange-calendar interpretation should remain
  optional layers, not assumptions in the `of_core` base contract.

See [Orderflow Integration](./docs/handbook/04-orderflow-integration.md) for
the full mapping from `TradePrint` and top-of-book data into `Tick`, and from
`Bar` into Orderflow-facing completed bar snapshots.

[crates-badge]: https://img.shields.io/crates/v/tickbar.svg
[crates-url]: https://crates.io/crates/tickbar
[pypi-badge]: https://img.shields.io/pypi/v/tickbar.svg?refresh=1
[pypi-url]: https://pypi.org/project/tickbar/

