Skip to main content
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:
FieldMethodDescription
Open pricegetOpenPrice()First trade price in the period
High pricegetHighPrice()Highest trade price in the period
Low pricegetLowPrice()Lowest trade price in the period
Close pricegetClosePrice()Last trade price in the period
VolumegetVolume()Total units traded
AmountgetAmount()Total notional traded (price × volume)
TradesgetTrades()Number of individual trades
Begin timegetBeginTime()Period start (UTC Instant)
End timegetEndTime()Period end (UTC Instant)
DurationgetTimePeriod()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.
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.
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.
// 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.