Documentation, examples and further information of the ta4j project
This project is maintained by ta4j Organization
⚠️ Early days: ta4j’s live-trading story is still evolving. The APIs below are intentionally bare bones and require custom glue (data ingestion, order routing, resilience) on your side. Treat this as a starting point rather than a fully featured framework.
ta4j is not just for backtests—its abstractions map directly onto production trading bots. This page outlines how to bootstrap a live engine, keep your BarSeries synchronized with the exchange, and execute trades responsibly.
series.getEndIndex(), and send orders to your broker.Num implementation consistent with your broker’s precision (decimal for FX/crypto, double for faster equity bots).setMaximumBarCount(n) to cap memory while keeping enough bars to cover every indicator period.TransactionCostModel and HoldingCostModel on your BarSeriesManager or strategy runner so live metrics align with backtest assumptions.BarSeries liveSeries = new BaseBarSeriesBuilder()
.withName("binance_eth_usd_live")
.withNumFactory(DecimalNumFactory.getInstance())
.build();
liveSeries.setMaximumBarCount(500);
bootstrapWithRecentBars(liveSeries, exchangeClient);
Strategy strategy = strategyFactory.apply(liveSeries);
TradingRecord tradingRecord = new BaseTradingRecord();
For live trading with concurrent access, ConcurrentBarSeries provides thread-safe trade ingestion:
The recommended approach is to use ingestTrade() methods, which automatically aggregate trades into bars:
// Simple trade ingestion
concurrentSeries.ingestTrade(
trade.getTime(),
trade.getVolume(),
trade.getPrice()
);
// With side and liquidity classification (for RealtimeBar analytics)
concurrentSeries.ingestTrade(
trade.getTime(),
trade.getVolume(),
trade.getPrice(),
trade.getSide() == TradeSide.BUY ? RealtimeBar.Side.BUY : RealtimeBar.Side.SELL,
trade.getLiquidity() == TradeLiquidity.TAKER ? RealtimeBar.Liquidity.TAKER : RealtimeBar.Liquidity.MAKER
);
The ingestTrade() methods automatically:
BarBuilderFor pre-aggregated candles (e.g., from WebSocket feeds):
Bar candle = concurrentSeries.barBuilder()
.timePeriod(Duration.ofMinutes(1))
.endTime(candleData.closeTime())
.openPrice(candleData.open())
.highPrice(candleData.high())
.lowPrice(candleData.low())
.closePrice(candleData.close())
.volume(candleData.volume())
.build();
StreamingBarIngestResult result = concurrentSeries.ingestStreamingBar(candle);
// result.action() indicates: APPENDED, REPLACED_LAST, or REPLACED_HISTORICAL
For single-threaded scenarios, you can still use the traditional approach:
Bar bar = liveSeries.barBuilder()
.timePeriod(Duration.ofMinutes(1))
.endTime(candle.closeTime())
.openPrice(candle.open())
.highPrice(candle.high())
.lowPrice(candle.low())
.closePrice(candle.close())
.volume(candle.volume())
.build();
liveSeries.addBar(bar);
When updates arrive before the bar closes:
liveSeries.addTrade(liveSeries.numFactory().numOf(trade.volume()),
liveSeries.numFactory().numOf(trade.price()));
// Or replace the last bar entirely if the exchange sends a revised candle
liveSeries.addBar(replacementBar, true);
int endIndex = liveSeries.getEndIndex();
Num price = liveSeries.getBar(endIndex).getClosePrice();
if (strategy.shouldEnter(endIndex, tradingRecord)) {
orderService.submitBuy(price, desiredQuantity());
tradingRecord.enter(endIndex, price, desiredQuantity());
} else if (strategy.shouldExit(endIndex, tradingRecord)) {
orderService.submitSell(price, openQuantity());
tradingRecord.exit(endIndex, price, openQuantity());
}
With ConcurrentBarSeries, you can safely evaluate strategies in a separate thread while another thread ingests data:
// Thread 1: Ingest trades (runs continuously)
executorService.submit(() -> {
while (running) {
Trade trade = websocket.receiveTrade();
concurrentSeries.ingestTrade(
trade.getTime(),
trade.getVolume(),
trade.getPrice(),
trade.getSide(),
trade.getLiquidity()
);
}
});
// Thread 2: Evaluate strategy (runs on a schedule or trigger)
executorService.submit(() -> {
while (running) {
int endIndex = concurrentSeries.getEndIndex();
if (endIndex < 0) continue; // No bars yet
Num price = concurrentSeries.getBar(endIndex).getClosePrice();
if (strategy.shouldEnter(endIndex, tradingRecord)) {
orderService.submitBuy(price, desiredQuantity());
tradingRecord.enter(endIndex, price, desiredQuantity());
} else if (strategy.shouldExit(endIndex, tradingRecord)) {
orderService.submitSell(price, openQuantity());
tradingRecord.exit(endIndex, price, openQuantity());
}
Thread.sleep(100); // Or use a scheduled executor
}
});
Guidelines:
tradingRecord.isOpened()/isClosed() lines up with your broker state. If an order is partially filled, delay updating the record until the fill completes.TradeExecutionModel implementations to simulate your broker’s order semantics before going live.ConcurrentBarSeries, multiple threads can safely read from the series concurrently (e.g., parallel strategy evaluation, indicator calculation).ConcurrentBarSeries when you need thread-safe access; it provides read/write locks internally.StrategySerialization.toJson(strategy) or keep NamedStrategy descriptors in configuration. This ensures bots can reload the exact same logic after restarts.BarSeries to disk or cache so warm restarts skip the backfill step.StrategyExecutionLogging from ta4j-examples is a good starting point.BacktestExecutor) on the most recent data to ensure live behavior matches expectations.