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

⚠️ Early days: ta4j’s live-trading story is still evolving. The APIs below are intentionally bare bones and require custom glue (data ingestion, order routing, resilience) on your side. Treat this as a starting point rather than a fully featured framework.

ta4j is not just for backtests—its abstractions map directly onto production trading bots. This page outlines how to bootstrap a live engine, keep your BarSeries synchronized with the exchange, and execute trades responsibly.

Architecture overview

  1. Initialization – load recent history to warm up indicators, build your bar series, and instantiate the strategy.
  2. Event loop – append/update bars, evaluate entry/exit rules at series.getEndIndex(), and send orders to your broker.
  3. State persistence – serialize strategies, parameters, and trading records so the bot can restart without losing context.

Initialization checklist

BarSeries liveSeries = new BaseBarSeriesBuilder()
        .withName("binance_eth_usd_live")
        .withNumFactory(DecimalNumFactory.getInstance())
        .build();

liveSeries.setMaximumBarCount(500);
bootstrapWithRecentBars(liveSeries, exchangeClient);

Strategy strategy = strategyFactory.apply(liveSeries);
TradingRecord tradingRecord = new BaseTradingRecord();

Choosing a trading record

Walkthrough: LiveTradingRecord with partial fills and cost basis

When your broker reports partial fills or you want per-lot cost basis and unrealized PnL, use LiveTradingRecord instead of BaseTradingRecord. The following examples show the same API for simple enter/exit, then how to record individual fills and read the position book.

1. Simple enter/exit (same as BaseTradingRecord)

You can use enter(index, price, amount) and exit(index, price, amount) exactly like BaseTradingRecord. Each call records a single fill; the record stays compatible with all criteria and with BarSeriesManager if you drive it manually.

LiveTradingRecord record = new LiveTradingRecord(Trade.TradeType.BUY);
Num price = series.getBar(endIndex).getClosePrice();
Num amount = series.numFactory().numOf(1);

if (strategy.shouldEnter(endIndex, record)) {
    record.enter(endIndex, price, amount);
} else if (strategy.shouldExit(endIndex, record)) {
    record.exit(endIndex, price, amount);
}

2. Partial fills and position book

When you receive fills from the broker one-by-one, record each fill with recordFill(ExecutionFill). Use ExecutionSide.BUY / ExecutionSide.SELL and optional fee, order id, and correlation id. The record applies FIFO (or your configured ExecutionMatchPolicy) and maintains open lots.

LiveTradingRecord record = new LiveTradingRecord(
    Trade.TradeType.BUY,
    ExecutionMatchPolicy.FIFO,
    new ZeroCostModel(),
    new ZeroCostModel(),
    null, null);

NumFactory num = series.numFactory();

// Two buy fills (e.g. from broker)
record.recordFill(new ExecutionFill(
    Instant.now(), num.numOf(100.0), num.numOf(0.5), num.zero(),
    ExecutionSide.BUY, "order-1", null));
record.recordFill(new ExecutionFill(
    Instant.now(), num.numOf(101.0), num.numOf(0.5), num.zero(),
    ExecutionSide.BUY, "order-2", null));

// Open position: two lots, average cost and total amount available
List<OpenPosition> openPositions = record.getOpenPositions();
OpenPosition net = record.getNetOpenPosition();
// net.amount(), net.averageEntryPrice(), net.costBasis(), etc.

// Exit (e.g. one full exit at current price – FIFO matches against first lot)
record.recordFill(new ExecutionFill(
    Instant.now(), num.numOf(102.0), num.numOf(1.0), num.zero(),
    ExecutionSide.SELL, "order-3", null));

3. Cost basis and unrealized PnL with criteria

After you have a BarSeries and a LiveTradingRecord (from live fills or from a custom backtest loop), you can measure cost basis and unrealized PnL with the same criteria used for analysis:

BarSeries series = ...;   // your bar series
LiveTradingRecord record = ...;  // populated via enter/exit or recordFill
int endIndex = series.getEndIndex();

AnalysisCriterion costBasis = new OpenPositionCostBasisCriterion();
AnalysisCriterion unrealizedPnL = new OpenPositionUnrealizedProfitCriterion();

System.out.println("Open position cost basis: " + costBasis.calculate(series, record));
System.out.println("Unrealized PnL: " + unrealizedPnL.calculate(series, record));

Use record.snapshot() to capture a point-in-time view of positions and trades for persistence or auditing.

Feeding the series

For live trading with concurrent access, ConcurrentBarSeries provides thread-safe trade ingestion:

Streaming trade ingestion

The recommended approach is to use ingestTrade() methods, which automatically aggregate trades into bars:

// Simple trade ingestion
concurrentSeries.ingestTrade(
    trade.getTime(),
    trade.getVolume(),
    trade.getPrice()
);

// With side and liquidity classification (for RealtimeBar analytics)
concurrentSeries.ingestTrade(
    trade.getTime(),
    trade.getVolume(),
    trade.getPrice(),
    trade.getSide() == TradeSide.BUY ? RealtimeBar.Side.BUY : RealtimeBar.Side.SELL,
    trade.getLiquidity() == TradeLiquidity.TAKER ? RealtimeBar.Liquidity.TAKER : RealtimeBar.Liquidity.MAKER
);

The ingestTrade() methods automatically:

Streaming bar ingestion

For pre-aggregated candles (e.g., from WebSocket feeds):

Bar candle = concurrentSeries.barBuilder()
        .timePeriod(Duration.ofMinutes(1))
        .endTime(candleData.closeTime())
        .openPrice(candleData.open())
        .highPrice(candleData.high())
        .lowPrice(candleData.low())
        .closePrice(candleData.close())
        .volume(candleData.volume())
        .build();

StreamingBarIngestResult result = concurrentSeries.ingestStreamingBar(candle);
// result.action() indicates: APPENDED, REPLACED_LAST, or REPLACED_HISTORICAL

Using BaseBarSeries (single-threaded)

For single-threaded scenarios, you can still use the traditional approach:

Bar bar = liveSeries.barBuilder()
        .timePeriod(Duration.ofMinutes(1))
        .endTime(candle.closeTime())
        .openPrice(candle.open())
        .highPrice(candle.high())
        .lowPrice(candle.low())
        .closePrice(candle.close())
        .volume(candle.volume())
        .build();

liveSeries.addBar(bar);

When updates arrive before the bar closes:

liveSeries.addTrade(liveSeries.numFactory().numOf(trade.volume()),
        liveSeries.numFactory().numOf(trade.price()));

// Or replace the last bar entirely if the exchange sends a revised candle
liveSeries.addBar(replacementBar, true);

Evaluating and executing

Stop-rule playbook for live trading

ta4j now includes fixed, trailing, fixed-amount, volatility-scaled, and ATR-based stop loss/gain rules. For selection guidance, configuration patterns, and deployment pitfalls, use:

Live-specific recommendation:

Single-threaded evaluation

int endIndex = liveSeries.getEndIndex();
Num price = liveSeries.getBar(endIndex).getClosePrice();

if (strategy.shouldEnter(endIndex, tradingRecord)) {
    orderService.submitBuy(price, desiredQuantity());
    tradingRecord.enter(endIndex, price, desiredQuantity());
} else if (strategy.shouldExit(endIndex, tradingRecord)) {
    orderService.submitSell(price, openQuantity());
    tradingRecord.exit(endIndex, price, openQuantity());
}

Multi-threaded evaluation with ConcurrentBarSeries

With ConcurrentBarSeries, you can safely evaluate strategies in a separate thread while another thread ingests data:

// Thread 1: Ingest trades (runs continuously)
executorService.submit(() -> {
    while (running) {
        Trade trade = websocket.receiveTrade();
        concurrentSeries.ingestTrade(
            trade.getTime(),
            trade.getVolume(),
            trade.getPrice(),
            trade.getSide(),
            trade.getLiquidity()
        );
    }
});

// Thread 2: Evaluate strategy (runs on a schedule or trigger)
executorService.submit(() -> {
    while (running) {
        int endIndex = concurrentSeries.getEndIndex();
        if (endIndex < 0) continue; // No bars yet
        
        Num price = concurrentSeries.getBar(endIndex).getClosePrice();
        
        if (strategy.shouldEnter(endIndex, tradingRecord)) {
            orderService.submitBuy(price, desiredQuantity());
            tradingRecord.enter(endIndex, price, desiredQuantity());
        } else if (strategy.shouldExit(endIndex, tradingRecord)) {
            orderService.submitSell(price, openQuantity());
            tradingRecord.exit(endIndex, price, openQuantity());
        }
        
        Thread.sleep(100); // Or use a scheduled executor
    }
});

Guidelines:

Live workflow for VWAP, support/resistance, and Wyckoff

For the full implementation playbook, see VWAP, Support/Resistance, and Wyckoff Guide. In live routing:

Persistence & recovery

Monitoring & alerting

Examples & references