Documentation, examples and further information of the ta4j project
This project is maintained by ta4j Organization
Backtesting estimates how a strategy would have behaved over historical data. In current ta4j, the default building blocks are:
BarSeriesManager for one strategy over one seriesBacktestExecutor for many strategies over one seriesBaseTradingRecord as the trading-state object underneath both backtest and live-style flows| Need | Recommended path | Notes |
|---|---|---|
| One strategy, minimal setup | BarSeriesManager.run(strategy) |
Creates a fresh BaseTradingRecord through the manager’s configured factory and defaults to next-open execution |
| One strategy with a preconfigured record | BarSeriesManager.run(strategy, tradingRecord, ...) |
Reuse a record instance or keep a custom ExecutionMatchPolicy / fee setup |
| Many strategies or tuning | BacktestExecutor |
Builds on the same next-open default, collects TradingStatements, and adds telemetry plus ranking helpers |
| Event-driven or fill-driven replay | Manual loop + BaseTradingRecord |
Use when fills do not happen exactly where the default execution model would place them |
| Older live-oriented adapters | LiveTradingRecord |
Compatibility facade only; not recommended for new backtests |
The main thing to keep in mind is that you do not need a manual loop just to get open-lot views, recorded fees, or open-position criteria. BaseTradingRecord already exposes getCurrentPosition(), getOpenPositions(), and recorded-fee-aware metrics.
BarSeriesManagerFor a normal single-strategy backtest, start here:
BarSeriesManager manager = new BarSeriesManager(series);
TradingRecord record = manager.run(strategy);
System.out.println("Closed positions: " + record.getPositionCount());
System.out.println("Open position? " + record.getCurrentPosition().isOpened());
BarSeriesManager handles the bar-by-bar loop, applies the configured TradeExecutionModel, and returns the resulting trading record.
If you also need specific cost models or execution semantics, configure them on the manager:
BarSeriesManager manager = new BarSeriesManager(
series,
new LinearTransactionCostModel(0.001),
new ZeroCostModel(),
new TradeOnNextOpenModel());
BaseTradingRecordRecent ta4j versions let BarSeriesManager run directly against a record you provide. That is the right choice when you want to preserve a specific match policy, start and end window, or recorded-fee behavior.
BaseTradingRecord record = new BaseTradingRecord(
strategy.getStartingType(),
ExecutionMatchPolicy.FIFO,
new ZeroCostModel(),
new ZeroCostModel(),
series.getBeginIndex(),
series.getEndIndex());
BarSeriesManager manager = new BarSeriesManager(series);
manager.run(strategy, record, series.numFactory().one(), series.getBeginIndex(), series.getEndIndex());
If you want every default run(...) overload to create your preferred record shape, provide a custom trading-record factory to the manager constructor.
The maintained parity example for this flow is TradingRecordParityBacktest, which compares:
BarSeriesManager runBaseTradingRecordBacktestExecutorWhen you want to rank many strategies, switch to BacktestExecutor:
BacktestExecutor executor = new BacktestExecutor(series);
BacktestExecutionResult result = executor.executeWithRuntimeReport(
strategies,
series.numFactory().one(),
Trade.TradeType.BUY,
ProgressCompletion.logging("wiki.backtesting"));
List<TradingStatement> topRuns = result.getTopStrategiesWeighted(
20,
WeightedCriterion.of(new NetProfitCriterion(), 7.0),
WeightedCriterion.of(new ReturnOverMaxDrawdownCriterion(), 3.0));
Use BacktestExecutor when you care about:
BacktestRuntimeReport)You have the same execution-wiring flexibility here as in BarSeriesManager: use new BacktestExecutor(series, tradeExecutionModel) for the common slippage/stop-limit case, or pass a preconfigured BarSeriesManager when you want a custom TradingRecordFactory or other manager-level defaults to flow through every batch run.
Choose the ranking style that matches the job:
getTopStrategies(...) for simple lexicographic ranking by one or more criteriagetTopStrategiesWeighted(...) plus WeightedCriterion.of(...) when you want normalized weighted scoring across different metricsexecuteAndKeepTopK(...) when the candidate set is so large that you want streaming top-K retention with one primary criterion instead of materializing every statementManual loops still matter, but for a narrower reason than before. Use them when execution itself is the thing you are modeling:
Deterministic custom loop:
BaseTradingRecord record = new BaseTradingRecord(strategy.getStartingType());
Num amount = series.numFactory().one();
for (int i = series.getBeginIndex(); i <= series.getEndIndex(); i++) {
Num price = series.getBar(i).getClosePrice();
if (strategy.shouldEnter(i, record)) {
record.enter(i, price, amount);
} else if (strategy.shouldExit(i, record)) {
record.exit(i, price, amount);
}
}
Fill-driven replay:
BaseTradingRecord record = new BaseTradingRecord(strategy.getStartingType());
record.operate(new TradeFill(
42,
Instant.parse("2025-01-02T10:15:00Z"),
series.numFactory().numOf("42100"),
series.numFactory().numOf("0.50"),
series.numFactory().numOf("4.21"),
ExecutionSide.BUY,
"order-42",
"decision-42"));
That same fill-driven pattern is what you will use in live or paper-trading systems when the broker is the source of truth for fills. If you already have the full partial-fill batch for one logical order, keep it together with Trade.fromFills(...) and pass the grouped trade into operate(...) instead.
Once you have a TradingRecord, the same analysis layer works no matter how the record was produced:
AnalysisCriterion netReturn = new NetReturnCriterion();
AnalysisCriterion totalFees = new TotalFeesCriterion();
AnalysisCriterion openCostBasis = new OpenPositionCostBasisCriterion();
AnalysisCriterion openUnrealized = new OpenPositionUnrealizedProfitCriterion();
System.out.println(netReturn.calculate(series, record));
System.out.println(totalFees.calculate(series, record));
System.out.println(openCostBasis.calculate(series, record));
System.out.println(openUnrealized.calculate(series, record));
Useful companions:
BaseTradingStatementCommissionsImpactPercentageCriterionPositionDurationCriterionRMultipleCriterionMonteCarloMaximumDrawdownCriterionFor open exposure, prefer getCurrentPosition() as the canonical net-open view and getOpenPositions() when you want one snapshot per remaining lot. getNetOpenPosition() remains available only as a compatibility alias.
For visualization, combine the resulting record with the ChartWorkflow APIs documented in Charting.
After you have a TradingRecord, render it with ChartWorkflow or inspect it with StrategyExecutionLogging from ta4j-examples. That is often the fastest way to catch look-ahead bias, missing warmup bars, or surprising execution timing before you trust a criterion leaderboard.
strategy.setUnstableBars(n) so signals do not fire before your indicators stabilize.setMaximumBarCount, do not evaluate criteria against evicted bars.TradeExecutionModel, fees, and borrowing costs aligned with what you are trying to simulate.Use walk-forward execution when you want training and testing windows instead of one monolithic run:
WalkForwardConfig config = WalkForwardConfig.builder()
.trainingBars(500)
.testingBars(100)
.build();
StrategyWalkForwardExecutionResult walkForward = new BarSeriesManager(series)
.runWalkForward(strategy, config);
For large-scale performance tuning, use BacktestPerformanceTuningHarness, which sits on top of BacktestExecutor.
LiveTradingRecord and ExecutionFill still exist for 0.22.x migration paths, but they are no longer the preferred way to explain or build new backtests.