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 the new AmountBarBuilder aggregate trades into bars by elapsed time, tick count, volume, or quote currency size respectively.timePeriod + beginTime) or end time. Use whichever matches your data source.series.getSubSeries(start, end) and series.split(...) are handy for walk-forward tests or training/testing splits.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.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(100),
liveSeries.numFactory().numOf(2)); // updates volume + close
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(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:
// 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:
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:
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.
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.