A BarSeries is the foundation of every ta4j workflow. It holds a time-ordered sequence of Bar objects, where each bar represents aggregated market data over a fixed time period (e.g., 1 minute, 1 day). Every indicator and strategy in ta4j reads from a BarSeries.
What is a Bar?
A Bar is a single time-period snapshot of market activity. It carries:
| Field | Method | Description |
|---|
| Open price | getOpenPrice() | First trade price in the period |
| High price | getHighPrice() | Highest trade price in the period |
| Low price | getLowPrice() | Lowest trade price in the period |
| Close price | getClosePrice() | Last trade price in the period |
| Volume | getVolume() | Total units traded |
| Amount | getAmount() | Total notional traded (price × volume) |
| Trades | getTrades() | Number of individual trades |
| Begin time | getBeginTime() | Period start (UTC Instant) |
| End time | getEndTime() | Period end (UTC Instant) |
| Duration | getTimePeriod() | Duration of the bar period |
Bar also provides two convenience predicates: isBullish() (close > open) and isBearish() (close < open).
All numeric fields return a Num — ta4j’s abstraction over the underlying number type. Two concrete implementations are available: DecimalNum (backed by BigDecimal) and DoubleNum (backed by Java double).
Creating a BarSeries
Use BaseBarSeriesBuilder to construct a series:
import org.ta4j.core.BarSeries;
import org.ta4j.core.BaseBarSeriesBuilder;
// Empty series — bars are added incrementally
BarSeries series = new BaseBarSeriesBuilder()
.withName("BTC-USD")
.build();
Adding bars
Once you have a series, add bars one at a time using addBar(). Each new bar’s end time must be strictly after the previous bar’s end time.
import java.time.Duration;
import java.time.Instant;
import org.ta4j.core.Bar;
import org.ta4j.core.num.DecimalNumFactory;
DecimalNumFactory nf = DecimalNumFactory.getInstance();
// Build a bar covering a 1-day period
Bar bar = series.barBuilder()
.timePeriod(Duration.ofDays(1))
.endTime(Instant.parse("2024-01-02T00:00:00Z"))
.openPrice(nf.numOf(42000))
.highPrice(nf.numOf(43500))
.lowPrice(nf.numOf(41800))
.closePrice(nf.numOf(43200))
.volume(nf.numOf(1500))
.build();
series.addBar(bar);
To replace the most recent bar (useful when an exchange streams live partial bars within the same period), pass true as the second argument:
series.addBar(updatedBar, true);
You can also update the current bar in-place without building a new one:
// Adds a trade and adjusts close/high/low
series.addTrade(volumeNum, priceNum);
// Updates only the close (and high/low if needed)
series.addPrice(priceNum);
Limiting memory with maximumBarCount
For live trading, you generally do not need to keep the entire price history in memory. Set a maximum bar count and ta4j will automatically evict the oldest bars as new ones arrive:
BarSeries series = new BaseBarSeriesBuilder()
.withName("AAPL")
.build();
// Keep only the last 500 bars in memory
series.setMaximumBarCount(500);
Bar indices do not reset when bars are evicted. If you add 1000 bars and apply a maximumBarCount of 500, the series begins at index 500 and ends at index 999. Accessing evicted indices transparently returns the first still-present bar.
ConcurrentBarSeries for live trading
BaseBarSeries is not thread-safe. When a data feed thread and a strategy evaluation thread share the same series, use ConcurrentBarSeries instead. It wraps all reads and writes in a ReentrantReadWriteLock.
import org.ta4j.core.ConcurrentBarSeries;
import org.ta4j.core.ConcurrentBarSeriesBuilder;
import org.ta4j.core.bars.TimeBarBuilderFactory;
import java.time.Duration;
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder()
.withName("BTC-USD")
.withBarBuilderFactory(new TimeBarBuilderFactory(Duration.ofMinutes(1)))
.build();
Streaming trade ingestion
ConcurrentBarSeries.ingestTrade() accepts raw tick-level trades and automatically aggregates them into OHLCV bars. When a trade falls outside the current bar’s period, the builder closes the current bar and opens a new one — skipping empty periods rather than inserting gap bars.
import java.time.Instant;
Instant t0 = Instant.parse("2024-01-01T10:05:00Z");
// First trade — creates the 10:05 bar
series.ingestTrade(t0, 1, 100.0);
// Second trade 2.5 minutes later — closes the 10:05 bar,
// skips the empty 10:06 bar, opens the 10:07 bar
series.ingestTrade(t0.plusSeconds(150), 2, 105.0);
System.out.println(series.getBarCount()); // 2
System.out.println(series.getBar(1).getBeginTime()); // 2024-01-01T10:07:00Z
Time gaps produce no empty bars. If your downstream code expects a contiguous series (e.g., for fixed-period SMA calculations), reconcile and backfill OHLCV data before ingestion.
Choosing a NumFactory
Every BarSeries has a NumFactory that governs the numeric type used for all calculations. You set it at construction time.
DecimalNum (default)
DoubleNum (fast)
DecimalNumFactory wraps Java BigDecimal. It gives arbitrary precision and is the safest choice for financial applications where rounding errors must be controlled.import org.ta4j.core.num.DecimalNumFactory;
BarSeries series = new BaseBarSeriesBuilder()
.withName("BTC-USD")
.withNumFactory(DecimalNumFactory.getInstance())
.build();
Use this when correctness matters more than throughput — e.g., position sizing, P&L calculations, or any code that will actually execute orders.DoubleNumFactory wraps Java double. It is significantly faster than DecimalNum but introduces the usual floating-point rounding errors.import org.ta4j.core.num.DoubleNumFactory;
BarSeries series = new BaseBarSeriesBuilder()
.withName("BTC-USD")
.withNumFactory(DoubleNumFactory.getInstance())
.build();
Use this for backtesting parameter sweeps and research where raw throughput matters and minor precision loss is acceptable.
You cannot mix Num types within a series. If you try to add a bar whose close price was produced by a different NumFactory, ta4j throws an IllegalArgumentException at runtime.
Navigating a series
// Total bar count (after any evictions)
int count = series.getBarCount();
// Inclusive begin and end indices
int first = series.getBeginIndex();
int last = series.getEndIndex();
// Access a specific bar
Bar bar = series.getBar(last);
// Period as a human-readable string (UTC)
String period = series.getSeriesPeriodDescription();
// e.g. "2024-01-01T00:00:00Z - 2024-12-31T00:00:00Z"
Subseries
You can slice a BarSeries into a smaller window without copying the underlying bars:
// Bars from index 100 (inclusive) to 200 (exclusive)
BarSeries sub = series.getSubSeries(100, 200);
The subseries has its own index range and can be passed to any indicator or strategy that accepts a BarSeries.