Documentation, examples and further information of the ta4j project
This project is maintained by ta4j Organization
This guide takes you from a fresh checkout to a working strategy, then shows how to choose between ta4j’s three main execution styles:
BarSeriesManager for straightforward historical runsBacktestExecutor for large batchesBaseTradingRecord when fills arrive asynchronouslyIf technical analysis is new to you, skim Wikipedia or Investopedia for terminology.
Start with the latest released line unless you specifically want unreleased master APIs:
<dependency>
<groupId>org.ta4j</groupId>
<artifactId>ta4j-core</artifactId>
<version>0.22.3</version>
</dependency>
If you want the newest development APIs described on this wiki, install the current snapshot locally first:
mvn -pl ta4j-core -am install
Then depend on ta4j-core with the snapshot version:
<dependency>
<groupId>org.ta4j</groupId>
<artifactId>ta4j-core</artifactId>
<version>0.22.4-SNAPSHOT</version>
</dependency>
implementation "org.ta4j:ta4j-core:0.22.4-SNAPSHOT"
If you are consuming a released build from Maven Central instead, replace the version with the newest released 0.22.x number from Release Notes. If a page mentions an API you do not see in 0.22.3 yet, that is your cue to build the current snapshot locally.
Prefer to inspect the code? Clone ta4j and import the root Maven project. ta4j-core and ta4j-examples live side by side.
mvn -pl ta4j-examples testta4jexamples.Quickstart| Concept | What it does | Learn more |
|---|---|---|
BarSeries |
Holds the ordered bars used by indicators and strategies | Bar Series & Bars |
Indicator<T extends Num> |
Derives values lazily from bars or other indicators | Technical Indicators |
Rule and Strategy |
Decide when to enter, exit, or stay flat | Trading Strategies |
BaseTradingRecord |
Unified trading state for backtests, live trading, and paper trading | Backtesting, Live Trading |
BarSeriesManager |
Runs one strategy over a series | Backtesting |
BacktestExecutor |
Runs many strategies and ranks the results | Backtesting |
ConcurrentBarSeries |
Thread-safe series for multi-threaded ingestion and evaluation | Live Trading |
The important mental model is that ta4j no longer needs a split “backtest record” versus “live record” story for new code. BaseTradingRecord already covers both.
We will:
BarSeries series = new BaseBarSeriesBuilder()
.withName("btc-usd-demo")
.build();
series.barBuilder()
.timePeriod(Duration.ofMinutes(5))
.endTime(Instant.parse("2025-01-01T00:05:00Z"))
.openPrice(42000)
.highPrice(42150)
.lowPrice(41980)
.closePrice(42100)
.volume(12.4)
.add();
If you already have bar data, call series.addBar(...). If you are aggregating raw trades, use the appropriate bar builder or a ConcurrentBarSeries in live flows.
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
SMAIndicator fastSma = new SMAIndicator(closePrice, 5);
SMAIndicator slowSma = new SMAIndicator(closePrice, 30);
Rule entryRule = new CrossedUpIndicatorRule(fastSma, slowSma);
Rule exitRule = new CrossedDownIndicatorRule(fastSma, slowSma)
.or(new StopLossRule(closePrice, series.numFactory().numOf(3)))
.or(new StopGainRule(closePrice, series.numFactory().numOf(5)));
Strategy strategy = new BaseStrategy("SMA crossover", entryRule, exitRule);
strategy.setUnstableBars(30);
| Goal | Recommended path | Why |
|---|---|---|
| One strategy over historical bars | BarSeriesManager |
Minimal wiring and deterministic trade-execution models |
| Same backtest loop, but with a preconfigured record | BarSeriesManager.run(strategy, tradingRecord, ...) |
Keep a specific ExecutionMatchPolicy, fee model, or record instance |
| Large parameter sweeps | BacktestExecutor |
Batched execution, runtime reports, and weighted ranked statements |
| Live or paper execution with confirmed fills | Manual loop + BaseTradingRecord |
Signal generation stays separate from fill recording |
For the common case, start with BarSeriesManager:
BarSeriesManager manager = new BarSeriesManager(series);
TradingRecord record = manager.run(strategy);
System.out.printf("Closed positions: %d%n", record.getPositionCount());
System.out.printf("Current position open? %s%n", record.getCurrentPosition().isOpened());
If you need a specific record configuration, provide your own BaseTradingRecord:
BaseTradingRecord record = new BaseTradingRecord(
strategy.getStartingType(),
ExecutionMatchPolicy.FIFO,
new ZeroCostModel(),
new ZeroCostModel(),
series.getBeginIndex(),
series.getEndIndex());
manager.run(strategy, record, series.numFactory().one(), series.getBeginIndex(), series.getEndIndex());
AnalysisCriterion netReturn = new NetReturnCriterion();
AnalysisCriterion romad = new ReturnOverMaxDrawdownCriterion();
AnalysisCriterion openCostBasis = new OpenPositionCostBasisCriterion();
System.out.println("Net return: " + netReturn.calculate(series, record));
System.out.println("Return over max drawdown: " + romad.calculate(series, record));
System.out.println("Open position cost basis: " + openCostBasis.calculate(series, record));
Useful follow-up metrics:
TotalFeesCriterionCommissionsImpactPercentageCriterionOpenPositionUnrealizedProfitCriterionMaxConsecutiveProfitCriterionMaxConsecutiveLossCriterionBaseTradingStatementIf you want to compare many parameter combinations instead of one strategy, move next to BacktestExecutor and SimpleMovingAverageRangeBacktest, which show the current weighted shortlist flow with WeightedCriterion.of(...) and getTopStrategiesWeighted(...).
When your orders are filled asynchronously, do not mutate the record at signal time. Emit the order intent first, then update the record from the confirmed fill:
BaseTradingRecord liveRecord = new BaseTradingRecord(strategy.getStartingType());
int endIndex = series.getEndIndex();
Num lastPrice = series.getBar(endIndex).getClosePrice();
Num amount = series.numFactory().one();
if (strategy.shouldEnter(endIndex, liveRecord)) {
orderRouter.submitBuy(lastPrice, amount);
}
TradeFill fill = new TradeFill(
endIndex,
Instant.now(),
lastPrice,
amount,
series.numFactory().zero(),
ExecutionSide.BUY,
"order-123",
"decision-123");
liveRecord.operate(fill);
If your exchange hands you the full partial-fill batch for one logical order, group it with Trade.fromFills(...) and pass that through operate(...) instead. Either way, the pattern is the same: ta4j strategies decide, brokers execute, then BaseTradingRecord is updated from confirmed fills.
| Goal | Where to go next |
|---|---|
| Learn data-loading and streaming patterns | Bar Series & Bars |
| Explore more indicators | Technical Indicators |
| Run larger backtests | Backtesting |
| Build a real bot loop | Live Trading |
| Run maintained examples | Usage Examples |
LiveTradingRecord and ExecutionFill still exist in the 0.22.x line so older adapters can migrate gradually, but new code should use BaseTradingRecord and TradeFill.