Ta4j Wiki

Documentation, examples and further information of the ta4j project

View the Wiki On GitHub

This project is maintained by ta4j Organization

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:

You still own:

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:

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:

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:

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

For a full production operating model (startup, persistence, recovery, reconciliation, and incident handling), follow the Live Trading Runbook.

Examples And References

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)