Live Trading
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.
What ta4j Handles, And What You Still Own
ta4j gives you:
BarSeriesandConcurrentBarSeriesfor market state- indicators, rules, and
Strategyevaluation BaseTradingRecordfor position and fill state- criteria and statements for reporting
You still own:
- historical backfill and websocket ingestion
- order routing and retry logic
- account reconciliation
- persistence, recovery, and operational alerting
Choose The Live Execution Path
Use Execution Decision Matrix for canonical path selection. This page focuses on live-architecture mechanics after the path is selected.
For new code, start with BaseTradingRecord. LiveTradingRecord is a deprecated compatibility facade over the same underlying model.
Initialize Market State And Trading State
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.
Feed The Series
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);
Evaluate On Bar Close, Record On Fill
This is the most important live-trading rule in the current stack:
- Strategy evaluation answers “should I try to trade?”
- The trading record answers “what actually filled?”
Do not mutate the record when you merely emit an order intent if your exchange can reject, partially fill, or delay that order.
Closed candle vs live candle
ta4j evaluates whatever bar state currently exists in your series:
- replacing the latest bar during a period means live-candle evaluation
- evaluating only after appending a completed bar means closed-candle evaluation
If you are new to live execution, start with closed-candle evaluation first. It is easier to reason about and avoids repeated intra-bar signals while you validate your end-to-end integration.
For a deeper guide (including duplicate-entry prevention), read Live Candle vs Closed Candle Evaluation.
int lastEntryBarIndex = -1;
int lastExitBarIndex = -1;
while (true) {
int endIndex = series.getEndIndex();
Num lastPrice = series.getBar(endIndex).getClosePrice();
Num amount = series.numFactory().one();
if (strategy.shouldEnter(endIndex, tradingRecord) && endIndex != lastEntryBarIndex) {
orderRouter.submitBuy(lastPrice, amount);
lastEntryBarIndex = endIndex;
} else if (strategy.shouldExit(endIndex, tradingRecord) && endIndex != lastExitBarIndex) {
orderRouter.submitSell(lastPrice, amount);
lastExitBarIndex = endIndex;
}
}
Walkthrough: broker-confirmed fills with BaseTradingRecord
When 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.
Open-Position Metrics And Reconciliation
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.
Persistence And Recovery
At minimum, persist:
- the strategy or strategy descriptor
- the latest processed bar or timestamp
- the serialized trading-record snapshot
- any broker-side identifiers needed to de-duplicate orders after restart
If you rebuild the series on startup, make sure its bar index alignment still matches the recovered fills before you resume strategy evaluation.
Operational Notes
- Persist fills or snapshots before acknowledging them as durable in your own pipeline.
- Keep monitoring around bar ingestion gaps, order rejections, and repeated stale signals; those are usually integration bugs, not ta4j math bugs.
- If feed ingestion and strategy evaluation happen on different threads, prefer
ConcurrentBarSeriesand keepTradingRecordmutation on the execution-confirmation path only.
For a full production operating model (startup, persistence, recovery, reconciliation, and incident handling), follow the Live Trading Runbook.
Examples And References
- TradeFillRecordingExample - Runnable walkthrough of streamed fills, grouped batches, and partial-exit matching policies
- TradingBotOnMovingBarSeries - Minimal manual bot loop
- Backtesting - Historical and replay-style execution patterns
- Bar Series & Bars - Bar ingestion and aggregation details
- Usage Examples - Runnable examples in
ta4j-examples - Troubleshooting Hub - Symptom-driven diagnostics and escalation checklist
Compatibility Note
LiveTradingRecord and ExecutionFill are deprecated compatibility APIs in 0.22.x so older integrations can migrate without a sudden compile break. The recommended path for new live code is BaseTradingRecord plus TradeFill or grouped Trade.fromFills(...); both preserve recorded fees through RecordedTradeCostModel.
Rationale Notes (2026-04-27)
- Rechecked this page against the trade API tightening in commit
49f3f5f8:TradeFillis the current fill contract,LiveTradingRecordandExecutionFillare deprecated compatibility surfaces, andBaseTradingRecordowns open-position views and fill-awareoperate(...)paths.