Skip to main content
A TradingRecord is ta4j’s ledger for a trading session. It records every entry and exit trade, builds positions from matched trades, and exposes them for performance analysis. The same TradingRecord API works in backtests (where BarSeriesManager writes to it automatically) and in live trading (where you write fills to it manually as orders are confirmed).

What TradingRecord tracks

  • Trades — every individual BUY or SELL action, with index, price, and amount.
  • Positions — matched pairs of entry and exit trades representing a complete round-trip.
  • Current position — the open position (if any) that has an entry but no exit yet.
  • Open lots — for lot-aware implementations, individual open entry positions not yet closed.
  • Fees — accumulated execution fees recorded from live fills.

BaseTradingRecord

BaseTradingRecord is the standard implementation. It handles both simulated (index/price/amount) and fill-aware (live execution) recording in a single class. It is also thread-safe, guarded by a ReentrantReadWriteLock.

Constructor options

1

Default (BUY first, FIFO, zero costs)

import org.ta4j.core.BaseTradingRecord;

TradingRecord record = new BaseTradingRecord();
2

Named record with explicit trade type

TradingRecord record = new BaseTradingRecord("my-strategy", TradeType.BUY);
3

With cost models

import org.ta4j.core.analysis.cost.LinearTransactionCostModel;
import org.ta4j.core.analysis.cost.ZeroCostModel;

TradingRecord record = new BaseTradingRecord(
        TradeType.BUY,
        new LinearTransactionCostModel(0.001),  // 0.1% per trade
        new ZeroCostModel()                     // no borrowing cost
);
4

With ExecutionMatchPolicy

import org.ta4j.core.ExecutionMatchPolicy;

TradingRecord record = new BaseTradingRecord(
        TradeType.BUY,
        ExecutionMatchPolicy.FIFO,
        new ZeroCostModel(),
        new ZeroCostModel(),
        null,  // no start index constraint
        null   // no end index constraint
);

Accessing trades and positions

// All recorded trades (entries and exits, in order)
List<Trade> trades = record.getTrades();

// All completed (closed) positions
List<Position> positions = record.getPositions();
int positionCount = record.getPositionCount();

// The currently open position (or an empty Position if flat)
Position current = record.getCurrentPosition();
boolean isFlat = record.isClosed();

// Most recent trades
Trade lastEntry = record.getLastEntry();
Trade lastExit  = record.getLastExit();

// Open positions (per-lot, for lot-aware recording)
List<Position> openLots = record.getOpenPositions();

Recording live fills

For live trading, route exchange execution confirmations into the record using operate(fill):
import org.ta4j.core.TradeFill;
import org.ta4j.core.ExecutionSide;
import java.time.Instant;

// A single fill from your exchange
TradeFill fill = new TradeFill(
        -1,                   // index: -1 means auto-assign next sequential index
        Instant.now(),        // execution timestamp
        price,                // fill price (Num)
        amount,               // fill amount (Num)
        fee,                  // commission (Num)
        ExecutionSide.BUY,    // side — required so the direction is unambiguous
        orderId,              // exchange order ID
        correlationId         // your correlation key (optional)
);

record.operate(fill);

Grouped fills (partial fills)

If your exchange reports multiple partial fills for one logical order, you can either stream them one at a time or batch them together with Trade.fromFills():
// Each fill goes in as it arrives
record.operate(fillOne);
record.operate(fillTwo);

ExecutionMatchPolicy

When an exit trade arrives, BaseTradingRecord must decide which open lot it closes. The ExecutionMatchPolicy enum controls this:
PolicyBehavior
FIFOClose the oldest open lot first. Default.
LIFOClose the most recently opened lot first.
AVG_COSTMerge all open lots into one average-cost lot before each operation.
SPECIFIC_IDMatch exit to the open lot whose correlationId or orderId matches the exit trade.
// LIFO matching — useful for certain tax strategies
TradingRecord lifoRecord = new BaseTradingRecord(
        TradeType.BUY,
        ExecutionMatchPolicy.LIFO,
        new ZeroCostModel(),
        new ZeroCostModel(),
        null, null
);
SPECIFIC_ID matching requires the exit trade to carry a correlationId or orderId that matches exactly one open lot. If no matching lot is found, ta4j throws an IllegalStateException.

CashFlow and equity curve analysis

After a backtest or live session, compute the equity curve with CashFlow:
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
);

// Equity value at each bar
for (int i = series.getBeginIndex(); i <= series.getEndIndex(); i++) {
    System.out.printf("Bar %d: %.2f%n", i, equity.getValue(i).doubleValue());
}

// Latest equity value
Num latest = equity.getValue(series.getEndIndex());
EquityCurveMode.MARK_TO_MARKET values open positions at the current close price. OpenPositionHandling.MARK_TO_MARKET includes the unrealized P&L from the open position in the equity calculation.

Serialization and rehydration

BaseTradingRecord implements Serializable. After deserializing, transient fields (cost models, locks) must be restored:
// After Java deserialization
BaseTradingRecord record = /* deserialized */;
record.rehydrate(
        new LinearTransactionCostModel(0.001),
        new ZeroCostModel()
);
If you only need to restore the holding cost model (the transaction cost model is preserved from the original session), use the single-argument overload:
record.rehydrate(holdingCostModel);

Backtest vs. live recording: using the same API

The same TradingRecord interface covers both workflows. In a backtest, BarSeriesManager writes to it automatically. In live trading, you write fills yourself. The record does not know or care which mode it is in — queries like getPositions() and getCurrentPosition() work identically in both cases.
// Backtest: BarSeriesManager drives the record
TradingRecord backtestRecord = new BarSeriesManager(series).run(strategy);

// Live: you drive the record
TradingRecord liveRecord = new BaseTradingRecord(TradeType.BUY,
        ExecutionMatchPolicy.FIFO,
        new ZeroCostModel(), new ZeroCostModel(), null, null);

// Your order fill handler
void onFill(TradeFill fill) {
    liveRecord.operate(fill);
}
See TradeFillRecordingExample in ta4j-examples for a complete walkthrough of live-style partial-fill recording, including side-by-side comparisons of FIFO, LIFO, AVG_COST, and SPECIFIC_ID matching.