Ta4j Wiki

Documentation, examples and further information of the ta4j project

View the Wiki On GitHub

This project is maintained by ta4j Organization

Portfolio Backtesting and Correlation Analytics

ta4j 0.26.0 provides a constructor-first portfolio workflow for strict time alignment, static target-weight execution, correlation and hierarchy analysis, and long-only minimum-variance allocation. The same PortfolioSeries drives accounting, the optional optimizer, charts, and reports, so one prepared data window has one asset order and one numeric domain.

Use this workflow for static allocation research with one or more risk assets and an optional cash sleeve, including a one-asset benchmark. Correlations and minimum-variance optimization require at least two risk assets. Continue to use BarSeriesManager or BacktestExecutor for strategy-driven trades over one BarSeries.

Release status: This API is available in ta4j 0.26.0. The runnable example links below are pinned to that release.

Quick Start

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import org.ta4j.core.BarSeries;
import org.ta4j.core.analysis.cost.LinearTransactionCostModel;
import org.ta4j.core.portfolio.PortfolioAllocation;
import org.ta4j.core.portfolio.PortfolioExecutionResult;
import org.ta4j.core.portfolio.PortfolioSeries;
import org.ta4j.core.portfolio.PortfolioSeriesManager;
import org.ta4j.core.portfolio.RebalancePolicy;

BarSeries equitySeries = ...;
BarSeries bondSeries = ...;
BarSeries commoditySeries = ...;

Map<String, BarSeries> assets = new LinkedHashMap<>();
assets.put("EQUITY", equitySeries);
assets.put("BONDS", bondSeries);
assets.put("COMMODITIES", commoditySeries);
PortfolioSeries portfolio = new PortfolioSeries(assets);

Map<String, Double> weights = new LinkedHashMap<>();
weights.put("EQUITY", 0.60);
weights.put("BONDS", 0.30);
weights.put("COMMODITIES", 0.05);
PortfolioAllocation allocation = new PortfolioAllocation(weights);

PortfolioSeriesManager manager = new PortfolioSeriesManager(
        portfolio,
        new LinearTransactionCostModel(0.001));
PortfolioExecutionResult result = manager.run(
        allocation,
        portfolio.numFactory().numOf("10000"),
        RebalancePolicy.onIndexes(Set.of(0, 21, 42, 63)));

System.out.println("Final value: " + result.getFinalValue());
System.out.println("Total return: " + result.getTotalReturn());
System.out.println("Costs: " + result.getTotalTransactionCost());

The weights sum to 0.95, so the remaining 0.05 is held as cash. Rebalance trade sizes are solved against post-cost portfolio value and constrained so transaction costs cannot make cash negative. Holdings are fractional and long-only; trades and valuation use aligned closes in one common quote currency, with adjustments supplied upstream and no implicit forward fill. Returns are fractions, so 0.05 means 5%.

Canonical Flow

graph TD
    BS["Named BarSeries inputs"] --> PS["new PortfolioSeries(...)"]
    PS --> CORR["new PortfolioCorrelations(...)"]
    PS --> OPT["new MinimumVarianceOptimizer(...)"]
    W["new PortfolioAllocation(...)"] --> MGR["new PortfolioSeriesManager(...)"]
    PS --> MGR
    RP["RebalancePolicy"] --> MGR
    MGR --> RESULT["PortfolioExecutionResult"]
    RESULT --> SNAP["PortfolioSnapshot"]
    RESULT --> VALUE["Portfolio value BarSeries"]
    CORR --> MATRIX["CorrelationMatrix"]
    MATRIX --> TREE["completeLinkage()"]
    MATRIX --> CHARTS["Heatmaps and dendrograms"]
    OPT --> W

Portfolio Series

PortfolioSeries snapshots its inputs once and aligns them by the intersection of bar end times present in every source. Missing bars are excluded rather than forward-filled.

Use source names when they are already the desired identifiers:

PortfolioSeries portfolio = new PortfolioSeries(equitySeries, bondSeries);
// Equivalent:
PortfolioSeries fromList = new PortfolioSeries(List.of(equitySeries, bondSeries));

Each BarSeries#getName() must be nonblank and unique. Use the map constructor for explicit aliases and deterministic encounter order:

Map<String, BarSeries> assets = new LinkedHashMap<>();
assets.put("US_EQUITY", equitySeries);
assets.put("TREASURY", bondSeries);
PortfolioSeries portfolio = new PortfolioSeries(assets);

Useful alignment methods:

int barCount = portfolio.getBarCount();
List<Instant> timeline = portfolio.getEndTimes();
Num equityClose = portfolio.getClosePrice("US_EQUITY", 0);
int originalIndex = portfolio.getSourceIndex("US_EQUITY", 0);
BarSeries defensiveCopy = portfolio.getBarSeries("US_EQUITY");

The original source index is retained even when an input has a nonzero begin index. Mutating an original input after construction does not change the portfolio snapshot.

Alignment rules:

Prepare currency conversion, exchange-calendar joins, or intentional carry-forward bars before construction. Strict alignment will not invent those policies.

Allocations

Use explicit weights when a cash sleeve is intentional:

Map<String, Double> weights = new LinkedHashMap<>();
weights.put("US_EQUITY", 0.60);
weights.put("TREASURY", 0.30);
PortfolioAllocation allocation = new PortfolioAllocation(weights);

Num investedWeight = allocation.getTotalWeight(); // 0.90
Num cashWeight = allocation.getCashWeight();      // 0.10

Map weights are plain Number values, including BigDecimal; the manager converts them using the portfolio’s NumFactory. Weights must be finite and non-negative, and their sum must not exceed one. Relative WeightedValue inputs retain their Num weights, including finite DecimalNum values outside the primitive double range.

Use relative weighted values for a normalized, fully invested allocation:

PortfolioAllocation allocation = new PortfolioAllocation(
        List.of(
                new WeightedValue<>("US_EQUITY", numFactory.two()),
                new WeightedValue<>("TREASURY", numFactory.one())));

This constructor normalizes the values to one and combines duplicate asset names. getTargetWeight(name) returns zero for an intentionally unallocated asset.

Execution and Rebalancing

PortfolioSeriesManager mirrors the role of BarSeriesManager: the constructor owns the series and transaction-cost configuration, while each run(...) supplies an allocation and starting cash.

PortfolioSeriesManager manager = new PortfolioSeriesManager(portfolio);
PortfolioExecutionResult initialOnly =
        manager.run(allocation, numFactory.numOf("10000"));

PortfolioExecutionResult scheduled = manager.run(
        allocation,
        numFactory.numOf("10000"),
        RebalancePolicy.onIndexes(Set.of(0, 63, 126, 189)));

The two-argument run(...) invests at the first aligned bar and then holds. A supplied policy schedules every trade, including the initial investment: onIndexes(Set.of(3)) stays in cash through bar 2; atStart().or(onIndexes(Set.of(3))) invests at bar 0 and rebalances at bar 3. Available policies are:

RebalancePolicy.atStart();
RebalancePolicy.everyBar();
RebalancePolicy.onIndexes(Set.of(0, 20, 40));
RebalancePolicy.everyNthBar(20);
RebalancePolicy.firstBarOf(ChronoUnit.MONTHS, ZoneOffset.UTC);
RebalancePolicy.whenDriftExceeds(0.03);

RebalancePolicy is a functional interface whose lambda receives a RebalancePolicy.Context, including the current aligned index and pre-trade weights. firstBarOf(...) uses aligned timestamps for calendar periods; everyNthBar(...) counts retained bars and does not imply a calendar month. Policies compose with or(...) and and(...).

Use any ta4j CostModel:

PortfolioSeriesManager manager = new PortfolioSeriesManager(
        portfolio,
        new LinearTransactionCostModel(0.001));

Execution is long-only and uses fractional units. Buys and sells are reduced or skipped when a fixed or step-like transaction cost would otherwise make cash negative. Invalid, negative, or non-finite costs are rejected.

Snapshots and Results

Output types are immutable and created by the manager:

for (PortfolioSnapshot snapshot : result.getSnapshots()) {
    System.out.printf(
            "index=%d value=%s cash=%s turnover=%s%n",
            snapshot.getIndex(),
            snapshot.getPortfolioValue(),
            snapshot.getCash(),
            snapshot.getTurnover());
}
Snapshot method Meaning
getIndex() / getEndTime() Aligned portfolio location.
getPrices() Close prices used for valuation.
getHoldings() Fractional units after any rebalance.
getCash() Cash after trades and costs.
getPortfolioValue() Cash plus marked-to-market holdings.
getPeriodReturn() Return since the previous snapshot, net of costs.
getTransactionCost() Costs paid at this snapshot.
getTradedNotional() Gross traded notional in the quote currency, excluding costs.
getTurnover() Traded notional divided by pre-trade portfolio value.
getAssetValue(name) Marked-to-market asset value.
getAssetWeights() / getCashWeight() Achieved asset and cash weights.
getRebalanceStatus() NOT_SCHEDULED, COMPLETED, PARTIAL, or SKIPPED.

Result summaries include getFinalValue(), getTotalReturn(), getTotalTransactionCost(), getTotalTurnover(), and getFinalWeights().

toString() gives a compact summary. Full snapshot history is explicit through getSnapshots(), including filtering by rebalance status.

Export the equity curve for normal ta4j indicators and criteria:

BarSeries equityCurve = result.toPortfolioValueSeries("portfolio-value");
ClosePriceIndicator value = new ClosePriceIndicator(equityCurve);
SMAIndicator smoothedValue = new SMAIndicator(value, 20);

The exported curve begins with the initial capital before the first aligned bar, so existing return and drawdown criteria include initial trading fees. StaticPortfolioBacktest demonstrates EnterAndHoldCriterion(NetReturnCriterion) agreeing with getTotalReturn().

Correlation Matrices

Construct analytics once for the aligned series:

PortfolioCorrelations correlations = new PortfolioCorrelations(portfolio);

CorrelationMatrix prices = correlations.getPriceMatrix();
CorrelationMatrix simpleReturns = correlations.getSimpleReturnMatrix();
CorrelationMatrix logReturns = correlations.getLogReturnMatrix();

Full-history methods end at the final aligned index. Rolling and historical forms make the decision index and observation count explicit:

CorrelationMatrix rolling = correlations.getSimpleReturnMatrix(60);
CorrelationMatrix historical =
        correlations.getLogReturnMatrix(index, 120, SampleType.SAMPLE);

Num coefficient = historical.getCoefficient("US_EQUITY", "TREASURY");
List<CorrelationPair> pairs = historical.getPairs();

getPriceMatrix(...) counts prices. Simple- and log-return windows count one-bar returns, so they need one earlier price. isStable() reports whether the requested index contains the complete transform and correlation window; unavailable off-diagonal values are NaN.

Complete-Linkage Hierarchy

Every finite matrix can produce a deterministic hierarchy:

CorrelationHierarchy hierarchy = simpleReturns.completeLinkage();

for (ClusterMerge merge : hierarchy.getMerges()) {
    System.out.printf(
            "%d + %d distance=%s size=%d%n",
            merge.getLeftClusterIndex(),
            merge.getRightClusterIndex(),
            merge.getDistance(),
            merge.getSize());
}
System.out.println(hierarchy.getLeafOrder());

completeLinkage() matches scipy.cluster.hierarchy.linkage(matrix, method="complete"): each asset is represented by its complete correlation row, row distance is Euclidean, and cluster distance is the maximum pairwise row distance. Leaf cluster indexes are 0..assetCount-1; each merge receives the next index.

A matrix containing NaN or infinite coefficients is rejected rather than silently clustered.

Minimum-Variance Allocation

MinimumVarianceOptimizer is an optional core calculation that returns the same PortfolioAllocation used by static execution. It estimates covariance from aligned one-bar simple returns and minimizes portfolio variance subject to non-negative weights summing to one. Ledoit-Wolf shrinkage toward a scaled identity is the default; withCovarianceEstimator(CovarianceEstimator.SAMPLE) selects raw sample covariance. Static allocation does not require the optimizer or any network/reporting dependency.

PortfolioAllocation pure =
        new MinimumVarianceOptimizer(portfolio).optimize();

PortfolioAllocation capped =
        new MinimumVarianceOptimizer(
                portfolio,
                0.50)
                .optimize();

The uncapped result is the mathematical long-only optimum for the estimated covariance matrix. A maximum weight can make the result more practical and diversified; it must be feasible for the number of assets. For example, a 25% cap requires at least four assets.

Historical windows are explicit and do not read future bars:

PortfolioAllocation walkForwardAllocation =
        new MinimumVarianceOptimizer(
                portfolio,
                decisionIndex,
                120,
                0.50)
                .optimize();

All covariance, projection, and convergence calculations use Num. The deterministic bounded-simplex projected-gradient solver supports singular covariance matrices without inversion. It rejects non-positive or non-finite prices, fewer than two return observations, infeasible caps, and numerical non-convergence.

Adjusted Yahoo Data

The examples module can load Yahoo data with yfinance-style auto_adjust=True behavior:

BarSeries adjusted =
        YahooFinanceHttpBarSeriesDataSource.loadAdjustedSeries(
                "QQQ",
                YahooFinanceInterval.DAY_1,
                start,
                end);

Yahoo’s adjusted-close ratio is applied to open, high, low, and close; volume is retained. Existing loadSeries(...) and loadSeriesInstance(...) methods continue to return raw prices.

For daily multi-asset research, normalize end times to one calendar convention before constructing PortfolioSeries. The runnable portfolio examples show a UTC-date normalization step.

Heatmaps, Dendrograms, and Reports

PortfolioCorrelationChartFactory in ta4j-examples creates headless-safe JFreeChart output:

PortfolioCorrelationChartFactory charts =
        new PortfolioCorrelationChartFactory();

JFreeChart heatmap =
        charts.createHeatmap("Return correlations", simpleReturns);
JFreeChart dendrogram =
        charts.createDendrogram(
                "Correlation hierarchy",
                simpleReturns.completeLinkage());

The anonymous DiversifiedPortfolioAnalysis example uses adjusted YTD data for:

QQQ, VWO, COIN, FBTC, IBIT, ETHW, RES, XOM, DOC, NKE, ARKK, TLT

It writes:

Run it from the ta4j repository root:

./mvnw -pl ta4j-examples -am install
./mvnw -pl ta4j-examples exec:java \
  -Dexec.mainClass=ta4jexamples.portfolio.DiversifiedPortfolioAnalysis

The example does not call an AI service. To embed a response from an external model, save the response to a file and rerun:

./mvnw -pl ta4j-examples exec:java \
  -Dexec.mainClass=ta4jexamples.portfolio.DiversifiedPortfolioAnalysis \
  -Dexec.args="--ai-analysis=/path/to/response.md"

The response is HTML-escaped before it is embedded. Use --output=/path/to/directory to select another output directory.

No-Look-Ahead Contract

Correlation methods and optimizer constructors with an explicit index read prices only through that index. A rolling rebalance loop should compute an allocation at decision index i, then apply it according to the execution timing in the research design.

Do not estimate one full-history allocation and present it as a historical strategy. That is an in-sample description, not a walk-forward backtest.

Common Pitfalls

Public Type Map

Type Purpose
PortfolioSeries Snapshot and strict common-end-time alignment of named asset series.
PortfolioAllocation Long-only explicit or normalized target weights.
PortfolioSeriesManager Cost-aware target-weight execution.
RebalancePolicy Functional rebalance schedule.
PortfolioSnapshot Immutable state at one aligned bar.
PortfolioExecutionResult Immutable execution summary and equity-curve export.
PortfolioCorrelations Price, simple-return, and log-return matrices.
CorrelationMatrix Immutable matrix, pairs, stability metadata, and hierarchy entrypoint.
CorrelationHierarchy / ClusterMerge Complete-linkage result.
MinimumVarianceOptimizer Num-native long-only covariance optimizer.

Runnable Examples

For general strategy mechanics, see Backtesting. For promotion gates and realism checks, see Backtesting Realism Checklist.