Skip to main content
Backtesting a single strategy on the full dataset can produce results that look good in-sample but fail in production — the strategy has been overfit to historical noise rather than capturing a real edge. Walk-forward analysis addresses this by splitting the dataset into a series of sequential folds: each fold optimizes on an in-sample window and validates on an untouched out-of-sample window. The out-of-sample results are what matter. If a strategy that scores well in-sample also performs well across many independent out-of-sample windows, that is much stronger evidence of a genuine edge.

StrategyWalkForwardExecutor

StrategyWalkForwardExecutor is the highest-level entry point for walk-forward testing a single strategy. Its API mirrors BarSeriesManager, so the transition from backtest to walk-forward is minimal.
1

Create the executor

// Simple: uses default cost models and next-open execution
StrategyWalkForwardExecutor executor = new StrategyWalkForwardExecutor(series);
Or with custom cost and execution models:
StrategyWalkForwardExecutor executor = new StrategyWalkForwardExecutor(
        series,
        new LinearTransactionCostModel(0.001),  // 0.1% fee per trade
        new ZeroCostModel(),
        new SlippageExecutionModel(series.numFactory().numOf(0.0005)));
2

Build the walk-forward configuration

// Use the series-aware default configuration
WalkForwardConfig config = WalkForwardConfig.defaultConfig(series);
Or specify fold geometry explicitly:
WalkForwardConfig config = new WalkForwardConfig(
        120,          // minTrainBars: minimum in-sample bars per fold
        40,           // testBars: out-of-sample bars per fold
        20,           // stepBars: how many bars to advance between folds
        1,            // purgeBars: bars purged before each test fold
        1,            // embargoBars: gap between train end and test start
        40,           // holdoutBars: trailing bars reserved for final holdout
        15,           // primaryHorizonBars: horizon for optimization scoring
        List.of(7, 30), // reportingHorizons
        3,            // optimizationTopK
        List.of(1, 5),  // reportingTopKs
        42L);           // seed for reproducibility
3

Run walk-forward

StrategyWalkForwardExecutionResult result = executor.execute(strategy, config);
4

Interpret results

AnalysisCriterion returnCriterion = new GrossReturnCriterion();

// Out-of-sample criterion values per fold
List<Num> oosCriterionValues = result.outOfSampleCriterionValues(returnCriterion);

// Optional holdout fold (reserved trailing bars)
Optional<Num> holdout = result.holdoutCriterionValue(returnCriterion);

oosCriterionValues.forEach(v -> System.out.println("OOS fold return: " + v));
holdout.ifPresent(h -> System.out.println("Holdout return: " + h));

System.out.println("Total folds: " + result.folds().size());

BacktestExecutor integration

BacktestExecutor supports running backtest-only, walk-forward-only, or both in one call. Use this when comparing many strategies and you want both full-series and fold-level results side by side.
BacktestExecutor executor = new BacktestExecutor(series);
WalkForwardConfig config = WalkForwardConfig.defaultConfig(series);

// Walk-forward only
StrategyWalkForwardExecutionResult wfResult = executor.executeWithWalkForward(strategy, config);

// Backtest + walk-forward combined
BacktestExecutor.BacktestAndWalkForwardResult combined =
        executor.executeWithWalkForward(strategy, config);

// Read backtest result
Num backtestReturn = returnCriterion.calculate(
        series,
        combined.backtest().tradingStatements().getFirst().getTradingRecord());

// Read walk-forward OOS average
List<Num> oosScores = combined.walkForward().outOfSampleCriterionValues(returnCriterion);

BarSeriesManager direct API

BarSeriesManager also exposes runWalkForward(...) for when you want full control over the trading record and execution model.
BarSeriesManager manager = new BarSeriesManager(series);
WalkForwardConfig config = WalkForwardConfig.defaultConfig(series);

StrategyWalkForwardExecutionResult result = manager.runWalkForward(strategy, config);

WalkForwardConfig fold geometry

Understanding how folds are constructed helps you choose parameters that avoid overfitting in the fold construction itself.
|<--- minTrainBars (in-sample) --->|purgeBars|embargo|<- testBars (OOS) ->|
                                              ↑                            ↑
                                        train end                    test end
After each fold, the window advances by stepBars bars. The holdoutBars parameter reserves a final set of trailing bars that are never part of any training fold and serve as a final out-of-sample validation.
FieldDefaultDescription
minTrainBars120Minimum in-sample bars required per fold
testBars40Out-of-sample bars per test fold
stepBars20Bars to advance between folds
purgeBars1Bars purged at the boundary to avoid look-ahead
embargoBars1Gap between train end and test start
holdoutBars40Trailing bars reserved for final holdout validation
primaryHorizonBars15Primary optimization horizon
reportingHorizons[7, 30]Additional reporting horizons
optimizationTopK3Ranking depth for optimization
reportingTopKs[1, 5]Additional ranking depths for reporting
seed42Deterministic seed for reproducibility

WalkForwardEngine

WalkForwardEngine is the generic engine underlying the strategy-oriented executors. Use it directly when your prediction pipeline is not a ta4j Strategy — for example when you want to walk-forward test a machine-learning classifier or a ranking model.
WalkForwardEngine<MyContext, MyPrediction, MyOutcome> engine =
        new WalkForwardEngine<>(
                splitter,
                predictionProvider,
                outcomeLabeler,
                List.of(accuracyMetric, precisionMetric));

WalkForwardRunResult<MyPrediction, MyOutcome> result =
        engine.run(series, context, config);

// Global metrics for the primary horizon
Map<String, Num> globalMetrics = result.globalMetricsForHorizon(config.primaryHorizonBars());

// Per-fold metrics
Map<String, Map<String, Num>> foldMetrics = result.foldMetricsForHorizon(config.primaryHorizonBars());

WalkForwardTuner and WalkForwardObjective

WalkForwardTuner evaluates a list of candidates in batches, scores each with a WalkForwardObjective, and returns a ranked WalkForwardLeaderboard.
// Define the objective: weighted sum of two metrics with fold-variance penalty
WalkForwardObjective objective = WalkForwardObjective.weighted(
        Map.of("gross_return", series.numFactory().numOf(0.7),
               "sharpe",       series.numFactory().numOf(0.3)),
        Map.of("gross_return", series.numFactory().numOf(1.0)),  // minimum guardrail
        Map.of(),                                                  // no maximum guardrails
        series.numFactory().numOf(0.1));                          // fold-variance penalty

WalkForwardTuner<MyContext, MyPrediction, MyOutcome> tuner =
        new WalkForwardTuner<>(engine, objective, 10, 50); // keep top 10, batch size 50

WalkForwardLeaderboard<MyContext> leaderboard = tuner.tune(series, candidates, config);

leaderboard.entries().forEach(entry -> {
    System.out.printf("Candidate: %s | score: %s%n",
            entry.candidate().id(),
            entry.objectiveScore().totalScore());
});

WalkForwardObjective.Score

The Score record returned by WalkForwardObjective.evaluate(...) contains:
FieldTypeDescription
totalScoreNumFinal score after fold-variance penalty; NaN if any guardrail is violated
weightedScoreNumWeighted metric sum before penalties
foldVarianceNumVariance of per-fold objective scores
guardrailPassedbooleanWhether all min/max guardrails passed
violationsList<String>Human-readable list of violated constraints
metricValuesMap<String, Num>Metric values used to compute the score

Example class

# Linux/macOS
./mvnw -pl ta4j-examples exec:java \
  -Dexec.mainClass=ta4jexamples.walkforward.WalkForward

# Windows CMD
mvnw.cmd -pl ta4j-examples exec:java "-Dexec.mainClass=ta4jexamples.walkforward.WalkForward"
The WalkForward example evaluates four strategies (CCI Correction, Global Extrema, Moving Momentum, RSI-2) on Bitstamp Bitcoin data, ranks them by average out-of-sample gross return, then runs the winner through BacktestExecutor.executeWithWalkForward(...) for a combined backtest and walk-forward summary.
Keep your WalkForwardConfig fixed across all candidates in a single study. Changing fold geometry or horizon settings between runs makes the metric values incomparable.
Walk-forward analysis reduces — but does not eliminate — the risk of overfitting. A strategy that survives many out-of-sample windows is stronger evidence of a genuine edge, but past performance still does not guarantee future results.