# How to Aggregate Ticks into Bars with Custom Rules in Nautilus Trader

> Aggregate ticks into bars with custom rules in Nautilus Trader using built-in classes or by subclassing BarAggregator. Easily implement your own emission logic for powerful trading analytics.

- Repository: [Nautech Systems/nautilus_trader](https://github.com/nautechsystems/nautilus_trader)
- Tags: how-to-guide
- Published: 2026-02-16

---

**You can aggregate ticks into bars with custom rules in Nautilus Trader by using built-in aggregator classes or subclassing `BarAggregator` to implement your own emission logic, then feeding raw ticks through the `handle_trade_tick` method.**

The `nautechsystems/nautilus_trader` repository provides a high-performance pipeline for transforming raw market ticks into aggregated bars (candlesticks) using both predefined rules and fully custom logic. This article explains how to leverage the `BarSpecification`, `BarBuilder`, and `BarAggregator` classes to create bars based on time, volume, tick count, imbalance, or your own proprietary conditions.

## Understanding the Bar Aggregation Pipeline

Before implementing custom rules, it is essential to understand the three core components that handle tick-to-bar conversion in `nautilus_trader/data/aggregation.pyx` and `nautilus_trader/model/data.pyx`.

### Bar Specification and Type Definitions

The `BarSpecification` class defines *what* triggers a bar and *how* OHLCV values are calculated. It specifies the aggregation method (tick, volume, time, etc.), the step size (threshold), and the price type to use.

Located in `nautilus_trader/model/data.pyx`, the `BarSpecification` is typically combined with an `InstrumentId` to create a `BarType`, which uniquely identifies the bar stream.

### The Bar Builder Utility

The `BarBuilder` class (`nautilus_trader/data/aggregation.pyx`) is a low-level utility that incrementally updates the open, high, low, close, and volume for the current bar. It handles the arithmetic of updating highs and lows and finalizing bar values when emission is triggered.

### The Bar Aggregator Base Class

`BarAggregator` (`nautilus_trader/data/aggregation.pyx`) orchestrates the builder, tracks state (`historical_mode`, `is_running`), and dispatches finished bars to a user-provided handler. It defines the interface that all concrete aggregators must implement, specifically the `_apply_update` method where custom emission logic resides.

## Using Built-In Aggregators to Aggregate Ticks into Bars

The repository ships with several concrete aggregators in `nautilus_trader/data/aggregation.pyx` that cover common rule sets. You can instantiate these directly and feed them ticks manually or through the data engine.

### Tick-Count Bars (Fixed Number of Ticks)

The `TickBarAggregator` emits a bar after receiving a fixed number of ticks, defined by the `step` parameter in the `BarSpecification`.

```python
from nautilus_trader.model.identifiers import InstrumentId
from nautilus_trader.model.data import BarSpecification, BarAggregation, BarType, PriceType
from nautilus_trader.data.aggregation import TickBarAggregator

instrument_id = InstrumentId("BTCUSDT", "BINANCE")

# Obtain instrument from catalog or venue

instrument = ...

spec = BarSpecification(
    step=100,
    aggregation=BarAggregation.TICK,
    price_type=PriceType.LAST
)
bar_type = BarType(instrument_id, spec, AggregationSource.INTERNAL)

def on_bar(bar):
    print(f"Bar completed: {bar}")

aggregator = TickBarAggregator(instrument, bar_type, on_bar)

# Feed ticks

for tick in incoming_trade_ticks:
    aggregator.handle_trade_tick(tick)

```

According to the source code in `nautilus_trader/data/aggregation.pyx`, `TickBarAggregator.handle_trade_tick` triggers bar emission when the internal tick count reaches `self.bar_type.spec.step`.

### Volume and Value Bars

`VolumeBarAggregator` accumulates trade volume until the threshold is reached, while `ValueBarAggregator` does the same for notional value. These are essential for analyzing market activity independent of time.

```python
from nautilus_trader.data.aggregation import VolumeBarAggregator

spec = BarSpecification(
    step=1_000,
    aggregation=BarAggregation.VOLUME,
    price_type=PriceType.LAST
)
bar_type = BarType(instrument_id, spec, AggregationSource.INTERNAL)

aggregator = VolumeBarAggregator(instrument, bar_type, on_bar)

```

The `_apply_update` method in `VolumeBarAggregator` loops over incoming size and emits a bar each time the accumulated volume crosses the step threshold.

### Imbalance and Runs Bars

For more advanced market microstructure analysis, `TickImbalanceBarAggregator` emits bars based on the cumulative imbalance of buyer-initiated versus seller-initiated ticks. Similarly, `TickRunsBarAggregator` triggers after a consecutive run of ticks with the same aggressor side.

```python
from nautilus_trader.data.aggregation import TickImbalanceBarAggregator

spec = BarSpecification(
    step=50,
    aggregation=BarAggregation.TICK_IMBALANCE,
    price_type=PriceType.LAST
)
bar_type = BarType(instrument_id, spec, AggregationSource.INTERNAL)

aggregator = TickImbalanceBarAggregator(instrument, bar_type, on_bar)

```

The internal logic tracks `_imbalance` and calls `_build_now_and_send()` when `abs(_imbalance) >= step`.

## Implementing Custom Bar Aggregation Rules

When built-in aggregators do not match your strategy requirements, you can aggregate ticks into bars with custom rules by subclassing `BarAggregator` and overriding the `_apply_update` method.

### Subclassing BarAggregator

The base class handles builder management, historical mode, and handler dispatch. Your subclass only needs to implement the emission logic:

1. Update the internal `BarBuilder` with the incoming price and size.
2. Check your custom condition (price delta, volatility threshold, etc.).
3. Call `self._build_now_and_send()` when the condition is met.

### Example: Price-Change Threshold Aggregator

The following example implements a custom rule that emits a bar when the absolute price change from the previous bar's close exceeds a user-defined threshold:

```python
from nautilus_trader.data.aggregation import BarAggregator
from libc.math cimport fabs

class PriceChangeBarAggregator(BarAggregator):
    """
    Emits a bar when the absolute price change from the previous bar's close
    exceeds the price_change_threshold (in raw price units).
    """
    def __init__(self, instrument, bar_type, handler, price_change_threshold):
        super().__init__(instrument, bar_type, handler)
        self._threshold = price_change_threshold
        self._last_close = None

    cdef void _apply_update(self, Price price, Quantity size, uint64_t ts_init):
        # Update the standard OHLCV builder

        self._builder.update(price, size, ts_init)

        # Skip until we have at least one completed bar to establish a baseline

        if self._builder.count == 1:
            return

        if self._last_close is not None:
            diff = fabs(price._mem.raw - self._last_close._mem.raw)
            if diff >= self._threshold:
                self._build_now_and_send()
                self._last_close = self._builder._close
        else:
            self._last_close = self._builder._close

```

Register this custom aggregator exactly as you would a built-in one. The custom rule lives entirely within `_apply_update`, while the base class handles all builder state and bar emission logistics.

## Integrating Aggregators with the Data Engine

While manual tick feeding works for backtesting or specialized use cases, production strategies typically rely on the `DataEngine` to automatically route market data to the appropriate aggregators.

### Manual Tick Feeding

For unit tests or custom data pipelines, instantiate any `BarAggregator` subclass and call `handle_trade_tick` (or `handle_quote_tick` for quote-based bars) directly:

```python
aggregator.handle_trade_tick(trade_tick)

```

When the internal condition is met, the aggregator builds a `Bar` and invokes your handler callback.

### Automatic Subscription via Trader API

The high-level `Trader` API automates aggregator creation and tick subscription. When you request aggregated bars, the `DataEngine` creates the appropriate aggregator based on the `BarType` and routes incoming `TradeTick` objects to it via `_handle_trade_tick`.

```python
from nautilus_trader.trading.trader import Trader

trader = Trader(...)

# Request 5-minute bars - the engine handles aggregator creation

trader.request_aggregated_bars(
    instrument_id=instrument_id,
    bar_type=BarType(
        instrument_id,
        BarSpecification(
            step=5,
            aggregation=BarAggregation.MINUTE,
            price_type=PriceType.LAST
        ),
        AggregationSource.INTERNAL
    ),
    handler=on_bar,
    update_subscriptions=True  # Automatically subscribes to raw ticks

)

```

According to the source code in `nautilus_trader/data/engine.pyx`, the `_handle_trade_tick` method forwards each raw tick to the appropriate aggregator instance based on the instrument and bar type configuration.

## Summary

- **BarSpecification** (`nautilus_trader/model/data.pyx`) defines the aggregation method (tick, volume, time, imbalance) and step threshold that determines when a bar closes.
- **BarBuilder** (`nautilus_trader/data/aggregation.pyx`) incrementally updates OHLCV values as individual ticks arrive.
- **BarAggregator** is the abstract base class that manages builder state and dispatches completed bars to handlers; concrete implementations like `TickBarAggregator`, `VolumeBarAggregator`, and `TickImbalanceBarAggregator` provide specific emission rules.
- To aggregate ticks into bars with custom rules, subclass `BarAggregator` and override the `_apply_update` method to implement your specific threshold logic, then call `_build_now_and_send()` when your condition is met.
- The `DataEngine` (`nautilus_trader/data/engine.pyx`) automatically routes ticks to registered aggregators, while the `Trader` API provides high-level methods like `request_aggregated_bars` for automatic subscription management.

## Frequently Asked Questions

### How do I choose between built-in aggregators and creating a custom one?

Use built-in aggregators like `TickBarAggregator`, `VolumeBarAggregator`, or `TickImbalanceBarAggregator` when your strategy relies on standard thresholds such as fixed tick counts, volume accumulation, or order-flow imbalance. Create a custom subclass of `BarAggregator` only when you need specialized logic—such as volatility-based thresholds, range bars, or multi-factor conditions—that is not covered by the existing implementations in `nautilus_trader/data/aggregation.pyx`.

### What is the performance impact of using custom aggregation rules?

The aggregation pipeline in Nautilus Trader is implemented in Cython (`.pyx` files) for high-performance execution. When you subclass `BarAggregator` and override `_apply_update`, your custom logic runs within the same optimized loop that processes market data. To maintain performance, minimize Python object creation inside `_apply_update` and use primitive types where possible; the base class handles all bar construction and memory management efficiently via the `BarBuilder`.

### Can I mix different aggregation types for the same instrument?

Yes. The `DataEngine` maintains separate aggregator instances for each unique `BarType`, which combines an `InstrumentId` with a specific `BarSpecification`. You can subscribe to 100-tick bars, 5-minute time bars, and custom imbalance bars for the same instrument simultaneously. Each aggregator receives the same raw tick stream via `_handle_trade_tick` but maintains independent state and emits bars according to its own specification.

### How do I handle historical data when using custom aggregators?

Enable historical mode by calling `set_historical_mode(True, handler)` on your aggregator instance before replaying historical ticks. In this mode, the aggregator buffers incoming data but does not emit partial bars during the backfill period, preventing incomplete bars from contaminating your strategy. Once historical replay completes, disable historical mode to resume real-time bar emission. This functionality is managed by the base `BarAggregator` class in `nautilus_trader/data/aggregation.pyx`.