Documentation, examples and further information of the ta4j project
This project is maintained by ta4j Organization
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.
Each Bar represents the market action during a time window and captures:
Duration)Instant since 0.18)Bars are immutable once added to a series (except for the latest bar which may be updated in-place while a period is in progress).
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:
TimeBarBuilder, TickBarBuilder, VolumeBarBuilder, and AmountBarBuilder aggregate trades into bars by elapsed time, tick count, volume, or quote currency size respectively. Their factories can also build BaseRealtimeBar instances when you enable the realtimeBars constructor flag.timePeriod + beginTime) or end time. Use whichever matches your data source.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.ta4j-examples module includes ready-made data sources for loading historical data from APIs (Yahoo Finance, Coinbase) and files (CSV, JSON). See Data Sources for comprehensive documentation.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:
DurationBarAggregator for higher timeframe rollups.RangeBarAggregator for fixed price-range bars.VolumeBarAggregator for volume-threshold bars.RenkoBarAggregator for brick-based trend views.Notes:
onlyFinalBars=false constructor when you also want the trailing partial bar.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:
setMaximumBarCount(int) to cap memory usage. This turns the series into a moving window—be careful not to reference bars older than getBeginIndex().BaseBarSeries is sufficient. For concurrent read/write access, use ConcurrentBarSeries (see below).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.
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();
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:
BarBuilderFor 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:
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 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.
ConcurrentBarSeries uses ReentrantReadWriteLock to provide:
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...
}
});
ConcurrentBarSeries supports Java serialization and preserves:
NumFactory configurationBarBuilderFactory configurationTransient locks are reinitialized on deserialization, and the trade bar builder is recreated lazily on the next ingestion call.
RangeBarAggregator, VolumeBarAggregator, and RenkoBarAggregator (org.ta4j.core.aggregator.*), introduced in commit 3d6ca7b7.BaseBarSeries#getSubSeries(...) and ConcurrentBarSeries#getSubSeries(...), which carry forward max-bar-count behavior.ConcurrentBarSeries#getBarData() (immutable snapshot return) and lock usage (ReentrantReadWriteLock in ConcurrentBarSeries), added in commit dc759436.RangeBarAggregator, VolumeBarAggregator, RenkoBarAggregator, and BarAggregator#requireEvenIntervals(...), including the 0.22.4 since-tag alignment from commit f8966331.TimeBarBuilderFactory(Duration, boolean) and ConcurrentBarSeries#ingestStreamingBars(...), from the live series work in commit dc759436.Use ConcurrentBarSeries when:
RealtimeBarUse BaseBarSeries when:
Num representationBarSeries 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.
BarSeriesUtils.findMissingBars(series, false) surfaces potential gaps so you can backfill or ignore known market closures.BarSeriesUtils.replaceBarIfChanged(series, newBar) to swap the affected bar in place.TimeBarBuilder/VolumeBarBuilder or BarSeriesUtils.aggregateBars(...) to keep indicators accurate.BarSeries remains homogeneous (one instrument / quote currency). If you need spreads between instruments, build multiple series and feed each indicator the relevant one.