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:

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).

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:

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(100),
        liveSeries.numFactory().numOf(2)); // updates volume + close
liveSeries.addBar(updatedBar, true); // replace the last bar entirely

Best practices:

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(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:

// 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:

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.

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