TradingRecord tracks every trade, position, and fill — both in backtests and live trading.
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).
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.
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);
// All recorded trades (entries and exits, in order)List<Trade> trades = record.getTrades();// All completed (closed) positionsList<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 tradesTrade lastEntry = record.getLastEntry();Trade lastExit = record.getLastExit();// Open positions (per-lot, for lot-aware recording)List<Position> openLots = record.getOpenPositions();
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 exchangeTradeFill 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);
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 arrivesrecord.operate(fillOne);record.operate(fillTwo);
When an exit trade arrives, BaseTradingRecord must decide which open lot it closes. The ExecutionMatchPolicy enum controls this:
Policy
Behavior
FIFO
Close the oldest open lot first. Default.
LIFO
Close the most recently opened lot first.
AVG_COST
Merge all open lots into one average-cost lot before each operation.
SPECIFIC_ID
Match exit to the open lot whose correlationId or orderId matches the exit trade.
// LIFO matching — useful for certain tax strategiesTradingRecord 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.
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 barfor (int i = series.getBeginIndex(); i <= series.getEndIndex(); i++) { System.out.printf("Bar %d: %.2f%n", i, equity.getValue(i).doubleValue());}// Latest equity valueNum 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.
BaseTradingRecord implements Serializable. After deserializing, transient fields (cost models, locks) must be restored:
// After Java deserializationBaseTradingRecord 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:
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 recordTradingRecord backtestRecord = new BarSeriesManager(series).run(strategy);// Live: you drive the recordTradingRecord liveRecord = new BaseTradingRecord(TradeType.BUY, ExecutionMatchPolicy.FIFO, new ZeroCostModel(), new ZeroCostModel(), null, null);// Your order fill handlervoid 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.