Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ta4j/ta4j/llms.txt

Use this file to discover all available pages before exploring further.

Backtesting in ta4j means replaying a Strategy bar-by-bar over a historical BarSeries and recording every entry and exit the strategy would have generated. The result is a TradingRecord you can query for performance metrics.

BarSeriesManager.run()

BarSeriesManager is the backtesting engine. Construct it with a series and optional cost models, then call run():
import org.ta4j.core.backtest.BarSeriesManager;
import org.ta4j.core.TradingRecord;

BarSeriesManager manager = new BarSeriesManager(series);
TradingRecord record = manager.run(strategy);
run() iterates from series.getBeginIndex() to series.getEndIndex() and, for each bar, asks the strategy whether to enter or exit. Trades are recorded in the returned TradingRecord.

Run options

// Specify trade type explicitly (default is strategy.getStartingType())
TradingRecord longRecord  = manager.run(strategy, TradeType.BUY);
TradingRecord shortRecord = manager.run(strategy, TradeType.SELL);

// Run over a sub-range of the series
TradingRecord partial = manager.run(strategy, 100, 500);

// Specify position size (default is 1 unit)
Num amount = series.numFactory().numOf(10);
TradingRecord sized = manager.run(strategy, TradeType.BUY, amount);

Adding trading costs

Real trading incurs transaction fees and, for leveraged positions, borrowing costs. Pass cost models to BarSeriesManager:
import org.ta4j.core.analysis.cost.LinearTransactionCostModel;
import org.ta4j.core.analysis.cost.LinearBorrowingCostModel;

BarSeriesManager manager = new BarSeriesManager(
        series,
        new LinearTransactionCostModel(0.001),  // 0.1% fee per trade
        new LinearBorrowingCostModel(0.0001)    // 0.01% holding cost per period
);

TradingRecord record = manager.run(strategy);
LinearTransactionCostModel scales fee by rate × tradeValue. LinearBorrowingCostModel scales borrowing cost by rate × positionValue × barsHeld. Both implement the CostModel interface so you can supply custom implementations.

Execution models

The default execution model is TradeOnNextOpenModel, which fills orders at the open of the bar following the signal bar. This is the most conservative model and avoids look-ahead bias.
// Fills at the open price of the next bar after the signal
BarSeriesManager manager = new BarSeriesManager(series);
TradingRecord record = manager.run(strategy);

Reading the results

After run(), query the TradingRecord for trade and position data:
// Number of individual trades (each entry and exit is one trade)
List<Trade> trades = record.getTrades();
System.out.println("Trades: " + trades.size());

// Number of completed positions (entry + matching exit)
List<Position> positions = record.getPositions();
System.out.println("Positions: " + positions.size());

// Last entry and exit trades
Trade lastEntry = record.getLastEntry();
Trade lastExit  = record.getLastExit();

// Current (open) position, if any
Position current = record.getCurrentPosition();

Calculating performance metrics

Use analysis criteria to compute performance numbers from the record:
import org.ta4j.core.criteria.pnl.NetReturnCriterion;
import org.ta4j.core.criteria.drawdown.MaximumDrawdownCriterion;

Num netReturn = new NetReturnCriterion().calculate(series, record);
Num maxDrawdown = new MaximumDrawdownCriterion().calculate(series, record);

System.out.printf("Net return: %.2f%%%n",
        netReturn.multipliedBy(series.numFactory().numOf(100)).doubleValue());
System.out.printf("Max drawdown: %.2f%%%n",
        maxDrawdown.multipliedBy(series.numFactory().numOf(100)).doubleValue());
Ta4j ships with 30+ criteria including Sharpe ratio, win rate, profit factor, and Calmar ratio.

Running multiple strategies

To compare strategies across a parameter grid, generate them in a loop and run them in parallel with BacktestExecutor:
import org.ta4j.core.backtest.BacktestExecutor;
import org.ta4j.core.reports.TradingStatement;

List<Strategy> strategies = new ArrayList<>();
for (int fast = 5; fast <= 20; fast += 5) {
    for (int slow = 20; slow <= 50; slow += 10) {
        EMAIndicator fastEma = new EMAIndicator(close, fast);
        EMAIndicator slowEma = new EMAIndicator(close, slow);
        strategies.add(new BaseStrategy(
                "EMA(" + fast + "," + slow + ")",
                new CrossedUpIndicatorRule(fastEma, slowEma),
                new CrossedDownIndicatorRule(fastEma, slowEma)
        ));
    }
}

BacktestExecutionResult result = new BacktestExecutor(series)
        .executeWithRuntimeReport(strategies,
                series.numFactory().numOf(1),
                Trade.TradeType.BUY,
                ProgressCompletion.loggingWithMemory());

// Rank by composite score: 70% net profit, 30% return/max-drawdown
List<TradingStatement> top10 = result.getTopStrategiesWeighted(10,
        WeightedCriterion.of(new NetProfitCriterion(), 7.0),
        WeightedCriterion.of(new ReturnOverMaxDrawdownCriterion(), 3.0));

Walk-forward testing

Single backtests are susceptible to overfitting. BarSeriesManager.runWalkForward() splits the series into in-sample training windows and out-of-sample validation windows:
import org.ta4j.core.walkforward.WalkForwardConfig;
import org.ta4j.core.backtest.StrategyWalkForwardExecutionResult;

WalkForwardConfig config = WalkForwardConfig.builder()
        .withInSampleBars(252)   // ~1 year of daily bars for training
        .withOutOfSampleBars(63) // ~1 quarter for validation
        .build();

StrategyWalkForwardExecutionResult wfResult =
        manager.runWalkForward(strategy, config);

Backtesting pitfalls

Look-ahead bias: Every indicator or rule that reads data beyond the current bar index introduces look-ahead bias. Ta4j indicators are designed to avoid this — result at index i depends only on bars beginIndex through i. Do not access series.getBar(i + 1) or later inside a rule.
Overfitting: A backtest that looks exceptional across a single time window is likely overfit to that data. Use walk-forward analysis, out-of-sample validation, or hold-out periods before trusting a result.
Survivorship bias: Historical data sources often include only assets that still exist today. Strategies that look good on surviving assets may have failed on assets that were delisted during the test period.
Unrealistic cost assumptions: The default ZeroCostModel applies no fees. Always add realistic transaction costs before drawing conclusions about net profitability.