Ta4j Wiki

Documentation, examples and further information of the ta4j project

View the Wiki On GitHub

This project is maintained by ta4j Organization

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:

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:

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:

Notes:

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:

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:

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:

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:

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:

Transient locks are reinitialized on deserialization, and the trade bar builder is recreated lazily on the next ingestion call.

Rationale Notes (2026-03-06)

Rationale Notes (2026-04-27)

When to use ConcurrentBarSeries

Use ConcurrentBarSeries when:

Use BaseBarSeries when:

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