Documentation, examples and further information of the ta4j project
This project is maintained by ta4j Organization
Ta4j provides powerful charting capabilities through the ChartWorkflow class and its fluent ChartBuilder API. You can create sophisticated trading charts with candlestick data, indicators, trading records, and analysis criteria overlays.
The charting system in ta4j-examples uses JFreeChart to render professional-quality trading charts. The refactored architecture (introduced in 0.19) separates concerns into dedicated components:
ChartWorkflow - Facade class that coordinates chart creation, display, and persistenceChartBuilder - Fluent API for constructing charts with type-safe stage interfacesTradingChartFactory - Renders JFreeChart instances from chart definitionsChartDisplayer - Interface for displaying charts (Swing implementation included)ChartStorage - Interface for persisting charts (filesystem implementation included)The API supports:
AnalysisCriterionIndicatorTo use charting in your project, you’ll need to include the ta4j-examples module as a dependency:
<dependency>
<groupId>org.ta4j</groupId>
<artifactId>ta4j-examples</artifactId>
<version>${USE_LATEST_VERSION}</version>
</dependency>
The simplest way to create a chart is using the ChartBuilder fluent API:
ChartWorkflow chartWorkflow = new ChartWorkflow();
chartWorkflow.builder()
.withSeries(series)
.display();
The ChartBuilder provides a fluent, stream-like API for constructing charts. It enforces valid transitions through stage interfaces and prevents accidental reuse (similar to Java Streams).
Start with a candlestick chart using withSeries():
ChartWorkflow chartWorkflow = new ChartWorkflow();
JFreeChart chart = chartWorkflow.builder()
.withSeries(series)
.toChart();
Start with an indicator as the base:
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
JFreeChart chart = chartWorkflow.builder()
.withIndicator(closePrice)
.toChart();
Overlays are added to the current chart and share the same plot area. The builder automatically manages axis assignment based on value ranges.
Add indicator overlays to visualize technical indicators on your price chart:
SMAIndicator sma = new SMAIndicator(closePrice, 50);
JFreeChart chart = chartWorkflow.builder()
.withSeries(series)
.withIndicatorOverlay(sma)
.toChart();
You can style indicator overlays with custom colors and line widths:
JFreeChart chart = chartWorkflow.builder()
.withSeries(series)
.withIndicatorOverlay(sma)
.withLineColor(Color.CYAN)
.withLineWidth(2.0f)
.toChart();
Display buy/sell signals and position markers:
BarSeriesManager seriesManager = new BarSeriesManager(series);
TradingRecord tradingRecord = seriesManager.run(strategy);
JFreeChart chart = chartWorkflow.builder()
.withSeries(series)
.withTradingRecordOverlay(tradingRecord)
.toChart();
Trading record overlays show:
Visualize analysis criteria (like net profit, return, etc.) over time on a secondary axis:
NetProfitCriterion netProfit = new NetProfitCriterion();
JFreeChart chart = chartWorkflow.builder()
.withSeries(series)
.withTradingRecordOverlay(tradingRecord)
.withAnalysisCriterionOverlay(netProfit, tradingRecord)
.toChart();
The AnalysisCriterionIndicator automatically calculates the criterion value at each bar index using a partial trading record, creating a time series visualization of performance metrics.
How it works: For each bar index, AnalysisCriterionIndicator creates a partial trading record containing only positions that have been entered (and optionally closed) up to that index, then calculates the criterion value for that partial record. This allows you to see how performance metrics evolve over time as trades are executed.
Example use cases:
// Visualize multiple performance metrics
NetProfitCriterion netProfit = new NetProfitCriterion();
GrossReturnCriterion grossReturn = new GrossReturnCriterion();
MaximumDrawdownCriterion maxDrawdown = new MaximumDrawdownCriterion();
chartWorkflow.builder()
.withSeries(series)
.withTradingRecordOverlay(tradingRecord)
.withAnalysisCriterionOverlay(netProfit, tradingRecord)
.withLineColor(Color.GREEN)
.withSubChart(grossReturn, tradingRecord)
.withSubChart(maxDrawdown, tradingRecord)
.display();
Sub-charts create separate panels below the main chart, each with its own Y-axis. This is useful for indicators with different scales or for detailed analysis.
RSIIndicator rsi = new RSIIndicator(closePrice, 14);
JFreeChart chart = chartWorkflow.builder()
.withSeries(series)
.withSubChart(rsi)
.toChart();
Create a dedicated panel for trading visualization:
JFreeChart chart = chartWorkflow.builder()
.withSeries(series)
.withSubChart(tradingRecord)
.toChart();
GrossReturnCriterion grossReturn = new GrossReturnCriterion();
JFreeChart chart = chartWorkflow.builder()
.withSeries(series)
.withSubChart(grossReturn, tradingRecord)
.toChart();
You can combine multiple overlays and sub-charts in a single chart:
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
SMAIndicator sma = new SMAIndicator(closePrice, 50);
RSIIndicator rsi = new RSIIndicator(closePrice, 14);
ADXIndicator adx = new ADXIndicator(series, 14);
JFreeChart chart = chartWorkflow.builder()
.withSeries(series)
.withTradingRecordOverlay(tradingRecord)
.withIndicatorOverlay(sma)
.withLineColor(Color.ORANGE)
.withSubChart(rsi)
.withSubChart(adx)
.withSubChart(new NetProfitCriterion(), tradingRecord)
.toChart();
Set a custom title for your chart:
JFreeChart chart = chartWorkflow.builder()
.withTitle("My Trading Strategy Analysis")
.withSeries(series)
.withTradingRecordOverlay(tradingRecord)
.toChart();
Or set the title after configuring the base chart:
JFreeChart chart = chartWorkflow.builder()
.withSeries(series)
.withTitle("Custom Title")
.withTradingRecordOverlay(tradingRecord)
.toChart();
The builder supports three terminal operations that consume the builder (preventing reuse):
Display the chart in a Swing window:
chartWorkflow.builder()
.withSeries(series)
.withTradingRecordOverlay(tradingRecord)
.display();
With a custom window title:
chartWorkflow.builder()
.withSeries(series)
.withTradingRecordOverlay(tradingRecord)
.display("My Strategy Chart");
Save the chart as a PNG image:
Optional<Path> savedPath = chartWorkflow.builder()
.withSeries(series)
.withTradingRecordOverlay(tradingRecord)
.save("my-strategy-chart");
Save to a specific directory:
Optional<Path> savedPath = chartWorkflow.builder()
.withSeries(series)
.withTradingRecordOverlay(tradingRecord)
.save("charts", "my-strategy-chart");
Or use a Path object:
Path chartsDir = Paths.get("output", "charts");
Optional<Path> savedPath = chartWorkflow.builder()
.withSeries(series)
.withTradingRecordOverlay(tradingRecord)
.save(chartsDir, "my-strategy-chart");
Get the JFreeChart object for further customization:
JFreeChart chart = chartWorkflow.builder()
.withSeries(series)
.withTradingRecordOverlay(tradingRecord)
.toChart();
// Further customize the chart if needed
chart.setTitle("Custom Title");
chartWorkflow.displayChart(chart);
The ChartBuilder intelligently manages Y-axes based on value ranges, ensuring your charts remain readable without manual configuration:
How it works: The builder maintains an AxisModel that tracks the value ranges of the primary and secondary axes. When you add an overlay:
Example:
// RSI (0-100) will automatically use secondary axis
// SMA (price range) will use primary axis
RSIIndicator rsi = new RSIIndicator(closePrice, 14);
SMAIndicator sma = new SMAIndicator(closePrice, 50);
chartWorkflow.builder()
.withSeries(series) // Primary axis: price range
.withIndicatorOverlay(sma) // Primary axis: same range as price
.withIndicatorOverlay(rsi) // Secondary axis: 0-100 range
.display();
Here’s a complete example from the ADX strategy:
// Build and run strategy
BarSeries series = CsvTradesLoader.loadBitstampSeries();
Strategy strategy = ADXStrategy.buildStrategy(series);
BarSeriesManager seriesManager = new BarSeriesManager(series);
TradingRecord tradingRecord = seriesManager.run(strategy);
// Create indicators
ClosePriceIndicator closePrice = new ClosePriceIndicator(series);
SMAIndicator sma = new SMAIndicator(closePrice, 50);
ADXIndicator adx = new ADXIndicator(series, 14);
PlusDIIndicator plusDI = new PlusDIIndicator(series, 14);
MinusDIIndicator minusDI = new MinusDIIndicator(series, 14);
// Build and display chart
ChartWorkflow chartWorkflow = new ChartWorkflow();
JFreeChart chart = chartWorkflow.builder()
.withSeries(series)
.withTradingRecordOverlay(tradingRecord)
.withIndicatorOverlay(sma)
.withSubChart(adx)
.withIndicatorOverlay(plusDI)
.withIndicatorOverlay(minusDI)
.withSubChart(new GrossReturnCriterion(), tradingRecord)
.toChart();
chartWorkflow.displayChart(chart);
chartWorkflow.saveChartImage(chart, series, "adx-strategy", "ta4j-examples/log/charts");
The ChartWorkflow class also provides convenience methods for common use cases. These methods are still available but the ChartBuilder API is recommended for new code:
// Display a trading record chart
chartWorkflow.displayTradingRecordChart(series, "Strategy Name", tradingRecord);
// Create an indicator chart
JFreeChart chart = chartWorkflow.createIndicatorChart(series, indicator1, indicator2);
chartWorkflow.displayChart(chart);
// Create a dual-axis chart
JFreeChart chart = chartWorkflow.createDualAxisChart(
series,
primaryIndicator, "Price (USD)",
secondaryIndicator, "RSI"
);
Create a ChartWorkflow that automatically saves charts to a directory:
ChartWorkflow chartWorkflow = new ChartWorkflow("output/charts");
// Charts will be saved to output/charts when using save() methods
Optional<Path> savedPath = chartWorkflow.builder()
.withSeries(series)
.withTradingRecordOverlay(tradingRecord)
.save(); // Uses the configured directory
The default constructor creates a ChartWorkflow without file persistence:
ChartWorkflow chartWorkflow = new ChartWorkflow();
// Use save() methods with explicit paths, or display only
Optional<Path> savedPath = chartWorkflow.builder()
.withSeries(series)
.withTradingRecordOverlay(tradingRecord)
.save("output/charts", "my-chart"); // Explicit directory required
You can also inject custom implementations for advanced use cases:
TradingChartFactory customFactory = new TradingChartFactory();
ChartDisplayer customDisplayer = new SwingChartDisplayer();
ChartStorage customStorage = new FileSystemChartStorage(Paths.get("custom/path"));
ChartWorkflow chartWorkflow = new ChartWorkflow(customFactory, customDisplayer, customStorage);
The charting system is organized into several packages for better maintainability:
ta4jexamples.charting.builder - ChartBuilder and ChartPlan for fluent chart constructionta4jexamples.charting.compose - TradingChartFactory for rendering chartsta4jexamples.charting.display - ChartDisplayer interface and SwingChartDisplayer implementationta4jexamples.charting.storage - ChartStorage interface and FileSystemChartStorage implementationta4jexamples.charting.workflow - ChartWorkflow facade classta4jexamples.charting.renderer - Custom renderers like BaseCandleStickRendererFor most users, only ChartWorkflow and ChartBuilder need to be imported directly.
When displaying charts in Swing, you can hover over chart elements to see detailed information:
The mouseover display appears in a label at the top of the chart window. The hover delay can be configured via the ta4j.chart.hoverDelay system property (default: 100ms).
// Configure hover delay (in milliseconds)
System.setProperty("ta4j.chart.hoverDelay", "200");
chartWorkflow.builder()
.withSeries(series)
.display();
The chart display size is automatically calculated based on your screen size, scaled by a factor (default: 75%). You can configure this via the ta4j.chart.displayScale system property:
// Set display scale (0.1 to 1.0)
System.setProperty("ta4j.chart.displayScale", "0.9");
chartWorkflow.builder()
.withSeries(series)
.display();
Use the fluent API: The ChartBuilder API provides better type safety and prevents common mistakes. It enforces valid transitions through stage interfaces, similar to Java Streams.
Combine related elements: Group overlays and sub-charts logically (e.g., price indicators together, momentum indicators together). This makes charts easier to read and interpret.
Use sub-charts for different scales: Indicators with vastly different scales (like RSI 0-100 vs price) work better as sub-charts. The automatic axis management will handle this, but explicit sub-charts give you more control.
Style overlays: Use custom colors to distinguish multiple overlays on the same chart. The builder provides a default color palette, but you can override it:
chartWorkflow.builder()
.withSeries(series)
.withIndicatorOverlay(sma1)
.withLineColor(Color.CYAN)
.withIndicatorOverlay(sma2)
.withLineColor(Color.MAGENTA)
.display();
Save important charts: Use the save() method to persist charts for reports or documentation. Charts are saved as JPEG images by default.
Use analysis criterion overlays for performance tracking: Visualize how your strategy’s performance metrics evolve over time using AnalysisCriterionIndicator. This is especially useful for understanding drawdown patterns and profit accumulation.
Leverage ChartPlan for reuse: The toPlan() method returns a ChartPlan that can be reused or serialized:
ChartPlan plan = chartWorkflow.builder()
.withSeries(series)
.withTradingRecordOverlay(tradingRecord)
.toPlan();
// Later, render the same chart
JFreeChart chart = chartWorkflow.render(plan);
chartWorkflow.displayChart(chart);
Create a comprehensive performance dashboard with multiple analysis criteria:
BarSeries series = CsvTradesLoader.loadBitstampSeries();
Strategy strategy = MyStrategy.buildStrategy(series);
BarSeriesManager manager = new BarSeriesManager(series);
TradingRecord record = manager.run(strategy);
ChartWorkflow workflow = new ChartWorkflow();
workflow.builder()
.withTitle("Strategy Performance Dashboard")
.withSeries(series)
.withTradingRecordOverlay(record)
// Performance metrics as overlays on main chart
.withAnalysisCriterionOverlay(new NetProfitCriterion(), record)
.withLineColor(Color.GREEN)
// Additional metrics as sub-charts
.withSubChart(new GrossReturnCriterion(), record)
.withSubChart(new MaximumDrawdownCriterion(), record)
.withSubChart(new InPositionPercentageCriterion(), record)
.display();
Compare multiple indicators with proper styling:
ClosePriceIndicator close = new ClosePriceIndicator(series);
SMAIndicator sma20 = new SMAIndicator(close, 20);
SMAIndicator sma50 = new SMAIndicator(close, 50);
EMAIndicator ema12 = new EMAIndicator(close, 12);
ChartWorkflow workflow = new ChartWorkflow();
workflow.builder()
.withSeries(series)
.withIndicatorOverlay(sma20)
.withLineColor(Color.BLUE)
.withLineWidth(1.5f)
.withIndicatorOverlay(sma50)
.withLineColor(Color.ORANGE)
.withLineWidth(2.0f)
.withIndicatorOverlay(ema12)
.withLineColor(Color.CYAN)
.withLineWidth(1.0f)
.withTradingRecordOverlay(record)
.display();
Compare two strategies side-by-side by saving charts:
TradingRecord strategy1Record = manager.run(strategy1);
TradingRecord strategy2Record = manager.run(strategy2);
ChartWorkflow workflow = new ChartWorkflow("output/comparison");
// Strategy 1 chart
workflow.builder()
.withTitle("Strategy 1: Moving Average Crossover")
.withSeries(series)
.withTradingRecordOverlay(strategy1Record)
.withSubChart(new NetProfitCriterion(), strategy1Record)
.save("strategy1-comparison");
// Strategy 2 chart
workflow.builder()
.withTitle("Strategy 2: RSI Mean Reversion")
.withSeries(series)
.withTradingRecordOverlay(strategy2Record)
.withSubChart(new NetProfitCriterion(), strategy2Record)
.save("strategy2-comparison");
Create a chart with extensive customization:
ChartWorkflow workflow = new ChartWorkflow();
JFreeChart chart = workflow.builder()
.withTitle("Custom Trading Analysis")
.withSeries(series)
.withTradingRecordOverlay(record)
.withIndicatorOverlay(sma)
.withLineColor(new Color(0x03DAC6)) // Custom teal color
.withLineWidth(2.5f)
.withSubChart(rsi)
.withSubChart(new NetProfitCriterion(), record)
.toChart();
// Further customize the JFreeChart if needed
chart.getTitle().setFont(new Font("Arial", Font.BOLD, 18));
workflow.displayChart(chart, "My Custom Chart");
See the following example classes in the ta4j-examples project for more charting patterns:
If charts don’t display, check:
If overlays don’t appear:
BarSeries as the chartFor large datasets:
save() method instead of display() for batch processing