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:
- Update the internal
BarBuilderwith the incoming price and size. - Check your custom condition (price delta, volatility threshold, etc.).
- 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, andTickImbalanceBarAggregatorprovide specific emission rules. - To aggregate ticks into bars with custom rules, subclass
BarAggregatorand override the_apply_updatemethod 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 theTraderAPI provides high-level methods likerequest_aggregated_barsfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →