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();
BaseTradingRecord: Simple list of positions (one entry + one exit per position). Use for backtests or live bots where every fill is treated as a single trade and you do not need per-lot cost basis or unrealized PnL. BarSeriesManager.run(strategy) returns a BaseTradingRecord.LiveTradingRecord: Supports partial fills, multiple lots per position, configurable execution matching (e.g. FIFO), and optional cost models. Use when you need cost basis, unrealized PnL, or a position book for live reconciliation. Thread-safe for concurrent ingestion and evaluation. See release notes for Position, OpenPosition, Trade, PositionBook, and related criteria (OpenPositionCostBasisCriterion, OpenPositionUnrealizedProfitCriterion).When your broker reports partial fills or you want per-lot cost basis and unrealized PnL, use LiveTradingRecord instead of BaseTradingRecord. The following examples show the same API for simple enter/exit, then how to record individual fills and read the position book.
1. Simple enter/exit (same as BaseTradingRecord)
You can use enter(index, price, amount) and exit(index, price, amount) exactly like BaseTradingRecord. Each call records a single fill; the record stays compatible with all criteria and with BarSeriesManager if you drive it manually.
LiveTradingRecord record = new LiveTradingRecord(Trade.TradeType.BUY);
Num price = series.getBar(endIndex).getClosePrice();
Num amount = series.numFactory().numOf(1);
if (strategy.shouldEnter(endIndex, record)) {
record.enter(endIndex, price, amount);
} else if (strategy.shouldExit(endIndex, record)) {
record.exit(endIndex, price, amount);
}
2. Partial fills and position book
When you receive fills from the broker one-by-one, record each fill with recordFill(ExecutionFill). Use ExecutionSide.BUY / ExecutionSide.SELL and optional fee, order id, and correlation id. The record applies FIFO (or your configured ExecutionMatchPolicy) and maintains open lots.
LiveTradingRecord record = new LiveTradingRecord(
Trade.TradeType.BUY,
ExecutionMatchPolicy.FIFO,
new ZeroCostModel(),
new ZeroCostModel(),
null, null);
NumFactory num = series.numFactory();
// Two buy fills (e.g. from broker)
record.recordFill(new ExecutionFill(
Instant.now(), num.numOf(100.0), num.numOf(0.5), num.zero(),
ExecutionSide.BUY, "order-1", null));
record.recordFill(new ExecutionFill(
Instant.now(), num.numOf(101.0), num.numOf(0.5), num.zero(),
ExecutionSide.BUY, "order-2", null));
// Open position: two lots, average cost and total amount available
List<OpenPosition> openPositions = record.getOpenPositions();
OpenPosition net = record.getNetOpenPosition();
// net.amount(), net.averageEntryPrice(), net.costBasis(), etc.
// Exit (e.g. one full exit at current price – FIFO matches against first lot)
record.recordFill(new ExecutionFill(
Instant.now(), num.numOf(102.0), num.numOf(1.0), num.zero(),
ExecutionSide.SELL, "order-3", null));
3. Cost basis and unrealized PnL with criteria
After you have a BarSeries and a LiveTradingRecord (from live fills or from a custom backtest loop), you can measure cost basis and unrealized PnL with the same criteria used for analysis:
BarSeries series = ...; // your bar series
LiveTradingRecord record = ...; // populated via enter/exit or recordFill
int endIndex = series.getEndIndex();
AnalysisCriterion costBasis = new OpenPositionCostBasisCriterion();
AnalysisCriterion unrealizedPnL = new OpenPositionUnrealizedProfitCriterion();
System.out.println("Open position cost basis: " + costBasis.calculate(series, record));
System.out.println("Unrealized PnL: " + unrealizedPnL.calculate(series, record));
Use record.snapshot() to capture a point-in-time view of positions and trades for persistence or auditing.
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);
ta4j now includes fixed, trailing, fixed-amount, volatility-scaled, and ATR-based stop loss/gain rules. For selection guidance, configuration patterns, and deployment pitfalls, use:
Live-specific recommendation:
StopLossRule or FixedAmountStopLossRule) even if your primary stop is volatility-adaptive.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.For the full implementation playbook, see VWAP, Support/Resistance, and Wyckoff Guide. In live routing:
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.