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

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.

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.

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.

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:

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:

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.

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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →