Skip to main content
This guide walks you through loading historical price data, building an EMA crossover strategy, running a backtest, and reading the results. All steps use the public ta4j API — no external services or API keys required.
Want to run a complete, self-contained example first? Clone the repository and execute the bundled Quickstart demo:
git clone https://github.com/ta4j/ta4j.git
cd ta4j
./mvnw clean install -DskipTests
./mvnw -pl ta4j-examples exec:java -Dexec.mainClass=ta4jexamples.Quickstart
This loads historical Bitcoin data, runs a full strategy, prints performance metrics, and displays an interactive chart.

Prerequisites

  • JDK 21 or later. ta4j requires Java 21+. Check your version with java -version.
  • Maven 3.9+ or Gradle 8+. Either build tool works. The examples below cover both.

Steps

1

Add the dependency

Add ta4j-core to your project. If you want to use the built-in data loaders (Yahoo Finance, Coinbase, CSV), also add ta4j-examples.
<dependency>
  <groupId>org.ta4j</groupId>
  <artifactId>ta4j-core</artifactId>
  <version>0.22.5</version>
</dependency>

<!-- Optional: data loaders, charting helpers, and runnable examples -->
<dependency>
  <groupId>org.ta4j</groupId>
  <artifactId>ta4j-examples</artifactId>
  <version>0.22.5</version>
</dependency>
2

Load historical price data

A BarSeries holds the OHLCV bars your strategy runs on. The quickest way to get real data is YahooFinanceHttpBarSeriesDataSource from ta4j-examples — no API key required.
import ta4jexamples.datasources.YahooFinanceHttpBarSeriesDataSource;
import org.ta4j.core.BarSeries;
import java.time.Duration;
import java.time.Instant;

// Enable response caching to avoid hitting rate limits during development
YahooFinanceHttpBarSeriesDataSource dataSource =
    new YahooFinanceHttpBarSeriesDataSource(true);

// Load one year of daily bars for Apple
BarSeries series = dataSource.loadSeries(
    "AAPL",
    Duration.ofDays(1),
    Instant.parse("2023-01-01T00:00:00Z"),
    Instant.parse("2023-12-31T23:59:59Z"));
If you prefer not to add ta4j-examples, build a series manually with BaseBarSeriesBuilder and add bars from your own data source:
import org.ta4j.core.BarSeries;
import org.ta4j.core.builder.BaseBarSeriesBuilder;
import org.ta4j.core.builder.BarBuilder;
import java.time.ZonedDateTime;

BarSeries series = new BaseBarSeriesBuilder().withName("AAPL").build();

// Add bars from your CSV, database, or broker API
series.addBar(
    series.barBuilder()
        .timePeriod(Duration.ofDays(1))
        .endTime(ZonedDateTime.parse("2023-01-03T16:00:00-05:00"))
        .openPrice(130.28)
        .highPrice(133.41)
        .lowPrice(129.89)
        .closePrice(125.07)
        .volume(112117500)
        .build());
// ... add remaining bars
3

Create indicators

Indicators calculate values from bar data. Start with ClosePriceIndicator as the base, then layer derived indicators on top. Here we build a 12-period and 26-period exponential moving average.
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
import org.ta4j.core.indicators.EMAIndicator;

ClosePriceIndicator close = new ClosePriceIndicator(series);
EMAIndicator fastEma = new EMAIndicator(close, 12);  // 12-period EMA
EMAIndicator slowEma = new EMAIndicator(close, 26);  // 26-period EMA
You can inspect any indicator value at a specific bar index:
int lastBar = series.getEndIndex();
System.out.println("Fast EMA at last bar: " + fastEma.getValue(lastBar));
System.out.println("Slow EMA at last bar: " + slowEma.getValue(lastBar));
4

Define entry and exit rules

Rules are boolean conditions that the backtesting engine evaluates at each bar. The entry rule fires when the fast EMA crosses above the slow EMA — a classic golden cross signal. The exit rule closes the position when the price drops 1.5% (stop loss) or gains 3% (take profit).
import org.ta4j.core.Rule;
import org.ta4j.core.rules.CrossedUpIndicatorRule;
import org.ta4j.core.rules.StopLossRule;
import org.ta4j.core.rules.StopGainRule;

// Enter when fast EMA crosses above slow EMA (golden cross)
Rule entryRule = new CrossedUpIndicatorRule(fastEma, slowEma);

// Exit when the price drops 1.5% OR gains 3%
Rule exitRule = new StopGainRule(close, 3.0)
        .or(new StopLossRule(close, 1.5));
Rules compose with .and(), .or(), and .xor(). For example, require an RSI confirmation on entry:
import org.ta4j.core.indicators.RSIIndicator;
import org.ta4j.core.rules.OverIndicatorRule;

RSIIndicator rsi = new RSIIndicator(close, 14);

// Enter only when EMA crosses up AND RSI is above 50 (momentum confirmation)
Rule entryRule = new CrossedUpIndicatorRule(fastEma, slowEma)
        .and(new OverIndicatorRule(rsi, 50));
5

Build the strategy

Wrap your entry and exit rules into a BaseStrategy. Give it a name — this appears in reports and charts.
import org.ta4j.core.Strategy;
import org.ta4j.core.BaseStrategy;

Strategy strategy = new BaseStrategy("EMA Crossover", entryRule, exitRule);
6

Run the backtest

BarSeriesManager iterates over every bar in the series, evaluates your rules, and records every trade. Pass a transaction cost model to get realistic results.
import org.ta4j.core.backtest.BarSeriesManager;
import org.ta4j.core.TradingRecord;
import org.ta4j.core.cost.LinearTransactionCostModel;
import org.ta4j.core.cost.LinearBorrowingCostModel;

// Run without costs (simple)
TradingRecord record = new BarSeriesManager(series).run(strategy);

// Run with realistic costs (0.1% transaction fee + 0.01% borrowing cost)
TradingRecord recordWithCosts = new BarSeriesManager(
        series,
        new LinearTransactionCostModel(0.001),
        new LinearBorrowingCostModel(0.0001))
    .run(strategy);
7

Inspect the results

TradingRecord holds every trade that was opened and closed during the backtest. Use analysis criteria to calculate performance metrics.
import org.ta4j.core.criteria.pnl.NetReturnCriterion;
import org.ta4j.core.criteria.pnl.NetProfitCriterion;
import org.ta4j.core.criteria.drawdown.MaximumDrawdownCriterion;

// Basic trade counts
System.out.println("Trades executed:  " + record.getTradeCount());
System.out.println("Positions closed: " + record.getPositionCount());

// Performance metrics
NetReturnCriterion netReturn = new NetReturnCriterion();
MaximumDrawdownCriterion maxDrawdown = new MaximumDrawdownCriterion();

System.out.printf("Net return:    %.4f%n",
    netReturn.calculate(series, record).doubleValue());
System.out.printf("Max drawdown:  %.4f%n",
    maxDrawdown.calculate(series, record).doubleValue());
A net return of 1.08 means the strategy returned 8% over the period. A max drawdown of 0.12 means the largest peak-to-trough decline was 12%.

Complete example

Here is the full strategy assembled in one block, ready to paste into your project:
import org.ta4j.core.*;
import org.ta4j.core.backtest.BarSeriesManager;
import org.ta4j.core.criteria.pnl.NetReturnCriterion;
import org.ta4j.core.criteria.drawdown.MaximumDrawdownCriterion;
import org.ta4j.core.indicators.EMAIndicator;
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
import org.ta4j.core.rules.CrossedUpIndicatorRule;
import org.ta4j.core.rules.StopGainRule;
import org.ta4j.core.rules.StopLossRule;
import ta4jexamples.datasources.YahooFinanceHttpBarSeriesDataSource;
import java.time.Duration;
import java.time.Instant;

// 1. Load data
YahooFinanceHttpBarSeriesDataSource dataSource =
    new YahooFinanceHttpBarSeriesDataSource(true);
BarSeries series = dataSource.loadSeries(
    "AAPL",
    Duration.ofDays(1),
    Instant.parse("2023-01-01T00:00:00Z"),
    Instant.parse("2023-12-31T23:59:59Z"));

// 2. Build indicators
ClosePriceIndicator close = new ClosePriceIndicator(series);
EMAIndicator fastEma = new EMAIndicator(close, 12);
EMAIndicator slowEma = new EMAIndicator(close, 26);

// 3. Define rules
Rule entryRule = new CrossedUpIndicatorRule(fastEma, slowEma);
Rule exitRule  = new StopGainRule(close, 3.0).or(new StopLossRule(close, 1.5));

// 4. Build strategy
Strategy strategy = new BaseStrategy("EMA Crossover", entryRule, exitRule);

// 5. Run backtest
TradingRecord record = new BarSeriesManager(series).run(strategy);

// 6. Report results
System.out.println("Trades: " + record.getTradeCount());
System.out.printf("Net return:   %.4f%n",
    new NetReturnCriterion().calculate(series, record).doubleValue());
System.out.printf("Max drawdown: %.4f%n",
    new MaximumDrawdownCriterion().calculate(series, record).doubleValue());

Next steps

Core concepts

Understand how BarSeries, Indicators, Rules, and Strategies fit together.

Data sources

Load data from Yahoo Finance, Coinbase, CSV files, or your own broker API.

Performance metrics

Sharpe ratio, win rate, profit factor, return over max drawdown, and more.

Live trading

Use the same strategy code to drive live order execution.