Bar Series and Bars
Everything in ta4j is grounded in a BarSeries: an ordered list of Bar objects containing OHLCV (open, high, low, close, volume) data for consistent time spans. Indicators, rules, and strategies read from the series; backtests and live trading both operate on it.
Bars 101
Each Bar represents the market action during a time window and captures:
- Open/High/Low/Close prices
- Volume (or amount, depending on the bar builder)
- Time period (
Duration) - Begin and end timestamps (stored as
Instantsince 0.18)
Mutating retained bars
A bar retained by a series must publish its mutations to the retaining series, or must not be mutated after a newer bar is appended.
BaseBar and BaseRealtimeBar publish their supported mutations when retained by BaseBarSeries or ConcurrentBarSeries. This includes changes to a historical bar that is still retained, so these built-in bars can be updated after a newer bar is appended.
A custom Bar that does not publish mutations may be updated while it is the terminal (latest) bar. Once a newer bar is appended, leave that custom bar unchanged. CachedIndicator fingerprints non-publishing bars only while they are terminal; its former-terminal promotion recheck does not keep watching historical bars. A later unannounced mutation can therefore leave cached indicator values stale.
See the Bar and BarSeries.addBar Javadoc for the API contract, and indicator cache policies for cache behavior. Individual indicators may provide stronger mutation detection; this general contract does not remove those guarantees.
Building series for backtesting
BarSeries series = new BaseBarSeriesBuilder()
.withName("btc_daily")
.build();
Instant endTime = Instant.parse("2024-01-02T00:00:00Z");
series.addBar(series.barBuilder()
.timePeriod(Duration.ofDays(1))
.endTime(endTime)
.openPrice(105.42)
.highPrice(112.99)
.lowPrice(104.01)
.closePrice(111.42)
.volume(1337)
.build());
Options to consider:
- Builders –
TimeBarBuilder,TickBarBuilder,VolumeBarBuilder, andAmountBarBuilderaggregate trades into bars by elapsed time, tick count, volume, or quote currency size respectively. Their factories can also buildBaseRealtimeBarinstances when you enable therealtimeBarsconstructor flag. - Begin vs. end timestamps – Since 0.18 you can build bars anchored on begin time (
timePeriod+beginTime) or end time. Use whichever matches your data source. - Split series –
series.getSubSeries(start, end)is the standard way to create train/test or walk-forward slices. The resulting series inherits the source max-bar-count setting. - Data Sources – The
ta4j-examplesmodule includes ready-made data sources for loading historical data from APIs (Yahoo Finance, Coinbase) and files (CSV, JSON). See Data Sources for comprehensive documentation.
Aggregating an existing series into alternate bar types
When you already have a time-based series and want alternate bar constructions for analysis, use the aggregator package.
BarAggregator renko = new RenkoBarAggregator(2.0, 2);
BarSeries renkoSeries = new BaseBarSeriesAggregator(renko)
.aggregate(series, "btc_renko");
Common choices:
DurationBarAggregatorfor higher timeframe rollups.RangeBarAggregatorfor fixed price-range bars.VolumeBarAggregatorfor volume-threshold bars.RenkoBarAggregatorfor brick-based trend views.
Notes:
- Range, volume, and Renko aggregators require source bars to be ordered, contiguous, and evenly spaced.
- Range and volume aggregators emit only completed threshold bars by default; use the
onlyFinalBars=falseconstructor when you also want the trailing partial bar. - Renko aggregation uses close-price movement and may emit multiple bricks from one source bar. The first brick emitted from a source bar carries the pending accumulated volume/amount/trade count; additional bricks from the same source bar carry zero volume/amount/trades.
Working with live data
BarSeries liveSeries = new BaseBarSeriesBuilder()
.withName("eth_usd_live")
.build();
Bar latest = liveSeries.barBuilder()
.timePeriod(Duration.ofMinutes(1))
.endTime(Instant.now())
.openPrice(100.0)
.highPrice(101.0)
.lowPrice(99.5)
.closePrice(100.7)
.volume(42)
.build();
liveSeries.addBar(latest);
As intrabar updates stream in:
liveSeries.addPrice(100.9); // updates close + high/low if needed
liveSeries.addTrade(liveSeries.numFactory().numOf(2),
liveSeries.numFactory().numOf(100)); // volume first, then price
liveSeries.addBar(updatedBar, true); // replace the last bar entirely
Best practices:
- Prime the series with recent historical bars before going live so indicators have enough context.
- Set maximum bar count via
setMaximumBarCount(int)to cap memory usage. This turns the series into a moving window—be careful not to reference bars older thangetBeginIndex(). - Threading – For single-threaded scenarios,
BaseBarSeriesis sufficient. For concurrent read/write access, useConcurrentBarSeries(see below).
Moving series and continued indicator state
Released in 0.27.0: a continuously evaluated EMA-family indicator can carry already-consumed history across raw-bar eviction. A fresh instance on the same pruned series instead seeds from retained inputs. The cache-policy guide owns the precise window requirements, mutation handling, explicit-reset exceptions and version boundary.
This example uses the prices, period and capacity from the core EMA continuation regression. Imports: java.time.Duration, java.time.Instant, org.ta4j.core.BarSeries, org.ta4j.core.BaseBarSeriesBuilder, org.ta4j.core.indicators.averages.EMAIndicator, org.ta4j.core.indicators.helpers.ClosePriceIndicator.
BarSeries moving = new BaseBarSeriesBuilder().build();
moving.setMaximumBarCount(4);
EMAIndicator continued = new EMAIndicator(new ClosePriceIndicator(moving), 3);
double[] closes = { 10, 13, 9, 15, 11, 17, 12, 19, 14, 21, 13, 18 };
Instant start = Instant.parse("2026-01-01T00:00:00Z");
for (int i = 0; i < closes.length; i++) {
moving.addBar(moving.barBuilder()
.timePeriod(Duration.ofMinutes(1))
.endTime(start.plusSeconds(60L * (i + 1)))
.openPrice(closes[i]).highPrice(closes[i]).lowPrice(closes[i])
.closePrice(closes[i]).volume(1).build());
continued.getValue(moving.getEndIndex()); // consume every append, including warm-up
}
// Retained absolute indexes are 8..11; continued matches an unbounded replay.
EMAIndicator fresh = new EMAIndicator(new ClosePriceIndicator(moving), 3);
fresh.getValue(10); // NaN: this instance is still warming up from retained index 8
fresh.getValue(11); // 18: first usable seed; not the continued full-history EMA
Keep the established instance if continuation is intended. A missed interval that loses required history, explicit source resets or historical revisions can require reseeding; merely constructing a new indicator does not restore the old chain. Choose capacity for the whole source graph, not only the outer EMA period.
Removed indexes are not historical-data access: some APIs map them to the retained head rather than throw or return NaN. Always restrict raw-bar reads to getBeginIndex()..getEndIndex(). State computed earlier from past bars does not authorize reading evicted bars or bars after the evaluation index.
Concurrent bar series for multi-threaded scenarios
Since 0.22.2, ConcurrentBarSeries provides thread-safe access for scenarios where multiple threads need to read from or write to a bar series simultaneously. This is essential for live trading systems where one thread ingests market data while another thread evaluates strategies.
Creating a concurrent series
ConcurrentBarSeries concurrentSeries = new ConcurrentBarSeriesBuilder()
.withName("btc_usd_concurrent")
.withNumFactory(DecimalNumFactory.getInstance())
.withBarBuilderFactory(new TimeBarBuilderFactory(Duration.ofMinutes(1), true))
.withMaxBarCount(1000) // Optional: limit memory usage
.build();
Streaming trade ingestion
The recommended approach for real-time data feeds is to use ingestTrade() methods, which let the configured BarBuilder handle bar rollovers automatically:
// If the TimeBarBuilderFactory did not set a default period, configure it once:
concurrentSeries.tradeBarBuilder().timePeriod(Duration.ofMinutes(1));
// Simple trade ingestion (no side/liquidity data)
concurrentSeries.ingestTrade(
Instant.now(),
tradeVolume,
tradePrice
);
// With side and liquidity classification (for RealtimeBar support)
concurrentSeries.ingestTrade(
Instant.now(),
tradeVolume,
tradePrice,
RealtimeBar.Side.BUY, // Optional: BUY or SELL
RealtimeBar.Liquidity.TAKER // Optional: MAKER or TAKER
);
The ingestTrade() methods automatically:
- Aggregate trades into bars using the configured
BarBuilder - Handle bar rollovers when thresholds are met (time period, volume, tick count, etc.)
- Update the latest bar in-place or append new bars as needed
- Maintain thread safety throughout the operation
Streaming bar ingestion
For scenarios where you receive pre-aggregated bars (e.g., from WebSocket candle feeds), use ingestStreamingBar():
Bar newBar = concurrentSeries.barBuilder()
.timePeriod(Duration.ofMinutes(1))
.endTime(Instant.now())
.openPrice(100.0)
.highPrice(101.0)
.lowPrice(99.5)
.closePrice(100.7)
.volume(42)
.build();
StreamingBarIngestResult result = concurrentSeries.ingestStreamingBar(newBar);
// result.action() indicates: APPENDED, REPLACED_LAST, or REPLACED_HISTORICAL
// result.index() is the affected series index
The method automatically handles:
- APPENDED: New bar added to the end
- REPLACED_LAST: Latest bar replaced (for corrections)
- REPLACED_HISTORICAL: Historical bar replaced (for reconciliation)
For exchange snapshots that arrive newest-first or contain more than one candle, use ingestStreamingBars(Collection<Bar>). It ignores null payloads, sorts the remaining bars by end time, and returns the per-bar actions in ascending end-time order.
RealtimeBar for side/liquidity analytics
RealtimeBar extends Bar with optional side and liquidity breakdowns, useful for analyzing market microstructure:
// RealtimeBar tracks buy/sell and maker/taker breakdowns
RealtimeBar bar = (RealtimeBar) concurrentSeries.getLastBar();
if (bar.hasSideData()) {
Num buyVolume = bar.getBuyVolume();
Num sellVolume = bar.getSellVolume();
long buyTrades = bar.getBuyTrades();
long sellTrades = bar.getSellTrades();
}
if (bar.hasLiquidityData()) {
Num makerVolume = bar.getMakerVolume();
Num takerVolume = bar.getTakerVolume();
long makerTrades = bar.getMakerTrades();
long takerTrades = bar.getTakerTrades();
}
Note: Side and liquidity data are optional—exchanges may not provide this information for every trade. When unavailable, the corresponding getters return zero values.
Thread safety guarantees
ConcurrentBarSeries uses ReentrantReadWriteLock to provide:
- Multiple concurrent readers: Read operations (getters, indicator evaluation) can proceed in parallel
- Exclusive writers: Write operations (bar ingestion, modifications) are serialized
- Safe access patterns: Read operations are lock-protected, and
getBarData()returns an immutable snapshot (List.copyOf(...))
Example concurrent usage:
// Thread 1: Ingest trades from WebSocket
executorService.submit(() -> {
while (running) {
Trade trade = websocket.receiveTrade();
concurrentSeries.ingestTrade(trade.getTime(), trade.getVolume(), trade.getPrice());
}
});
// Thread 2: Evaluate strategy
executorService.submit(() -> {
while (running) {
int endIndex = concurrentSeries.getEndIndex();
if (strategy.shouldEnter(endIndex, tradingRecord)) {
// Execute trade...
}
}
});
// Thread 3: Calculate indicators
executorService.submit(() -> {
while (running) {
int endIndex = concurrentSeries.getEndIndex();
Num rsiValue = rsiIndicator.getValue(endIndex);
// Use indicator value...
}
});
Serialization
ConcurrentBarSeries supports Java serialization and preserves:
- Bar data
NumFactoryconfigurationBarBuilderFactoryconfiguration- Maximum bar count settings
Transient locks are reinitialized on deserialization, and the trade bar builder is recreated lazily on the next ingestion call.
Rationale Notes (2026-03-06)
- Added aggregator guidance because ta4j now includes
RangeBarAggregator,VolumeBarAggregator, andRenkoBarAggregator(org.ta4j.core.aggregator.*), introduced in commit3d6ca7b7. - Clarified sub-series behavior to match
BaseBarSeries#getSubSeries(...)andConcurrentBarSeries#getSubSeries(...), which carry forward max-bar-count behavior. - Clarified concurrency wording to match
ConcurrentBarSeries#getBarData()(immutable snapshot return) and lock usage (ReentrantReadWriteLockinConcurrentBarSeries), added in commitdc759436.
Rationale Notes (2026-04-27)
- Refined threshold aggregation guidance against
RangeBarAggregator,VolumeBarAggregator,RenkoBarAggregator, andBarAggregator#requireEvenIntervals(...), including the 0.22.4 since-tag alignment from commitf8966331. - Updated concurrent ingestion examples for
TimeBarBuilderFactory(Duration, boolean)andConcurrentBarSeries#ingestStreamingBars(...), from the live series work in commitdc759436.
When to use ConcurrentBarSeries
Use ConcurrentBarSeries when:
- Multiple threads need concurrent read access (e.g., parallel strategy evaluation)
- One thread ingests data while others read (e.g., live trading bots)
- You need thread-safe bar ingestion from WebSocket feeds
- You want to leverage side/liquidity analytics with
RealtimeBar
Use BaseBarSeries when:
- Single-threaded access is sufficient (most backtesting scenarios)
- You want maximum performance without synchronization overhead
- You’re working with historical data that doesn’t change
Choosing a Num representation
BarSeries delegates number creation through a NumFactory. Use DecimalNumFactory for high precision (default) or DoubleNumFactory for performance-sensitive scenarios. See Num for guidance plus tips on mixing integer-based quantities (contracts) with price-based values.
Troubleshooting data issues
- Missing timestamps –
BarSeriesUtils.findMissingBars(series, false)surfaces potential gaps so you can backfill or ignore known market closures. - Revised data – Exchanges occasionally restate candles; call
BarSeriesUtils.replaceBarIfChanged(series, newBar)to swap the affected bar in place. - Irregular intervals – Aggregate trades into constant-duration bars using
TimeBarBuilder/VolumeBarBuilderorBarSeriesUtils.aggregateBars(...)to keep indicators accurate. - Currency conversions – Preprocess your data so a single
BarSeriesremains homogeneous (one instrument / quote currency). If you need spreads between instruments, build multiple series and feed each indicator the relevant one.