Deploy the same strategies you backtest to live markets — no code rewrites needed.
The same strategy code that runs a backtest runs live. There are no separate interfaces or translation layers. The only difference is where the bars come from: a historical file during backtesting, a broker or exchange API in production.
Start with an empty series using BaseBarSeriesBuilder. The series grows as bars arrive.
import org.ta4j.core.builder.BaseBarSeriesBuilder;BarSeries liveSeries = new BaseBarSeriesBuilder() .withName("BTC-USD") .build();
2
Build your strategy
Use the exact same strategy code as your backtest.
// Same method you use for backtestingStrategy strategy = buildStrategy(liveSeries);
3
Run the trading loop
On each new bar, add it to the series, then check for entry and exit signals.
while (true) { // Fetch the latest completed bar from your broker or exchange API Bar latest = fetchLatestBarFromBroker(); // Your integration here liveSeries.addBar(latest); int endIndex = liveSeries.getEndIndex(); if (strategy.shouldEnter(endIndex)) { placeBuyOrder(); // Your order execution logic } else if (strategy.shouldExit(endIndex)) { placeSellOrder(); // Your order execution logic } Thread.sleep(60_000); // Wait for the next bar}
Streaming trade ingestion with ConcurrentBarSeries
When you receive raw trades (ticks) from an exchange feed rather than pre-built OHLCV bars, use ConcurrentBarSeries with a TimeBarBuilderFactory. It aggregates trades into time bars automatically.
ingestTrade(timestamp, amount, price) adds a trade to the appropriate bar, advancing the time window as needed. Time gaps are omitted — no empty bars are inserted for periods with no trades.
If your downstream indicators or strategy require continuous prices (no gaps), reconcile and backfill OHLCV data upstream before ingesting into ta4j.
ConcurrentBarSeries is thread-safe. You can call ingestTrade() from a market data thread while reading bar data from a strategy evaluation thread.
Use TradingRecord.operate(fill) to record each fill as it arrives:
import org.ta4j.core.BaseTradingRecord;import org.ta4j.core.TradeFill;import org.ta4j.core.TradingRecord;import org.ta4j.core.ExecutionMatchPolicy;import org.ta4j.core.ExecutionSide;import org.ta4j.core.Trade.TradeType;import org.ta4j.core.analysis.cost.ZeroCostModel;import java.time.Instant;TradingRecord record = new BaseTradingRecord( TradeType.BUY, ExecutionMatchPolicy.FIFO, new ZeroCostModel(), new ZeroCostModel(), null, null);// Map an incoming exchange fill and record itTradeFill fill = new TradeFill( -1, // bar index (-1 for live fills not tied to a bar) Instant.now(), price, amount, fee, ExecutionSide.BUY, orderId, correlationId);record.operate(fill);
If the exchange already gives you multiple partial fills for a single logical order, you can stream them one at a time or group them:
TradeFill partialOne = mapExchangeFill(rawFillOne);TradeFill partialTwo = mapExchangeFill(rawFillTwo);// Option 1: stream each fill as it arrivesrecord.operate(partialOne);record.operate(partialTwo);// Option 2: group them once the full batch is availableList<TradeFill> exchangeFills = List.of(partialOne, partialTwo);record.operate(Trade.fromFills(TradeType.BUY, exchangeFills));
ExecutionMatchPolicy controls how exits are matched to open lots when you have multiple partial positions:
Policy
Behaviour
FIFO
Matches the oldest open lot first (first in, first out)
LIFO
Matches the newest open lot first (last in, first out)
AVG_COST
Uses the average cost of all open lots
SPECIFIC_ID
Matches the lot whose correlationId or orderId matches the exit fill
// FIFO is the standard default for most strategiesTradingRecord fifoRecord = new BaseTradingRecord( TradeType.BUY, ExecutionMatchPolicy.FIFO, new ZeroCostModel(), new ZeroCostModel(), null, null);// Use SPECIFIC_ID when your exchange reports which lot is being closedTradingRecord specificRecord = new BaseTradingRecord( TradeType.BUY, ExecutionMatchPolicy.SPECIFIC_ID, new ZeroCostModel(), new ZeroCostModel(), null, null);
SPECIFIC_ID matches exits to the lot with a matching correlationId or orderId in the original entry fill.
After deserializing a BaseTradingRecord (for example, on service restart), call rehydrate(holdingCostModel) to restore transient cost models before using it.
Data sources
Load historical data to build and test strategies before going live.
Serialization
Persist and restore strategies and trading records across restarts.