Skip to main content
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.

The basic live loop

1

Build a live series

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 backtesting
Strategy 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.
import org.ta4j.core.ConcurrentBarSeries;
import org.ta4j.core.ConcurrentBarSeriesBuilder;
import org.ta4j.core.bars.TimeBarBuilderFactory;
import java.time.Duration;
import java.time.Instant;

ConcurrentBarSeries series = new ConcurrentBarSeriesBuilder()
        .withName("BTC-USD")
        .withBarBuilderFactory(new TimeBarBuilderFactory(Duration.ofMinutes(1)))
        .build();

Instant t0 = Instant.parse("2024-01-01T10:05:00Z");
series.ingestTrade(t0, 1, 100);               // price=100, amount=1
series.ingestTrade(t0.plusSeconds(150), 2, 105); // skips the 10:06 bar

System.out.println(series.getBarCount());   // 2
System.out.println(series.getBar(1).getBeginTime());  // 2024-01-01T10:07:00Z
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.

Recording live fills

When your orders are filled, record them in a TradingRecord so that performance metrics and analytics remain accurate.

Streaming individual fills

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 it
TradeFill 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);

Batching partial fills from one order

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 arrives
record.operate(partialOne);
record.operate(partialTwo);

// Option 2: group them once the full batch is available
List<TradeFill> exchangeFills = List.of(partialOne, partialTwo);
record.operate(Trade.fromFills(TradeType.BUY, exchangeFills));

TradeFill fields

FieldDescription
barIndexBar index (-1 for live fills not tied to a specific bar)
timestampWhen the fill occurred
priceFill price
amountFill quantity
feeCommission paid
executionSideExecutionSide.BUY or ExecutionSide.SELL
orderIdExchange-assigned order ID
correlationIdYour internal order ID (used with SPECIFIC_ID matching)

ExecutionMatchPolicy options

ExecutionMatchPolicy controls how exits are matched to open lots when you have multiple partial positions:
PolicyBehaviour
FIFOMatches the oldest open lot first (first in, first out)
LIFOMatches the newest open lot first (last in, first out)
AVG_COSTUses the average cost of all open lots
SPECIFIC_IDMatches the lot whose correlationId or orderId matches the exit fill
// FIFO is the standard default for most strategies
TradingRecord fifoRecord = new BaseTradingRecord(
        TradeType.BUY, ExecutionMatchPolicy.FIFO,
        new ZeroCostModel(), new ZeroCostModel(), null, null);

// Use SPECIFIC_ID when your exchange reports which lot is being closed
TradingRecord 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.

Reading live equity

Use CashFlow to compute the running equity curve including open positions:
import org.ta4j.core.analysis.CashFlow;
import org.ta4j.core.analysis.EquityCurveMode;
import org.ta4j.core.analysis.OpenPositionHandling;

CashFlow equity = new CashFlow(series, record,
        EquityCurveMode.MARK_TO_MARKET,
        OpenPositionHandling.MARK_TO_MARKET);

Num latestEquity = equity.getValue(series.getEndIndex());
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.