Documentation, examples and further information of the ta4j project
This project is maintained by ta4j Organization
ta4j gives you the strategy, series, and trading-record primitives for live systems. It does not replace your exchange adapter, order router, risk controls, or persistence layer, but it does let you use the same BaseTradingRecord class across historical, paper, and live execution paths.
ta4j gives you:
BarSeries and ConcurrentBarSeries for market stateStrategy evaluationBaseTradingRecord for position and fill stateYou still own:
| Situation | Recommended path | Why |
|---|---|---|
| Single-threaded bot with synchronous fills | BaseBarSeries + BaseTradingRecord |
Simple loop, minimal moving pieces |
| Multi-threaded ingestion and evaluation | ConcurrentBarSeries + BaseTradingRecord |
Thread-safe reads and writes |
| Partial fills, fee capture, broker order IDs, or reconciliation | TradingRecord.operate(fill) or operate(Trade.fromFills(...)) |
Preserve the exact fill stream without a separate live-only API |
Legacy adapter that still exposes LiveTradingRecord or ExecutionFill |
Keep temporarily, migrate when practical | Compatibility only while moving toward TradeFill / Trade |
For new code, start with BaseTradingRecord. LiveTradingRecord is a deprecated compatibility facade over the same underlying model.
Single-threaded setup:
BarSeries series = new BaseBarSeriesBuilder()
.withName("eth-usd-live")
.build();
BaseTradingRecord tradingRecord = new BaseTradingRecord(
strategy.getStartingType(),
ExecutionMatchPolicy.FIFO,
RecordedTradeCostModel.INSTANCE,
new ZeroCostModel(),
null,
null);
Concurrent setup:
ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder()
.withName("eth-usd-live")
.withMaxBarCount(1000)
.build();
BaseTradingRecord tradingRecord = new BaseTradingRecord(
strategy.getStartingType(),
ExecutionMatchPolicy.FIFO,
RecordedTradeCostModel.INSTANCE,
new ZeroCostModel(),
null,
null);
Use RecordedTradeCostModel when your broker already tells you the actual fee for each fill. That keeps analytics aligned with what actually happened at the broker.
If you have raw trades, let ConcurrentBarSeries aggregate them:
series.ingestTrade(
trade.timestamp(),
trade.volume(),
trade.price());
If your venue sends completed or partial candles, ingest bars directly:
Bar candle = series.barBuilder()
.timePeriod(Duration.ofMinutes(1))
.endTime(candleCloseTime)
.openPrice(open)
.highPrice(high)
.lowPrice(low)
.closePrice(close)
.volume(volume)
.build();
series.ingestStreamingBar(candle);
This is the most important live-trading rule in the current stack:
Do not mutate the record when you merely emit an order intent if your exchange can reject, partially fill, or delay that order.
int endIndex = series.getEndIndex();
Num lastPrice = series.getBar(endIndex).getClosePrice();
Num amount = series.numFactory().one();
if (strategy.shouldEnter(endIndex, tradingRecord)) {
orderRouter.submitBuy(lastPrice, amount);
} else if (strategy.shouldExit(endIndex, tradingRecord)) {
orderRouter.submitSell(lastPrice, amount);
}
BaseTradingRecordWhen the broker confirms a fill, write that fill into the record:
TradeFill fill = new TradeFill(
endIndex,
Instant.now(),
series.numFactory().numOf("42100"),
series.numFactory().numOf("0.50"),
series.numFactory().numOf("4.21"),
ExecutionSide.BUY,
"order-123",
"decision-123");
tradingRecord.operate(fill);
If the exchange already gives you the complete batch for one logical order, keep the fills together:
List<TradeFill> exchangeFills = List.of(fillOne, fillTwo);
tradingRecord.operate(Trade.fromFills(Trade.TradeType.BUY, exchangeFills));
Both paths preserve metadata such as side, fee, order ID, and correlation ID.
Because BaseTradingRecord now exposes lot-aware views directly, you can inspect open positions and live PnL without switching to a separate record type:
Position currentPosition = tradingRecord.getCurrentPosition();
List<Position> lots = tradingRecord.getOpenPositions();
AnalysisCriterion costBasis = new OpenPositionCostBasisCriterion();
AnalysisCriterion unrealizedPnL = new OpenPositionUnrealizedProfitCriterion();
System.out.println("Net open amount: " + currentPosition.amount());
System.out.println("Cost basis: " + costBasis.calculate(series, tradingRecord));
System.out.println("Unrealized PnL: " + unrealizedPnL.calculate(series, tradingRecord));
getCurrentPosition() is the canonical net-open view. getNetOpenPosition() still exists in 0.22.x as a compatibility alias, but new code should not need it.
This is also the surface used by downstream systems for dashboards and snapshots.
At minimum, persist:
If you rebuild the series on startup, make sure its bar index alignment still matches the recovered fills before you resume strategy evaluation.
ConcurrentBarSeries and keep TradingRecord mutation on the execution-confirmation path only.ta4j-examplesLiveTradingRecord and ExecutionFill are still present in 0.22.x so older integrations can migrate without a sudden compile break, but the recommended path for new live code is BaseTradingRecord plus TradeFill or grouped Trade.fromFills(...).