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. For execution-path selection and full onboarding routing, use canonical pages:
If technical analysis is new to you, skim Wikipedia or Investopedia for terminology.
Use the canonical dependency snippets in the repository README:
If you need current master APIs that are not yet on Maven Central, install locally first:
mvn -pl ta4j-core -am install
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);
For execution-path decisions, use Execution Decision Matrix. For the common starter case, run one strategy 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.