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.

ta4j runs strategy backtests on the JVM with no GIL bottleneck. BacktestExecutor saturates all available CPU cores, making it practical to evaluate hundreds or thousands of parameter combinations in seconds.

Running a single strategy

For a single strategy, use BarSeriesManager directly:
import org.ta4j.core.backtest.BarSeriesManager;

BarSeriesManager manager = new BarSeriesManager(series);
TradingRecord record = manager.run(strategy);

Running multiple strategies in parallel

BacktestExecutor accepts a list of strategies and runs them concurrently:
import org.ta4j.core.backtest.BacktestExecutor;
import org.ta4j.core.backtest.BacktestExecutionResult;
import org.ta4j.core.backtest.ProgressCompletion;
import org.ta4j.core.reports.TradingStatement;

BacktestExecutionResult result = new BacktestExecutor(series)
        .executeWithRuntimeReport(
                strategies,
                series.numFactory().numOf(1),  // position size: 1 unit
                Trade.TradeType.BUY,            // long positions
                ProgressCompletion.loggingWithMemory()); // logs progress + memory stats
For strategy counts above 1000, BacktestExecutor automatically switches to batched sequential processing to avoid memory exhaustion.

Parameter sweep example

Generate strategies across a grid of parameters, run them all, and pick the top performers:
import org.ta4j.core.indicators.EMAIndicator;
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
import org.ta4j.core.rules.CrossedUpIndicatorRule;
import org.ta4j.core.rules.CrossedDownIndicatorRule;
import org.ta4j.core.BaseStrategy;
import org.ta4j.core.criteria.pnl.NetProfitCriterion;
import org.ta4j.core.criteria.ReturnOverMaxDrawdownCriterion;

ClosePriceIndicator close = new ClosePriceIndicator(series);

// Generate strategies with varying EMA periods
List<Strategy> strategies = new ArrayList<>();
for (int fastPeriod = 5; fastPeriod <= 20; fastPeriod += 5) {
    for (int slowPeriod = 20; slowPeriod <= 50; slowPeriod += 10) {
        EMAIndicator fastEma = new EMAIndicator(close, fastPeriod);
        EMAIndicator slowEma = new EMAIndicator(close, slowPeriod);
        Rule entry = new CrossedUpIndicatorRule(fastEma, slowEma);
        Rule exit  = new CrossedDownIndicatorRule(fastEma, slowEma);
        strategies.add(new BaseStrategy(
                "EMA(" + fastPeriod + "," + slowPeriod + ")", entry, exit));
    }
}

// Run all strategies with progress tracking
BacktestExecutionResult result = new BacktestExecutor(series)
        .executeWithRuntimeReport(
                strategies,
                series.numFactory().numOf(1),
                Trade.TradeType.BUY,
                ProgressCompletion.loggingWithMemory());

// Composite ranking: 70% weight on net profit, 30% on return over max drawdown
AnalysisCriterion netProfit  = new NetProfitCriterion();
AnalysisCriterion romad      = new ReturnOverMaxDrawdownCriterion();

// Weights are normalised internally; 7/3 and 0.7/0.3 are equivalent
List<TradingStatement> top10 = result.getTopStrategiesWeighted(10,
        WeightedCriterion.of(netProfit, 7.0),
        WeightedCriterion.of(romad, 3.0));

// Review the winners
top10.forEach(statement -> {
    System.out.printf("Strategy: %s, Net Profit: %s, RoMaD: %s%n",
            statement.getStrategy().getName(),
            statement.getCriterionScore(netProfit).orElse(series.numOf(0)),
            statement.getCriterionScore(romad).orElse(series.numOf(0)));
});

Progress reporting

ProgressCompletion provides ready-made callbacks to track execution:
// No-op (silent)
ProgressCompletion.noOp()

// Log progress at the default interval (every 100 completions + 25/50/75/100%)
ProgressCompletion.logging()

// Log progress at a custom interval
ProgressCompletion.logging(50)

// Log progress with memory stats (heap used/free/max)
ProgressCompletion.loggingWithMemory()

// Pass a custom callback (receives completed count)
Consumer<Integer> myCallback = count -> updateProgressBar(count);
Progress is logged at TRACE level, so it does not appear unless trace logging is enabled.

Accessing results

getTopStrategiesWeighted

Returns the top N strategies sorted by a composite weighted score:
List<TradingStatement> top = result.getTopStrategiesWeighted(10,
        WeightedCriterion.of(new NetProfitCriterion(), 0.7),
        WeightedCriterion.of(new SharpeRatioCriterion(), 0.3));

getTopStrategies (lexicographic)

Returns the top N strategies using the first criterion as primary sort key and subsequent criteria as tie-breakers:
List<TradingStatement> top = result.getTopStrategies(10,
        new NetProfitCriterion(),
        new SharpeRatioCriterion());

TradingStatement

Each TradingStatement carries the strategy, its trading record, and pre-computed criterion scores:
TradingStatement statement = top10.get(0);

statement.getStrategy().getName();   // strategy name
statement.getTradingRecord();        // full trading record
statement.getPositionStatsReport();  // win rate, avg profit, etc.
statement.getPerformanceReport();    // return, drawdown, etc.

// Access pre-computed criterion scores (no recalculation needed)
statement.getCriterionScore(netProfit).orElse(series.numOf(0));

Memory-efficient top-K execution

For very large parameter sweeps (10,000+ strategies), use executeAndKeepTopK to minimise memory usage. It uses a min-heap to track only the top K results and discards worse performers immediately:
BacktestExecutionResult result = new BacktestExecutor(series)
        .executeAndKeepTopK(
                strategies,
                series.numFactory().numOf(1),
                Trade.TradeType.BUY,
                new NetProfitCriterion(),
                10,   // keep only the top 10
                ProgressCompletion.loggingWithMemory());
Memory usage is O(K + batchSize) instead of O(strategyCount).

Walk-forward analysis

Walk-forward analysis splits the data into consecutive in-sample and out-of-sample windows to test whether a strategy generalises beyond its training period.
import org.ta4j.core.walkforward.WalkForwardConfig;

WalkForwardConfig config = WalkForwardConfig.builder()
        .trainBars(252)   // 1 year in-sample
        .testBars(63)     // 3 months out-of-sample
        .build();

BacktestExecutor executor = new BacktestExecutor(series);
StrategyWalkForwardExecutionResult wf = executor.executeWalkForward(strategy, config);
For a detailed guide, see the advanced walk-forward documentation.

Performance tips

ta4j supports two numeric types:
  • DecimalNum (default): arbitrary-precision arithmetic. More accurate, slower.
  • DoubleNum: double-backed arithmetic. Faster, acceptable precision for most strategies.
To use DoubleNum for a parameter sweep:
import org.ta4j.core.num.DoubleNumFactory;

BarSeries series = new BaseBarSeriesBuilder()
        .withNumFactory(DoubleNumFactory.getInstance())
        .withName("BTC-USD")
        .build();
This can meaningfully reduce backtest time for large sweeps.
If your strategy only needs a rolling window of history (for example the last 200 bars), set maximumBarCount to cap memory usage:
series.setMaximumBarCount(500); // keep only the last 500 bars
This is especially useful for live series that grow continuously.
When evaluating more than a few thousand strategies and you only need the top performers, executeAndKeepTopK avoids storing all results in memory.

Performance metrics

Understand the criteria you use for ranking.

Charting

Visualise the top performers after the sweep.