/* * SPDX-License-Identifier: MIT */ package ta4jexamples.backtesting; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.ta4j.core.*; import org.ta4j.core.backtest.BacktestExecutionResult; import org.ta4j.core.backtest.BacktestExecutor; import org.ta4j.core.backtest.ProgressCompletion; import org.ta4j.core.criteria.ExpectancyCriterion; import org.ta4j.core.criteria.pnl.NetProfitCriterion; import org.ta4j.core.indicators.NetMomentumIndicator; import org.ta4j.core.indicators.RSIIndicator; import org.ta4j.core.indicators.helpers.ClosePriceIndicator; import org.ta4j.core.num.Num; import org.ta4j.core.num.NumFactory; import org.ta4j.core.reports.TradingStatement; import org.ta4j.core.rules.CrossedDownIndicatorRule; import org.ta4j.core.rules.CrossedUpIndicatorRule; import org.ta4j.core.serialization.DurationTypeAdapter; import ta4jexamples.datasources.JsonFileBarSeriesDataSource; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.management.GarbageCollectorMXBean; import java.lang.management.ManagementFactory; import java.lang.management.MemoryUsage; import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.StringJoiner; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; /** * Performance tuning harness for backtesting large numbers of strategies. *

* This class provides a comprehensive tool for optimizing backtest performance * by systematically testing different parameter combinations and identifying * optimal settings for your hardware and dataset. It helps tune several * interrelated performance parameters: *

*

* The harness uses a non-trivial NetMomentumIndicator-based strategy workload * to make garbage collection (GC) and caching behavior visible. It * automatically detects non-linear performance degradation (e.g., excessive GC * overhead or slowdown beyond expected scaling) and recommends optimal * parameter combinations. *

*

Execution Modes

*

* The harness supports four execution modes: *

    *
  1. Run Once (default): Execute a single backtest with specified * parameters. Useful for quick performance checks or production runs with known * optimal settings.
  2. *
  3. Tune In-Process: Run multiple backtests with varying parameters to * find optimal settings. Tests different strategy counts, bar counts, and * maximum bar count hints systematically.
  4. *
  5. Tune Across Heaps: Fork child JVMs with different heap sizes to * test memory configuration impact. Each child JVM runs a full tuning * cycle.
  6. *
  7. Throughput Control: Run a fixed strategy/bar/cache matrix and * write {@code matrix_performance.json} with cells/min and hypotheses/min for * reproducible before/after comparisons.
  8. *
*

*

Usage Examples

*

*

Example 1: Quick Performance Check

Run a single backtest with 1000 * strategies on the last 2000 bars: * *
{@code
 * java BacktestPerformanceTuningHarness \
 *   --strategies 1000 \
 *   --barCount 2000 \
 *   --executionMode full
 * }
*

*

Example 2: Find Optimal Settings

Run a tuning cycle to find optimal * parameters for your hardware: * *
{@code
 * java BacktestPerformanceTuningHarness \
 *   --tune \
 *   --tuneStrategyStart 2000 \
 *   --tuneStrategyStep 2000 \
 *   --tuneStrategyMax 20000 \
 *   --tuneBarCounts 500,1000,2000,full \
 *   --tuneMaxBarCountHints 0,512,1024,2048 \
 *   --executionMode topK \
 *   --topK 20
 * }
* * This will test strategy counts from 2000 to 20000 (in steps of 2000) across * different bar counts and maximum bar count hints, then recommend the best * configuration. *

*

Example 3: Test Different Heap Sizes

Test performance across * different JVM heap sizes: * *
{@code
 * java BacktestPerformanceTuningHarness \
 *   --tuneHeaps 4g,8g,16g \
 *   --tuneStrategyStart 5000 \
 *   --tuneStrategyMax 50000 \
 *   --executionMode topK \
 *   --topK 20
 * }
* * This forks separate JVMs with 4GB, 8GB, and 16GB heaps, running a full tuning * cycle in each. *

*

Example 4: Production Run with Optimal Settings

After tuning, use * the recommended settings for a production run: * *
{@code
 * java BacktestPerformanceTuningHarness \
 *   --strategies 10000 \
 *   --barCount 2000 \
 *   --maxBarCountHint 1024 \
 *   --executionMode topK \
 *   --topK 20 \
 *   --progress
 * }
* * The {@code --progress} flag enables progress logging with memory usage * information. *

*

Example 5: Fixed Throughput Matrix

Produce parseable throughput * telemetry for a fixed matrix: * *
{@code
 * java BacktestPerformanceTuningHarness \
 *   --throughputControl \
 *   --throughputOutputDir .agents/benchmarks/backtest-throughput/current \
 *   --matrixStrategyCounts 250,500,1000 \
 *   --matrixBarCounts 500,1000 \
 *   --matrixMaxBarCountHints 0 \
 *   --executionMode topK \
 *   --topK 10 \
 *   --parallelism 1
 * }
*

*

Performance Tuning Workflow

*
    *
  1. Initial Exploration: Start with a broad tuning run to identify * promising regions: * *
    {@code --tune --tuneStrategyStart 1000 --tuneStrategyStep 5000 --tuneStrategyMax 50000}
    * *
  2. *
  3. Fine-Tuning: Narrow down to the promising region with smaller * steps: * *
    {@code --tune --tuneStrategyStart 8000 --tuneStrategyStep 1000 --tuneStrategyMax 15000}
    * *
  4. *
  5. Memory Optimization: Test different maximum bar count hints to * balance memory and performance: * *
    {@code --tune --tuneMaxBarCountHints 0,256,512,1024,2048,4096}
    * *
  6. *
  7. Heap Size Testing: If memory is a concern, test different heap * sizes: * *
    {@code --tuneHeaps 2g,4g,8g,16g}
    * *
  8. *
*

*

Understanding Results

*

* The harness outputs several types of information: *

*

*

Strategy Generation

*

* The harness generates strategies using a grid search over * NetMomentumIndicator parameters: *

* This generates approximately 10,416 unique strategy combinations. When fewer * strategies are requested, the harness samples from this grid. When more are * requested, it repeats the grid with different repetition markers. *

*

Command-Line Options

*

* Run with {@code --help} to see all available options. Key options include: *

*

*

Performance Notes

* *

*

See Also

* */ public class BacktestPerformanceTuningHarness { // PERFORMANCE NOTE: The current ranges generate ~10,000+ strategies. // BacktestExecutor automatically uses batch processing for large strategy // counts (>1000) // to prevent memory exhaustion. If execution is still too slow, consider: // 1. Increasing INCREMENT values to reduce grid density // 2. Narrowing MIN/MAX ranges based on preliminary results // 3. Using coarser increments for initial exploration, then fine-tuning // promising regions private static final int RSI_BARCOUNT_INCREMENT = 7; private static final int RSI_BARCOUNT_MIN = 7; private static final int RSI_BARCOUNT_MAX = 49; private static final int MOMENTUM_TIMEFRAME_INCREMENT = 100; private static final int MOMENTUM_TIMEFRAME_MIN = 100; private static final int MOMENTUM_TIMEFRAME_MAX = 400; private static final int OVERBOUGHT_THRESHOLD_INCREMENT = 250; private static final int OVERBOUGHT_THRESHOLD_MIN = 0; private static final int OVERBOUGHT_THRESHOLD_MAX = 1500; private static final int OVERSOLD_THRESHOLD_INCREMENT = 250; private static final int OVERSOLD_THRESHOLD_MIN = -2000; private static final int OVERSOLD_THRESHOLD_MAX = 0; private static final double DECAY_FACTOR_INCREMENT = 0.02; private static final double DECAY_FACTOR_MIN = 0.9; private static final double DECAY_FACTOR_MAX = 1; private static final Logger LOG = LogManager.getLogger(BacktestPerformanceTuningHarness.class); static final String DEFAULT_OHLC_RESOURCE_FILE = "Coinbase-ETH-USD-PT1D-20160517_20251028.json"; static final int DEFAULT_TOP_K = 20; static final int DEFAULT_TUNE_STRATEGY_START = 2_000; static final int DEFAULT_TUNE_STRATEGY_STEP = 2_000; static final int DEFAULT_TUNE_STRATEGY_MAX = 20_000; static final double DEFAULT_NONLINEAR_GC_OVERHEAD = 0.25d; static final double DEFAULT_NONLINEAR_SLOWDOWN_RATIO = 1.25d; static final List DEFAULT_MATRIX_STRATEGY_COUNTS = List.of(250, 500, 1_000); static final List DEFAULT_MATRIX_BAR_COUNTS = List.of(500, 1_000); static final List DEFAULT_MATRIX_MAX_BAR_COUNT_HINTS = List.of(0); static final String MATRIX_PERFORMANCE_FILE = "matrix_performance.json"; static final String MATRIX_CELLS_FILE = "matrix_cells.json"; static final String THROUGHPUT_MANIFEST_FILE = "throughput_manifest.json"; static final String HARNESS_RESULT_PREFIX = "HARNESS_RESULT: "; static final String RECOMMENDED_SETTINGS_PREFIX = "RECOMMENDED_SETTINGS: "; static final String THROUGHPUT_RESULT_PREFIX = "THROUGHPUT_RESULT: "; static final Gson GSON = new GsonBuilder().registerTypeAdapter(Duration.class, new DurationTypeAdapter()).create(); static final Gson PRETTY_GSON = new GsonBuilder().registerTypeAdapter(Duration.class, new DurationTypeAdapter()) .setPrettyPrinting() .create(); /** * Main entry point for the performance tuning harness. *

* Parses command-line arguments and executes the requested operation: *

    *
  • If {@code --help} is specified, prints usage information and exits
  • *
  • If {@code --tuneHeaps} is specified, forks child JVMs with different heap * sizes and runs tuning in each
  • *
  • If {@code --tune} is specified, runs an in-process tuning cycle to find * optimal parameters
  • *
  • Otherwise, runs a single backtest with the specified parameters
  • *
*

* Example usage: * *

{@code
     * // Single run
     * java BacktestPerformanceTuningHarness --strategies 1000 --barCount 2000
     *
     * // Tuning mode
     * java BacktestPerformanceTuningHarness --tune --tuneStrategyMax 20000
     *
     * // Cross-heap tuning
     * java BacktestPerformanceTuningHarness --tuneHeaps 4g,8g,16g
     * }
* * @param args Command-line arguments (see {@code --help} for full list) * @throws Exception If an error occurs during execution */ public static void main(String[] args) throws Exception { HarnessCli cli = HarnessCli.parse(args); if (cli.help) { logUsage(); return; } if (!cli.tuneHeaps.isEmpty()) { runTuneAcrossHeaps(cli); return; } BarSeries baseSeries = loadSeries(cli.ohlcResourceFile); Objects.requireNonNull(baseSeries, "Bar series was null"); if (cli.throughputControl) { runThroughputControl(baseSeries, cli); return; } if (cli.tune) { warmupOnce(baseSeries); runTuneInProcess(baseSeries, cli); return; } RunOnceConfig runConfig = new RunOnceConfig(cli.strategyCount, cli.barCount, cli.maximumBarCountHint, cli.executionMode, cli.topK, cli.progress); RunOutcome runOutcome = runOnce(baseSeries, runConfig); if (cli.topK > 0) { logTopStrategies(runOutcome.result(), cli.topK); } } static void runThroughputControl(BarSeries baseSeries, HarnessCli cli) throws IOException { Objects.requireNonNull(baseSeries, "baseSeries must not be null"); Objects.requireNonNull(cli, "cli must not be null"); ThroughputControlPlan plan = ThroughputControlPlan.fromCli(cli, baseSeries.getBarCount()); Files.createDirectories(plan.outputDir()); HostTelemetry host = HostTelemetry.capture(); writeJson(plan.outputDir().resolve(THROUGHPUT_MANIFEST_FILE), plan.toManifest(host)); LOG.info("Throughput control plan: cells={}, parallelism={}, outputDir={}", plan.cells().size(), plan.resolvedParallelism(), plan.outputDir()); long startedNanos = System.nanoTime(); ThroughputMatrixPerformanceTracker tracker = new ThroughputMatrixPerformanceTracker(); if (plan.resolvedParallelism() == 1) { for (ThroughputMatrixCell cell : plan.cells()) { tracker.record(runThroughputCell(baseSeries, plan, cell)); if (plan.gcBetweenRuns()) { System.gc(); Thread.yield(); } } } else { runThroughputCellsInParallel(baseSeries, plan, tracker); } long totalWallTimeMs = elapsedMillis(startedNanos); JsonObject performance = tracker.toJson(totalWallTimeMs, plan, host); writeJson(plan.outputDir().resolve(MATRIX_PERFORMANCE_FILE), performance); writeJson(plan.outputDir().resolve(MATRIX_CELLS_FILE), tracker.cellsJson()); LOG.info(THROUGHPUT_RESULT_PREFIX + "{}", PRETTY_GSON.toJson(performance)); } private static void runThroughputCellsInParallel(BarSeries baseSeries, ThroughputControlPlan plan, ThroughputMatrixPerformanceTracker tracker) throws IOException { ExecutorService executor = Executors.newFixedThreadPool(plan.resolvedParallelism()); try { List> futures = new ArrayList<>(plan.cells().size()); for (ThroughputMatrixCell cell : plan.cells()) { futures.add(executor.submit(() -> runThroughputCell(baseSeries, plan, cell))); } for (Future future : futures) { try { tracker.record(future.get()); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); throw new IOException("Interrupted while executing throughput matrix cells", ex); } catch (ExecutionException ex) { Throwable cause = ex.getCause() == null ? ex : ex.getCause(); if (cause instanceof IOException ioException) { throw ioException; } throw new IOException("Throughput matrix cell failed", cause); } } } finally { executor.shutdownNow(); } } private static ThroughputCellResult runThroughputCell(BarSeries baseSeries, ThroughputControlPlan plan, ThroughputMatrixCell cell) { long startedNanos = System.nanoTime(); RunOutcome outcome = runOnce(baseSeries, cell.toRunOnceConfig(plan.progress())); return new ThroughputCellResult(cell, outcome.runResult(), elapsedMillis(startedNanos)); } private static void warmupOnce(BarSeries baseSeries) { int warmupStrategies = Math.min(250, DEFAULT_TUNE_STRATEGY_START); int warmupBars = Math.min(500, baseSeries.getBarCount()); RunOnceConfig warmupConfig = new RunOnceConfig(warmupStrategies, warmupBars, 0, ExecutionMode.KEEP_TOP_K, 1, false); LOG.info("Warm-up run (strategies={}, bars={})", warmupStrategies, warmupBars); try { runOnce(baseSeries, warmupConfig); } catch (Exception ex) { LOG.warn("Warm-up failed (continuing): {}", ex.getMessage()); } System.gc(); Thread.yield(); } /** * Executes a single backtest run with the specified configuration. *

* This method: *

    *
  1. Slices the base series to the requested bar count
  2. *
  3. Applies the maximum bar count hint if specified
  4. *
  5. Creates the requested number of strategies
  6. *
  7. Executes the backtest with progress monitoring
  8. *
  9. Captures performance metrics (GC, heap, runtime statistics)
  10. *
  11. Logs results in JSON format with the {@code HARNESS_RESULT:} prefix
  12. *
* * @param baseSeries The base bar series to use * @param config Configuration for this run (strategy count, bar count, * execution mode, etc.) * @return A {@link RunOutcome} containing both the execution result and * performance metrics * @throws NullPointerException If baseSeries or config is null */ private static RunOutcome runOnce(BarSeries baseSeries, RunOnceConfig config) { Objects.requireNonNull(baseSeries, "baseSeries must not be null"); Objects.requireNonNull(config, "config must not be null"); BarSeries series = sliceToLastBars(baseSeries, config.barCount()); series = applyMaximumBarCountHint(series, config.maximumBarCountHint()); long strategiesStart = System.nanoTime(); List strategies = createStrategies(series, config.strategyCount()); Duration strategiesBuildDuration = Duration.ofNanos(System.nanoTime() - strategiesStart); int barCount = series.getEndIndex() - series.getBeginIndex() + 1; long workUnits = (long) strategies.size() * (long) barCount; LOG.info("Backtesting {} strategies (mode={}) on {} bars (maxBarCountHint={}, heapMax={})", strategies.size(), config.executionMode(), barCount, series.getMaximumBarCount(), formatBytes(Runtime.getRuntime().maxMemory())); GcSnapshot gcBefore = GcSnapshot.capture(); HeapSnapshot heapBefore = HeapSnapshot.capture(); Consumer progressCallback = config.progress() ? ProgressCompletion.loggingWithMemory(BacktestPerformanceTuningHarness.class) : null; BacktestExecutionResult result = executeBacktest(series, strategies, config.executionMode(), config.topK(), progressCallback); HeapSnapshot heapAfter = HeapSnapshot.capture(); GcSnapshot gcAfter = GcSnapshot.capture(); GcSnapshot gcDelta = gcAfter.delta(gcBefore); BacktestRuntimeStats runtimeStats = BacktestRuntimeStats.from(result.runtimeReport()); RunResult runResult = new RunResult(config.executionMode(), strategies.size(), barCount, config.maximumBarCountHint(), series.getMaximumBarCount(), config.barCount(), strategiesBuildDuration, runtimeStats, workUnits, gcDelta, heapBefore, heapAfter, series.numFactory().getClass().getSimpleName()); LOG.info("Backtest complete. runtimeReport={}", runtimeStats.runtimeReportJson()); LOG.info(HARNESS_RESULT_PREFIX + "{}", runResult.toJson()); return new RunOutcome(result, runResult); } private static BacktestExecutionResult executeBacktest(BarSeries series, List strategies, ExecutionMode mode, int topK, Consumer progressCallback) { BacktestExecutor executor = new BacktestExecutor(series); Num amount = series.numFactory().numOf(1); if (mode == ExecutionMode.KEEP_TOP_K) { int effectiveTopK = Math.max(1, topK); AnalysisCriterion criterion = new NetProfitCriterion(); return executor.executeAndKeepTopK(strategies, amount, Trade.TradeType.BUY, criterion, effectiveTopK, progressCallback); } return executor.executeWithRuntimeReport(strategies, amount, Trade.TradeType.BUY, progressCallback); } private static void runTuneInProcess(BarSeries baseSeries, HarnessCli cli) { Thresholds thresholds = new Thresholds(cli.nonlinearGcOverheadThreshold, cli.nonlinearSlowdownRatioThreshold); TunePlan plan = TunePlan.fromCli(cli, baseSeries.getBarCount()); LOG.info("Tuning plan: {}", plan.describe()); LOG.info("Non-linear thresholds: {}", thresholds.describe()); List variantResults = new ArrayList<>(plan.variants().size()); for (SeriesVariant variant : plan.variants()) { BarSeries series = variant.apply(baseSeries); series = applyMaximumBarCountHint(series, variant.maximumBarCountHint()); LOG.info("=== Series variant: {} ===", variant.describe(series)); RunResult lastLinear = null; RunResult previous = null; RunResult firstNonLinear = null; for (int strategyCount : plan.strategyCounts()) { RunOnceConfig runConfig = new RunOnceConfig(strategyCount, variant.barCount(), variant.maximumBarCountHint(), plan.executionMode(), plan.topK(), plan.progress()); RunOutcome outcome = runOnce(series, runConfig); RunResult current = outcome.runResult(); if (previous != null && isNonLinear(previous, current, thresholds)) { firstNonLinear = current; LOG.info("Non-linear behavior detected at strategies={} (previousLinearStrategies={})", current.strategyCount(), lastLinear != null ? lastLinear.strategyCount() : null); break; } lastLinear = current; previous = current; if (plan.gcBetweenRuns()) { System.gc(); Thread.yield(); } } if (lastLinear == null) { LOG.info("No linear runs recorded for {}", variant.describe(series)); } else { LOG.info("Sweet spot (last linear run): {}", lastLinear.describeSweetSpot()); } variantResults.add(new VariantTuningResult(variant, lastLinear, firstNonLinear)); } logRecommendedSettings(cli, plan, thresholds, variantResults); } private static void logRecommendedSettings(HarnessCli cli, TunePlan plan, Thresholds thresholds, List results) { long heapMax = Runtime.getRuntime().maxMemory(); LOG.info("=== Recommended settings (heapMax={}, dataset={}) ===", formatBytes(heapMax), cli.ohlcResourceFile); LOG.info("Non-linear definition: {}", thresholds.describe()); RunResult best = selectBestRecommendation(results); if (best == null) { LOG.info(RECOMMENDED_SETTINGS_PREFIX + "No recommendation available (no successful linear runs)."); return; } LOG.info(RECOMMENDED_SETTINGS_PREFIX + "BEST {}", best.describeSweetSpot()); LOG.info(RECOMMENDED_SETTINGS_PREFIX + "BEST CLI {}", buildRunOnceArgs(cli, plan, best)); for (VariantTuningResult result : results) { if (result.lastLinear() == null) { continue; } String label = result.variant().describeLabel(); String transition = result.firstNonLinear() == null ? "no non-linear detected up to max tested" : "non-linear at strategies=" + result.firstNonLinear().strategyCount(); LOG.info(RECOMMENDED_SETTINGS_PREFIX + "{} strategies<={} ({}) | {}", label, result.lastLinear().strategyCount(), transition, buildRunOnceArgs(cli, plan, result.lastLinear())); } LOG.info(RECOMMENDED_SETTINGS_PREFIX + "If you hit 'no non-linear detected', increase --tuneStrategyMax to probe further."); } /** * Selects the best recommendation from a list of variant tuning results. *

* The best recommendation is determined by: *

    *
  1. Highest work units (strategies × bars) - indicates most work done * efficiently
  2. *
  3. Highest strategy count (tie-breaker)
  4. *
  5. Highest bar count (tie-breaker)
  6. *
  7. Highest effective maximum bar count hint (tie-breaker)
  8. *
*

* Only results with a non-null {@code lastLinear} (indicating successful linear * performance) are considered. * * @param results List of variant tuning results to evaluate * @return The best recommendation, or null if no valid results are found */ static RunResult selectBestRecommendation(List results) { if (results == null || results.isEmpty()) { return null; } return results.stream() .map(VariantTuningResult::lastLinear) .filter(Objects::nonNull) .max(Comparator.comparingLong(RunResult::workUnits) .thenComparingInt(RunResult::strategyCount) .thenComparingInt(RunResult::barCount) .thenComparingInt(RunResult::maximumBarCountHintEffective)) .orElse(null); } private static String buildRunOnceArgs(HarnessCli cli, TunePlan plan, RunResult recommendation) { StringJoiner args = new StringJoiner(" "); args.add("--dataset"); args.add(cli.ohlcResourceFile); args.add("--strategies"); args.add(Integer.toString(recommendation.strategyCount())); if (recommendation.barCountRequested() > 0) { args.add("--barCount"); args.add(Integer.toString(recommendation.barCountRequested())); } if (recommendation.maximumBarCountHintRequested() > 0) { args.add("--maxBarCountHint"); args.add(Integer.toString(recommendation.maximumBarCountHintRequested())); } args.add("--executionMode"); args.add(plan.executionMode() == ExecutionMode.KEEP_TOP_K ? "topK" : "full"); if (plan.executionMode() == ExecutionMode.KEEP_TOP_K) { args.add("--topK"); args.add(Integer.toString(plan.topK())); } return args.toString(); } private static void runTuneAcrossHeaps(HarnessCli cli) throws Exception { List childArgs = cli.toChildTuneArgs(); String javaExecutable = javaExecutablePath(); String classpath = System.getProperty("java.class.path"); for (String heap : cli.tuneHeaps) { LOG.info("=== Forking tune run: heap={} ===", heap); List command = new ArrayList<>(); command.add(javaExecutable); command.add("-Xms" + heap); command.add("-Xmx" + heap); command.add("-cp"); command.add(classpath); command.add(BacktestPerformanceTuningHarness.class.getName()); command.addAll(childArgs); ProcessBuilder builder = new ProcessBuilder(command); builder.redirectErrorStream(true); Process process = builder.start(); try (BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { String line; while ((line = reader.readLine()) != null) { LOG.info(line); } } int exitCode = process.waitFor(); if (exitCode != 0) { throw new IllegalStateException("Child JVM exited with code=" + exitCode + " for heap=" + heap); } } } private static String javaExecutablePath() { String javaHome = System.getProperty("java.home"); String executable = isWindows() ? "java.exe" : "java"; return Path.of(javaHome, "bin", executable).toString(); } private static boolean isWindows() { String osName = System.getProperty("os.name"); return osName != null && osName.toLowerCase(Locale.ROOT).contains("win"); } private static BarSeries loadSeries(String jsonOhlcResourceFile) { try (InputStream resourceStream = BacktestPerformanceTuningHarness.class.getClassLoader() .getResourceAsStream(jsonOhlcResourceFile)) { if (resourceStream == null) { LOG.error("Resource not found: {}", jsonOhlcResourceFile); return null; } return JsonFileBarSeriesDataSource.DEFAULT_INSTANCE.loadSeries(resourceStream); } catch (IOException ex) { LOG.error("IOException while loading resource: {} - {}", jsonOhlcResourceFile, ex.getMessage()); return null; } } /** * Determines if performance has degraded non-linearly between two runs. *

* Non-linear behavior is detected when either: *

    *
  • GC overhead exceeds the threshold (default: 25% of total runtime)
  • *
  • Normalized slowdown ratio exceeds the threshold (default: 1.25x)
  • *
*

* The normalized slowdown ratio is calculated as: * *

{@code
     * (runtimeRatio / workRatio)
     * }
* * where runtimeRatio is the ratio of runtimes and workRatio is the ratio of * work units (strategies × bars). A value greater than 1.0 indicates that * runtime increased faster than work, suggesting non-linear scaling. *

* This method is used during tuning to identify the point where increasing * strategy count or bar count causes performance to degrade beyond expected * linear scaling. * * @param previous The previous run result (baseline) * @param current The current run result (to compare against baseline) * @param thresholds The thresholds for detecting non-linear behavior * @return true if non-linear behavior is detected, false otherwise * @throws NullPointerException If any parameter is null */ static boolean isNonLinear(RunResult previous, RunResult current, Thresholds thresholds) { Objects.requireNonNull(previous, "previous must not be null"); Objects.requireNonNull(current, "current must not be null"); Objects.requireNonNull(thresholds, "thresholds must not be null"); if (previous.workUnits() <= 0 || current.workUnits() <= 0) { return false; } if (previous.runtimeStats().overallRuntime().isZero() || current.runtimeStats().overallRuntime().isZero()) { return false; } double workRatio = current.workUnits() / (double) previous.workUnits(); double runtimeRatio = current.runtimeStats().overallRuntime().toNanos() / (double) previous.runtimeStats().overallRuntime().toNanos(); double normalizedSlowdown = runtimeRatio / workRatio; double gcOverhead = current.gcOverhead(); boolean gcNonLinear = gcOverhead >= thresholds.gcOverheadThreshold(); boolean slowdownNonLinear = normalizedSlowdown >= thresholds.slowdownRatioThreshold(); if (gcNonLinear || slowdownNonLinear) { LOG.info("Non-linear check: gcOverhead={} (threshold={}), slowdown={} (threshold={})", formatPercent(gcOverhead), formatPercent(thresholds.gcOverheadThreshold()), String.format(Locale.ROOT, "%.3f", normalizedSlowdown), String.format(Locale.ROOT, "%.3f", thresholds.slowdownRatioThreshold())); return true; } return false; } private static String formatPercent(double value) { return String.format(Locale.ROOT, "%.2f%%", value * 100d); } private static long elapsedMillis(long startedNanos) { return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedNanos); } private static void writeJson(Path path, JsonObject object) throws IOException { Files.createDirectories(path.getParent()); Files.writeString(path, PRETTY_GSON.toJson(object) + System.lineSeparator(), StandardCharsets.UTF_8); } static String shortSha256(String value) { try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] hash = digest.digest(value.getBytes(StandardCharsets.UTF_8)); StringBuilder builder = new StringBuilder(); for (int i = 0; i < 8; i++) { builder.append(String.format(Locale.ROOT, "%02x", hash[i])); } return builder.toString(); } catch (NoSuchAlgorithmException ex) { throw new IllegalStateException("SHA-256 is unavailable", ex); } } /** * Slices a bar series to contain only the last N bars. *

* If barCount is 0, negative, or greater than or equal to the available bars, * the original series is returned unchanged. Otherwise, returns a sub-series * containing the last barCount bars. *

* This is useful for testing performance with different dataset sizes without * loading multiple files. * * @param series The bar series to slice * @param barCount The number of bars to keep (0 or negative = keep all) * @return A sub-series containing the last barCount bars, or the original * series if no slicing is needed * @throws NullPointerException If series is null */ static BarSeries sliceToLastBars(BarSeries series, int barCount) { Objects.requireNonNull(series, "series must not be null"); if (barCount <= 0) { return series; } int availableBars = series.getBarCount(); if (barCount >= availableBars) { return series; } int endExclusive = series.getEndIndex() + 1; int startIndex = Math.max(0, endExclusive - barCount); return series.getSubSeries(startIndex, endExclusive); } /** * Applies a maximum bar count hint to a bar series for indicator caching * optimization. *

* The maximum bar count hint controls the size of the indicator cache window. * When set, indicators will only cache values for the most recent N bars, * reducing memory usage for large datasets. *

* If maximumBarCountHint is 0 or negative, the original series is returned * unchanged. If it matches the series' current maximum bar count, the original * series is returned. Otherwise, returns a wrapper that overrides * {@link BarSeries#getMaximumBarCount()}. *

* This is useful for testing the impact of indicator caching on performance and * memory usage. * * @param series The bar series to wrap * @param maximumBarCountHint The maximum bar count hint (0 = disabled, use * series default) * @return A series with the maximum bar count hint applied, or the original * series if no change is needed * @throws NullPointerException If series is null */ static BarSeries applyMaximumBarCountHint(BarSeries series, int maximumBarCountHint) { Objects.requireNonNull(series, "series must not be null"); if (maximumBarCountHint <= 0) { return series; } if (maximumBarCountHint == series.getMaximumBarCount()) { return series; } return new MaxBarCountHintSeries(series, maximumBarCountHint); } /** * Creates a variety of strategies using NetMomentumIndicator with different * parameter combinations for performance testing. *

* Generates strategies by systematically varying: *

    *
  • RSI bar count: 7 to 49 (increment: 7)
  • *
  • Momentum timeframe: 100 to 400 (increment: 100)
  • *
  • Oversold threshold: -2000 to 0 (increment: 250)
  • *
  • Overbought threshold: 0 to 1500 (increment: 250)
  • *
  • Decay factor: 0.9 to 1.0 (increment: 0.02)
  • *
*

* This generates approximately 10,416 unique strategy combinations. When fewer * strategies are requested, the method samples from this grid. When more are * requested, it repeats the grid with different repetition markers. *

* Strategies use: *

    *
  • Entry rule: CrossedUpIndicatorRule when NetMomentumIndicator crosses * above oversold threshold
  • *
  • Exit rule: CrossedDownIndicatorRule when NetMomentumIndicator crosses * below overbought threshold
  • *
*

* Strategies that share the same RSI, Net Momentum timeframe, and decay factor * reuse the same indicator graph. The underlying cached indicators are * thread-safe, and sharing them keeps this benchmark focused on rule thresholds * instead of repeatedly recomputing identical momentum series. * * @param series The bar series to use for indicator * calculations * @param requestedStrategyCount The number of strategies to create. Use -1 for * full grid, or a positive number to sample that * many strategies * @return A list of strategies to test * @throws NullPointerException If series is null * @throws IllegalArgumentException If requestedStrategyCount is zero */ static List createStrategies(BarSeries series, int requestedStrategyCount) { Objects.requireNonNull(series, "series cannot be null"); int effectiveTarget; if (requestedStrategyCount < 0) { effectiveTarget = Integer.MAX_VALUE; } else if (requestedStrategyCount == 0) { throw new IllegalArgumentException("requestedStrategyCount must not be zero"); } else { effectiveTarget = requestedStrategyCount; } List strategies = new ArrayList<>(requestedStrategyCount > 0 ? requestedStrategyCount : 10_416); ClosePriceIndicator closePriceIndicator = new ClosePriceIndicator(series); Map rsiIndicators = new LinkedHashMap<>(); Map netMomentumIndicators = new LinkedHashMap<>(); int created = 0; int repetition = 0; while (created < effectiveTarget) { boolean fullGrid = requestedStrategyCount < 0; boolean addedAny = false; for (int rsiBarCount = RSI_BARCOUNT_MIN; rsiBarCount <= RSI_BARCOUNT_MAX; rsiBarCount += RSI_BARCOUNT_INCREMENT) { for (int timeFrame = MOMENTUM_TIMEFRAME_MIN; timeFrame <= MOMENTUM_TIMEFRAME_MAX; timeFrame += MOMENTUM_TIMEFRAME_INCREMENT) { for (int oversoldThreshold = OVERSOLD_THRESHOLD_MIN; oversoldThreshold <= OVERSOLD_THRESHOLD_MAX; oversoldThreshold += OVERSOLD_THRESHOLD_INCREMENT) { for (int overboughtThreshold = OVERBOUGHT_THRESHOLD_MIN; overboughtThreshold <= OVERBOUGHT_THRESHOLD_MAX; overboughtThreshold += OVERBOUGHT_THRESHOLD_INCREMENT) { if (oversoldThreshold >= overboughtThreshold) { continue; } for (double decayFactor = DECAY_FACTOR_MIN; decayFactor <= DECAY_FACTOR_MAX; decayFactor += DECAY_FACTOR_INCREMENT) { try { int currentRsiBarCount = rsiBarCount; int currentTimeFrame = timeFrame; double currentDecayFactor = decayFactor; NetMomentumIndicator netMomentumIndicator = netMomentumIndicators .computeIfAbsent( netMomentumKey(currentRsiBarCount, currentTimeFrame, currentDecayFactor), ignored -> NetMomentumIndicator .forRsiWithDecay( rsiIndicators.computeIfAbsent(currentRsiBarCount, key -> new RSIIndicator(closePriceIndicator, key)), currentTimeFrame, currentDecayFactor)); Strategy strategy = createStrategy(netMomentumIndicator, currentRsiBarCount, currentTimeFrame, oversoldThreshold, overboughtThreshold, currentDecayFactor, repetition); strategies.add(strategy); created++; addedAny = true; if (created >= effectiveTarget) { return strategies; } } catch (Exception e) { LOG.debug( "Skipping invalid strategy combination: rsiBarCount={}, timeFrame={}, oversoldThreshold={}, overboughtThreshold={}, decayFactor={}: {}", rsiBarCount, timeFrame, oversoldThreshold, overboughtThreshold, decayFactor, e.getMessage()); } } } } } } if (fullGrid) { break; } if (!addedAny) { break; } repetition++; } return strategies; } private static String netMomentumKey(int rsiBarCount, int timeFrame, double decayFactor) { return rsiBarCount + "|" + timeFrame + "|" + String.format(Locale.ROOT, "%.8f", decayFactor); } /** * Creates a single strategy using NetMomentumIndicator with the specified * parameters. *

* The strategy uses: *

    *
  • RSI indicator with the specified bar count
  • *
  • NetMomentumIndicator wrapping the RSI with the specified timeframe and * decay factor
  • *
  • Entry rule: Buy when NetMomentumIndicator crosses above the oversold * threshold
  • *
  • Exit rule: Sell when NetMomentumIndicator crosses below the overbought * threshold
  • *
*

* The repetition parameter is used to create multiple strategies with the same * parameters when more strategies are requested than the grid can provide. It's * included in the strategy name for identification. * * @param series The bar series to use for indicator calculations * @param rsiBarCount The number of bars to use for RSI calculation * (must be positive) * @param timeFrame The timeframe for NetMomentumIndicator (must be * positive) * @param oversoldThreshold The oversold threshold for entry signals * @param overboughtThreshold The overbought threshold for exit signals * @param decayFactor The decay factor for NetMomentumIndicator * (typically 0.9 to 1.0) * @param repetition The repetition number (0 for first occurrence, * incremented for repeats) * @return A new strategy with the specified parameters * @throws NullPointerException If series is null * @throws IllegalArgumentException If rsiBarCount or timeFrame is not positive */ static Strategy createStrategy(BarSeries series, int rsiBarCount, int timeFrame, int oversoldThreshold, int overboughtThreshold, double decayFactor, int repetition) { Objects.requireNonNull(series, "series cannot be null"); ClosePriceIndicator closePriceIndicator = new ClosePriceIndicator(series); RSIIndicator rsiIndicator = new RSIIndicator(closePriceIndicator, rsiBarCount); NetMomentumIndicator netMomentumIndicator = NetMomentumIndicator.forRsiWithDecay(rsiIndicator, timeFrame, decayFactor); return createStrategy(netMomentumIndicator, rsiBarCount, timeFrame, oversoldThreshold, overboughtThreshold, decayFactor, repetition); } private static Strategy createStrategy(NetMomentumIndicator netMomentumIndicator, int rsiBarCount, int timeFrame, int oversoldThreshold, int overboughtThreshold, double decayFactor, int repetition) { Objects.requireNonNull(netMomentumIndicator, "netMomentumIndicator cannot be null"); if (rsiBarCount <= 0) { throw new IllegalArgumentException("rsiBarCount should be positive"); } if (timeFrame <= 0) { throw new IllegalArgumentException("timeFrame should be positive"); } Rule entryRule = new CrossedUpIndicatorRule(netMomentumIndicator, oversoldThreshold); Rule exitRule = new CrossedDownIndicatorRule(netMomentumIndicator, overboughtThreshold); String suffix = repetition > 0 ? " (rep=" + repetition + ")" : ""; String strategyName = "Entry Crossed Up: {rsiBarCount=" + rsiBarCount + ", timeFrame=" + timeFrame + ", oversoldThreshold=" + oversoldThreshold + "}, Exit Crossed Down: {rsiBarCount=" + rsiBarCount + ", timeFrame=" + timeFrame + ", overboughtThreshold=" + overboughtThreshold + ", decayFactor=" + decayFactor + "}" + suffix; return new BaseStrategy(strategyName, entryRule, exitRule); } private static void logTopStrategies(BacktestExecutionResult result, int topK) { AnalysisCriterion netProfitCriterion = new NetProfitCriterion(); AnalysisCriterion expectancyCriterion = new ExpectancyCriterion(); List topStrategies = result.getTopStrategies(topK, netProfitCriterion, expectancyCriterion); LOG.debug("=== Top {} Strategies ===", topStrategies.size()); for (int i = 0; i < topStrategies.size(); i++) { TradingStatement statement = topStrategies.get(i); Strategy strategy = statement.getStrategy(); Num netProfit = statement.getCriterionScore(netProfitCriterion) .orElseGet(() -> netProfitCriterion.calculate(result.barSeries(), statement.getTradingRecord())); Num expectancy = statement.getCriterionScore(expectancyCriterion) .orElseGet(() -> expectancyCriterion.calculate(result.barSeries(), statement.getTradingRecord())); LOG.debug("{}. {}", (i + 1), strategy.getName()); LOG.debug(" Net Profit: {}", netProfit); LOG.debug(" Expectancy: {}", expectancy); LOG.debug(" Positions: {}", statement.getTradingRecord().getPositionCount()); } } private static void logUsage() { StringJoiner usage = new StringJoiner(System.lineSeparator()); usage.add("BacktestPerformanceTuningHarness - performance harness"); usage.add(""); usage.add("Run once (default):"); usage.add(" --strategies (default: full grid ~10,416)"); usage.add(" --barCount (default: full series)"); usage.add(" --maxBarCountHint (0 disables; default: 0)"); usage.add(" --executionMode full|topK (default: full)"); usage.add(" --topK (default: 20)"); usage.add(" --progress (enable progress+memory logging)"); usage.add(""); usage.add("Throughput control matrix:"); usage.add(" --throughputControl"); usage.add(" --throughputOutputDir

"); usage.add(" --matrixStrategyCounts (default: 250,500,1000)"); usage.add(" --matrixBarCounts (default: 500,1000; accepts full)"); usage.add(" --matrixMaxBarCountHints (default: 0)"); usage.add(" --parallelism (default: 1)"); usage.add(""); usage.add("Tune in current JVM:"); usage.add(" --tune"); usage.add(" --tuneStrategyStart (default: " + DEFAULT_TUNE_STRATEGY_START + ")"); usage.add(" --tuneStrategyStep (default: " + DEFAULT_TUNE_STRATEGY_STEP + ")"); usage.add(" --tuneStrategyMax (default: " + DEFAULT_TUNE_STRATEGY_MAX + ")"); usage.add(" --tuneBarCounts (default: 500,1000,2000,full)"); usage.add(" --tuneMaxBarCountHints (default: 0,512,1024,2048)"); usage.add(" --nonlinearGcOverhead <0..1> (default: " + DEFAULT_NONLINEAR_GC_OVERHEAD + ")"); usage.add(" --nonlinearSlowdownRatio (default: " + DEFAULT_NONLINEAR_SLOWDOWN_RATIO + ")"); usage.add(" --gcBetweenRuns (default: true)"); usage.add(""); usage.add("Tune across heaps (fork child JVM per heap):"); usage.add(" --tuneHeaps (e.g. 4g,8g,16g)"); LOG.info(System.lineSeparator() + usage); } /** * Formats a byte count as a human-readable string with appropriate units. *

* Formats bytes using binary units (KiB, MiB, GiB, TiB) with 2 decimal places. * Examples: *

    *
  • 1023 bytes → "1023 B"
  • *
  • 1024 bytes → "1.00 KiB"
  • *
  • 1048576 bytes → "1.00 MiB"
  • *
* * @param bytes The number of bytes to format * @return A formatted string with appropriate unit (B, KiB, MiB, GiB, or TiB) */ static String formatBytes(long bytes) { if (bytes < 1024) { return bytes + " B"; } double value = bytes; String[] units = new String[] { "B", "KiB", "MiB", "GiB", "TiB" }; int unitIndex = 0; while (value >= 1024d && unitIndex < units.length - 1) { value /= 1024d; unitIndex++; } return String.format(Locale.ROOT, "%.2f %s", value, units[unitIndex]); } } /** * Execution mode for backtest runs. *
    *
  • {@link #FULL_RESULT}: Execute all strategies and return full results for * all strategies
  • *
  • {@link #KEEP_TOP_K}: Execute all strategies but only keep results for the * top K performers (more memory-efficient for large strategy counts)
  • *
*/ enum ExecutionMode { /** Execute all strategies and return full results. */ FULL_RESULT, /** Execute all strategies but only keep top K results. */ KEEP_TOP_K } /** * Configuration for a single backtest run. * * @param strategyCount Number of strategies to test (-1 for full grid) * @param barCount Number of bars to use (0 or negative for full * series) * @param maximumBarCountHint Maximum bar count hint for indicator caching (0 to * disable) * @param executionMode Execution mode (FULL_RESULT or KEEP_TOP_K) * @param topK Number of top strategies to keep when using * KEEP_TOP_K mode * @param progress Whether to enable progress logging with memory * information */ record RunOnceConfig(int strategyCount, int barCount, int maximumBarCountHint, ExecutionMode executionMode, int topK, boolean progress) { } /** * Thresholds for detecting non-linear performance behavior. * * @param gcOverheadThreshold GC overhead threshold (0.0 to 1.0, e.g., 0.25 = * 25% of runtime) * @param slowdownRatioThreshold Normalized slowdown ratio threshold (e.g., 1.25 * = 25% slowdown) */ record Thresholds(double gcOverheadThreshold, double slowdownRatioThreshold) { String describe() { return String.format(Locale.ROOT, "{gcOverhead=%s, slowdownRatio>=%.3f}", formatPercent(gcOverheadThreshold), slowdownRatioThreshold); } private static String formatPercent(double value) { return String.format(Locale.ROOT, "%.2f%%", value * 100d); } } record TunePlan(List strategyCounts, List variants, ExecutionMode executionMode, int topK, boolean progress, boolean gcBetweenRuns) { static TunePlan fromCli(HarnessCli cli, int fullBarCount) { List strategyCounts = cli.buildTuneStrategyCounts(); List variants = cli.buildSeriesVariants(fullBarCount); return new TunePlan(strategyCounts, variants, cli.executionMode, cli.topK, cli.progress, cli.gcBetweenRuns); } String describe() { return String.format(Locale.ROOT, "{strategyCounts=%s, variants=%d, executionMode=%s, topK=%d}", strategyCounts, variants.size(), executionMode, topK); } } record SeriesVariant(int barCount, int maximumBarCountHint) { BarSeries apply(BarSeries baseSeries) { return BacktestPerformanceTuningHarness.sliceToLastBars(baseSeries, barCount); } String describeLabel() { return String.format(Locale.ROOT, "barCount=%s, maxBarCountHint=%s", barCount <= 0 ? "full" : Integer.toString(barCount), maximumBarCountHint <= 0 ? "default" : Integer.toString(maximumBarCountHint)); } String describe(BarSeries series) { return String.format(Locale.ROOT, "{barCount=%s, maxBarCountHint=%s, effectiveBars=%d}", barCount <= 0 ? "full" : Integer.toString(barCount), maximumBarCountHint <= 0 ? "default" : Integer.toString(maximumBarCountHint), series.getEndIndex() - series.getBeginIndex() + 1); } } record VariantTuningResult(SeriesVariant variant, RunResult lastLinear, RunResult firstNonLinear) { } record BacktestRuntimeStats(Duration overallRuntime, Duration minStrategyRuntime, Duration maxStrategyRuntime, Duration averageStrategyRuntime, Duration medianStrategyRuntime, String runtimeReportJson) { static BacktestRuntimeStats from(org.ta4j.core.backtest.BacktestRuntimeReport report) { return new BacktestRuntimeStats(report.overallRuntime(), report.minStrategyRuntime(), report.maxStrategyRuntime(), report.averageStrategyRuntime(), report.medianStrategyRuntime(), report.toString()); } } record RunOutcome(BacktestExecutionResult result, RunResult runResult) { } /** * Host metadata captured with a stable hashed host identifier so benchmark * artifacts can be shared without exposing local machine names. */ record HostTelemetry(String hostId, String osName, String osArch, String osVersion, int logicalProcessors, long maxMemoryBytes, String javaVersion, String javaVmName) { static HostTelemetry capture() { return new HostTelemetry(detectHostId(), System.getProperty("os.name", "unknown"), System.getProperty("os.arch", "unknown"), System.getProperty("os.version", "unknown"), Runtime.getRuntime().availableProcessors(), Runtime.getRuntime().maxMemory(), System.getProperty("java.version", "unknown"), System.getProperty("java.vm.name", "unknown")); } private static String detectHostId() { String hostname = detectHostname(); return "unknown".equals(hostname) ? hostname : "sha256:" + BacktestPerformanceTuningHarness.shortSha256(hostname); } private static String detectHostname() { try { return InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException ex) { return "unknown"; } } } record ThroughputMatrixCell(String cellId, int strategyCount, int barCount, int maximumBarCountHint, ExecutionMode executionMode, int topK) { RunOnceConfig toRunOnceConfig(boolean progress) { return new RunOnceConfig(strategyCount, barCount, maximumBarCountHint, executionMode, topK, progress); } JsonObject toJson() { return BacktestPerformanceTuningHarness.GSON.toJsonTree(this).getAsJsonObject(); } } record ThroughputCellResult(ThroughputMatrixCell cell, RunResult runResult, long wallTimeMs) { JsonObject toJson() { JsonObject object = new JsonObject(); object.add("cell", cell.toJson()); object.add("runResult", BacktestPerformanceTuningHarness.GSON.toJsonTree(runResult)); object.addProperty("wallTimeMs", wallTimeMs); object.addProperty("strategyBuildWallTimeMs", runResult.strategyBuildDuration().toMillis()); object.addProperty("backtestRuntimeMs", runResult.runtimeStats().overallRuntime().toMillis()); return object; } } record ThroughputControlPlan(String dataset, Path outputDir, List cells, String parallelism, int resolvedParallelism, ExecutionMode executionMode, int topK, boolean progress, boolean gcBetweenRuns, String specFingerprint) { static ThroughputControlPlan fromCli(HarnessCli cli, int fullBarCount) { List cells = cli.buildThroughputCells(fullBarCount); int resolvedParallelism = resolveParallelism(cli.parallelism, cells.size()); StringJoiner fingerprintSource = new StringJoiner("|"); fingerprintSource.add(cli.ohlcResourceFile) .add(cli.executionMode.name()) .add(Integer.toString(cli.topK)) .add(cli.parallelism) .add(Integer.toString(resolvedParallelism)) .add(Boolean.toString(cli.progress)) .add(Boolean.toString(cli.gcBetweenRuns)); cells.forEach(cell -> fingerprintSource.add(cell.toString())); Path outputDir = cli.throughputOutputDir == null ? Path.of(".agents", "benchmarks", "backtest-throughput", "matrix-" + Instant.now().toString().replace(':', '-')) : cli.throughputOutputDir; return new ThroughputControlPlan(cli.ohlcResourceFile, outputDir.toAbsolutePath().normalize(), cells, cli.parallelism, resolvedParallelism, cli.executionMode, cli.topK, cli.progress, cli.gcBetweenRuns, BacktestPerformanceTuningHarness.shortSha256(fingerprintSource.toString())); } JsonObject toManifest(HostTelemetry host) { JsonObject object = new JsonObject(); object.addProperty("schemaVersion", 1); object.addProperty("createdAt", Instant.now().toString()); object.addProperty("dataset", dataset); object.addProperty("specFingerprint", specFingerprint); object.addProperty("parallelism", parallelism); object.addProperty("resolvedParallelism", resolvedParallelism); object.addProperty("executionMode", executionMode.name()); object.addProperty("topK", topK); object.addProperty("progress", progress); object.addProperty("gcBetweenRuns", gcBetweenRuns); object.add("host", BacktestPerformanceTuningHarness.GSON.toJsonTree(host)); JsonArray cellArray = new JsonArray(); cells.forEach(cell -> cellArray.add(cell.toJson())); object.add("cells", cellArray); return object; } private static int resolveParallelism(String rawParallelism, int cellCount) { int cells = Math.max(1, cellCount); String raw = rawParallelism == null || rawParallelism.isBlank() ? "1" : rawParallelism.trim(); if ("auto".equalsIgnoreCase(raw)) { int processors = Math.max(1, Runtime.getRuntime().availableProcessors()); int withHeadroom = Math.max(1, (int) Math.ceil(processors * 0.50d)); return Math.min(cells, withHeadroom); } int parsed = Integer.parseInt(raw); if (parsed <= 0) { throw new IllegalArgumentException("--parallelism must be positive or auto"); } return Math.min(cells, parsed); } } /** * Aggregates additive throughput telemetry for fixed backtest matrix runs. */ final class ThroughputMatrixPerformanceTracker { private final List cells = new ArrayList<>(); synchronized void record(ThroughputCellResult cell) { cells.add(Objects.requireNonNull(cell, "cell")); } synchronized JsonObject cellsJson() { JsonObject root = new JsonObject(); JsonArray cellArray = new JsonArray(); for (ThroughputCellResult cell : cells) { cellArray.add(cell.toJson()); } root.add("cells", cellArray); return root; } synchronized JsonObject toJson(long totalWallTimeMs, ThroughputControlPlan plan, HostTelemetry host) { int cellCount = cells.size(); int hypothesisCount = 0; long sumCellWallTimeMs = 0L; long strategyBuildWallTimeMs = 0L; long backtestRuntimeMs = 0L; JsonArray cellArray = new JsonArray(); for (ThroughputCellResult cell : cells) { RunResult runResult = cell.runResult(); hypothesisCount += runResult.strategyCount(); sumCellWallTimeMs += cell.wallTimeMs(); strategyBuildWallTimeMs += runResult.strategyBuildDuration().toMillis(); backtestRuntimeMs += runResult.runtimeStats().overallRuntime().toMillis(); cellArray.add(cell.toJson()); } JsonObject root = new JsonObject(); root.addProperty("schemaVersion", 1); root.addProperty("completedAt", Instant.now().toString()); root.addProperty("dataset", plan.dataset()); root.addProperty("specFingerprint", plan.specFingerprint()); root.addProperty("parallelism", plan.parallelism()); root.addProperty("resolvedParallelism", plan.resolvedParallelism()); root.addProperty("executionMode", plan.executionMode().name()); root.addProperty("topK", plan.topK()); root.addProperty("progress", plan.progress()); root.addProperty("gcBetweenRuns", plan.gcBetweenRuns()); root.addProperty("totalWallTimeMs", totalWallTimeMs); root.addProperty("sumCellWallTimeMs", sumCellWallTimeMs); root.addProperty("strategyBuildWallTimeMs", strategyBuildWallTimeMs); root.addProperty("backtestRuntimeMs", backtestRuntimeMs); root.addProperty("cellCount", cellCount); root.addProperty("hypothesisKind", "strategy"); root.addProperty("hypothesisCount", hypothesisCount); root.addProperty("cellsPerMinute", perMinute(cellCount, totalWallTimeMs)); root.addProperty("hypothesesPerMinute", perMinute(hypothesisCount, totalWallTimeMs)); root.add("host", BacktestPerformanceTuningHarness.GSON.toJsonTree(host)); JsonObject phases = new JsonObject(); JsonObject matrix = new JsonObject(); matrix.addProperty("cellCount", cellCount); matrix.addProperty("hypothesisCount", hypothesisCount); matrix.addProperty("sumCellWallTimeMs", sumCellWallTimeMs); matrix.add("cells", cellArray); phases.add("backtest", matrix); root.add("phases", phases); return root; } private static double perMinute(long count, long wallTimeMs) { return wallTimeMs <= 0L ? count * 60_000.0d : count * 60_000.0d / wallTimeMs; } } /** * Results from a single backtest run, including performance metrics. * * @param executionMode The execution mode used * @param strategyCount Number of strategies tested * @param barCount Actual number of bars used * @param maximumBarCountHintRequested The maximum bar count hint that was * requested * @param maximumBarCountHintEffective The effective maximum bar count hint * applied * @param barCountRequested The bar count that was requested (0 = * full series) * @param strategyBuildDuration Time taken to build all strategies * @param runtimeStats Runtime statistics from the backtest * execution * @param workUnits Total work units (strategies × bars) * @param gcDelta GC statistics delta (after - before) * @param heapBefore Heap snapshot before execution * @param heapAfter Heap snapshot after execution * @param numFactory The NumFactory class name used */ record RunResult(ExecutionMode executionMode, int strategyCount, int barCount, int maximumBarCountHintRequested, int maximumBarCountHintEffective, int barCountRequested, Duration strategyBuildDuration, BacktestRuntimeStats runtimeStats, long workUnits, GcSnapshot gcDelta, HeapSnapshot heapBefore, HeapSnapshot heapAfter, String numFactory) { String toJson() { return BacktestPerformanceTuningHarness.GSON.toJson(this); } double gcOverhead() { if (runtimeStats.overallRuntime().isZero()) { return 0d; } return gcDelta.collectionTime().toNanos() / (double) runtimeStats.overallRuntime().toNanos(); } String describeSweetSpot() { return String.format(Locale.ROOT, "{strategies=%d, bars=%d, barCount=%s, maxBarCountHint=%s (effective=%s), heapMax=%s, overallRuntime=%s, gcOverhead=%s}", strategyCount, barCount, barCountRequested <= 0 ? "full" : Integer.toString(barCountRequested), maximumBarCountHintRequested <= 0 ? "default" : Integer.toString(maximumBarCountHintRequested), Integer.toString(maximumBarCountHintEffective), BacktestPerformanceTuningHarness.formatBytes(heapAfter.maxBytes()), runtimeStats.overallRuntime(), String.format(Locale.ROOT, "%.2f%%", gcOverhead() * 100d)); } } record HeapSnapshot(long maxBytes, long committedBytes, long usedBytes) { static HeapSnapshot capture() { MemoryUsage heap = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage(); return new HeapSnapshot(Runtime.getRuntime().maxMemory(), heap.getCommitted(), heap.getUsed()); } } record GcSnapshot(long collections, Duration collectionTime) { static GcSnapshot capture() { long count = 0; long timeMillis = 0; for (GarbageCollectorMXBean bean : ManagementFactory.getGarbageCollectorMXBeans()) { long beanCount = bean.getCollectionCount(); if (beanCount >= 0) { count += beanCount; } long beanTime = bean.getCollectionTime(); if (beanTime >= 0) { timeMillis += beanTime; } } return new GcSnapshot(count, Duration.ofMillis(timeMillis)); } GcSnapshot delta(GcSnapshot before) { return new GcSnapshot(collections - before.collections, collectionTime.minus(before.collectionTime)); } } /** * A wrapper around a BarSeries that overrides the maximum bar count hint for * indicator caching. *

* This wrapper is used during performance tuning to test the impact of * different maximum bar count hints on performance and memory usage. It * delegates all BarSeries operations to the underlying series but overrides * {@link #getMaximumBarCount()} to return the specified hint. *

* The maximum bar count hint cannot be changed after construction * (setMaximumBarCount throws UnsupportedOperationException) since this is a * hint-only override for benchmarking purposes. */ final class MaxBarCountHintSeries implements BarSeries { private static final long serialVersionUID = 4398573823756330718L; private final BarSeries delegate; private final int maximumBarCountHint; MaxBarCountHintSeries(BarSeries delegate, int maximumBarCountHint) { this.delegate = Objects.requireNonNull(delegate, "delegate must not be null"); this.maximumBarCountHint = maximumBarCountHint; } @Override public NumFactory numFactory() { return delegate.numFactory(); } @Override public BarBuilder barBuilder() { return delegate.barBuilder(); } @Override public String getName() { return delegate.getName(); } @Override public Bar getBar(int i) { return delegate.getBar(i); } @Override public int getBarCount() { return delegate.getBarCount(); } @Override public List getBarData() { return delegate.getBarData(); } @Override public int getBeginIndex() { return delegate.getBeginIndex(); } @Override public int getEndIndex() { return delegate.getEndIndex(); } @Override public int getMaximumBarCount() { return maximumBarCountHint; } @Override public void setMaximumBarCount(int maximumBarCount) { throw new UnsupportedOperationException("Maximum bar count is a hint-only override for benchmarking"); } @Override public int getRemovedBarsCount() { return delegate.getRemovedBarsCount(); } @Override public void addBar(Bar bar, boolean replace) { delegate.addBar(bar, replace); } @Override public void addTrade(Num tradeVolume, Num tradePrice) { delegate.addTrade(tradeVolume, tradePrice); } @Override public void addPrice(Num price) { delegate.addPrice(price); } @Override public BarSeries getSubSeries(int startIndex, int endIndex) { return delegate.getSubSeries(startIndex, endIndex); } } final class HarnessCli { boolean help; boolean tune; boolean throughputControl; boolean progress; boolean gcBetweenRuns = true; int topK = BacktestPerformanceTuningHarness.DEFAULT_TOP_K; int barCount; int strategyCount = -1; int maximumBarCountHint; int tuneStrategyStart = BacktestPerformanceTuningHarness.DEFAULT_TUNE_STRATEGY_START; int tuneStrategyStep = BacktestPerformanceTuningHarness.DEFAULT_TUNE_STRATEGY_STEP; int tuneStrategyMax = BacktestPerformanceTuningHarness.DEFAULT_TUNE_STRATEGY_MAX; double nonlinearGcOverheadThreshold = BacktestPerformanceTuningHarness.DEFAULT_NONLINEAR_GC_OVERHEAD; double nonlinearSlowdownRatioThreshold = BacktestPerformanceTuningHarness.DEFAULT_NONLINEAR_SLOWDOWN_RATIO; String ohlcResourceFile = BacktestPerformanceTuningHarness.DEFAULT_OHLC_RESOURCE_FILE; String parallelism = "1"; Path throughputOutputDir; ExecutionMode executionMode = ExecutionMode.FULL_RESULT; List tuneBarCounts = List.of(); List tuneMaxBarCountHints = List.of(); List tuneHeaps = List.of(); List matrixStrategyCounts = List.of(); List matrixBarCounts = List.of(); List matrixMaxBarCountHints = List.of(); static HarnessCli parse(String[] args) { HarnessCli cli = new HarnessCli(); if (args == null || args.length == 0) { return cli; } for (int i = 0; i < args.length; i++) { String arg = args[i]; switch (arg) { case "-h", "--help" -> cli.help = true; case "--tune" -> cli.tune = true; case "--throughputControl", "--throughput-control" -> cli.throughputControl = true; case "--progress" -> cli.progress = true; case "--gcBetweenRuns" -> cli.gcBetweenRuns = true; case "--noGcBetweenRuns" -> cli.gcBetweenRuns = false; case "--topK" -> cli.topK = Integer.parseInt(requireValue(args, ++i, arg)); case "--bars", "--barCount" -> cli.barCount = Integer.parseInt(requireValue(args, ++i, arg)); case "--strategies" -> cli.strategyCount = Integer.parseInt(requireValue(args, ++i, arg)); case "--maxBarCountHint" -> cli.maximumBarCountHint = Integer.parseInt(requireValue(args, ++i, arg)); case "--dataset" -> cli.ohlcResourceFile = requireValue(args, ++i, arg); case "--executionMode" -> cli.executionMode = parseExecutionMode(requireValue(args, ++i, arg)); case "--parallelism" -> cli.parallelism = parseParallelism(requireValue(args, ++i, arg)); case "--throughputOutputDir", "--throughput-output-dir" -> cli.throughputOutputDir = Path.of(requireValue(args, ++i, arg)); case "--matrixStrategyCounts", "--matrix-strategy-counts" -> cli.matrixStrategyCounts = parseCsvPositiveInts(requireValue(args, ++i, arg), arg); case "--matrixBarCounts", "--matrix-bar-counts" -> cli.matrixBarCounts = parseCsvBarCounts(requireValue(args, ++i, arg), arg); case "--matrixMaxBarCountHints", "--matrix-max-bar-count-hints" -> cli.matrixMaxBarCountHints = parseCsvNonNegativeInts(requireValue(args, ++i, arg), arg); case "--tuneStrategyStart" -> cli.tuneStrategyStart = Integer.parseInt(requireValue(args, ++i, arg)); case "--tuneStrategyStep" -> cli.tuneStrategyStep = Integer.parseInt(requireValue(args, ++i, arg)); case "--tuneStrategyMax" -> cli.tuneStrategyMax = Integer.parseInt(requireValue(args, ++i, arg)); case "--tuneBarCounts" -> cli.tuneBarCounts = parseCsvBarCounts(requireValue(args, ++i, arg), arg); case "--tuneMaxBarCountHints" -> cli.tuneMaxBarCountHints = parseCsvNonNegativeInts(requireValue(args, ++i, arg), arg); case "--nonlinearGcOverhead" -> cli.nonlinearGcOverheadThreshold = Double.parseDouble(requireValue(args, ++i, arg)); case "--nonlinearSlowdownRatio" -> cli.nonlinearSlowdownRatioThreshold = Double.parseDouble(requireValue(args, ++i, arg)); case "--tuneHeaps" -> cli.tuneHeaps = parseCsvStrings(requireValue(args, ++i, arg)); default -> throw new IllegalArgumentException("Unknown argument: " + arg); } } if (!cli.tuneHeaps.isEmpty()) { cli.tune = true; } return cli; } List toChildTuneArgs() { List args = new ArrayList<>(); args.add("--tune"); args.add("--dataset"); args.add(ohlcResourceFile); args.add("--executionMode"); args.add(executionMode == ExecutionMode.KEEP_TOP_K ? "topK" : "full"); args.add("--topK"); args.add(Integer.toString(topK)); args.add("--tuneStrategyStart"); args.add(Integer.toString(tuneStrategyStart)); args.add("--tuneStrategyStep"); args.add(Integer.toString(tuneStrategyStep)); args.add("--tuneStrategyMax"); args.add(Integer.toString(tuneStrategyMax)); if (!tuneBarCounts.isEmpty()) { args.add("--tuneBarCounts"); args.add(joinCsvInts(tuneBarCounts)); } if (!tuneMaxBarCountHints.isEmpty()) { args.add("--tuneMaxBarCountHints"); args.add(joinCsvInts(tuneMaxBarCountHints)); } args.add("--nonlinearGcOverhead"); args.add(Double.toString(nonlinearGcOverheadThreshold)); args.add("--nonlinearSlowdownRatio"); args.add(Double.toString(nonlinearSlowdownRatioThreshold)); if (progress) { args.add("--progress"); } if (gcBetweenRuns) { args.add("--gcBetweenRuns"); } else { args.add("--noGcBetweenRuns"); } return args; } List buildTuneStrategyCounts() { if (tuneStrategyStart <= 0 || tuneStrategyStep <= 0 || tuneStrategyMax <= 0) { throw new IllegalArgumentException("Tune strategy counts must be positive"); } if (tuneStrategyStart > tuneStrategyMax) { throw new IllegalArgumentException("tuneStrategyStart must be <= tuneStrategyMax"); } List counts = new ArrayList<>(); for (int strategies = tuneStrategyStart; strategies <= tuneStrategyMax; strategies += tuneStrategyStep) { counts.add(strategies); } return counts; } List buildSeriesVariants(int fullBarCount) { List variants = new ArrayList<>(); List barCounts = tuneBarCounts.isEmpty() ? List.of(500, 1_000, 2_000, 0) : tuneBarCounts; for (int barCount : barCounts) { int normalized = barCount <= 0 ? 0 : Math.min(barCount, fullBarCount); variants.add(new SeriesVariant(normalized, 0)); } List hints = tuneMaxBarCountHints.isEmpty() ? List.of(0, 512, 1_024, 2_048) : tuneMaxBarCountHints; for (int hint : hints) { if (hint < 0) { continue; } variants.add(new SeriesVariant(0, hint)); } return dedupeVariants(variants); } List buildThroughputCells(int fullBarCount) { List strategyCounts = matrixStrategyCounts.isEmpty() ? BacktestPerformanceTuningHarness.DEFAULT_MATRIX_STRATEGY_COUNTS : matrixStrategyCounts; strategyCounts = dedupeIntegers(strategyCounts); List barCounts = matrixBarCounts.isEmpty() ? BacktestPerformanceTuningHarness.DEFAULT_MATRIX_BAR_COUNTS : matrixBarCounts; barCounts = dedupeIntegers(barCounts); List maximumBarCountHints = matrixMaxBarCountHints.isEmpty() ? BacktestPerformanceTuningHarness.DEFAULT_MATRIX_MAX_BAR_COUNT_HINTS : matrixMaxBarCountHints; maximumBarCountHints = dedupeIntegers(maximumBarCountHints); List cells = new ArrayList<>(); for (int strategyCount : strategyCounts) { for (int rawBarCount : barCounts) { int barCount = rawBarCount <= 0 ? 0 : Math.min(rawBarCount, fullBarCount); for (int maximumBarCountHint : maximumBarCountHints) { String cellId = "s" + strategyCount + "-b" + (barCount <= 0 ? "full" : barCount) + "-m" + maximumBarCountHint; cells.add(new ThroughputMatrixCell(cellId, strategyCount, barCount, maximumBarCountHint, executionMode, topK)); } } } return cells; } private static List dedupeIntegers(List values) { List deduped = new ArrayList<>(); for (Integer candidate : values) { if (!deduped.contains(candidate)) { deduped.add(candidate); } } return deduped; } private List dedupeVariants(List variants) { List deduped = new ArrayList<>(); for (SeriesVariant candidate : variants) { boolean exists = false; for (SeriesVariant existing : deduped) { if (existing.barCount() == candidate.barCount() && existing.maximumBarCountHint() == candidate.maximumBarCountHint()) { exists = true; break; } } if (!exists) { deduped.add(candidate); } } return deduped; } private static ExecutionMode parseExecutionMode(String raw) { String normalized = raw == null ? "" : raw.trim().toLowerCase(Locale.ROOT); return switch (normalized) { case "topk", "top_k", "keeptopk", "keep_top_k" -> ExecutionMode.KEEP_TOP_K; case "full", "all", "full_result", "fullresult" -> ExecutionMode.FULL_RESULT; default -> throw new IllegalArgumentException("Unknown executionMode: " + raw); }; } private static String requireValue(String[] args, int index, String flag) { if (index >= args.length) { throw new IllegalArgumentException("Missing value for " + flag); } return args[index]; } private static List parseCsvStrings(String value) { if (value == null || value.isBlank()) { return List.of(); } return Arrays.stream(value.split(",")).map(String::trim).filter(part -> !part.isEmpty()).toList(); } private static String parseParallelism(String value) { String normalized = value == null ? "" : value.trim().toLowerCase(Locale.ROOT); if ("auto".equals(normalized)) { return normalized; } int parsed = Integer.parseInt(normalized); if (parsed <= 0) { throw new IllegalArgumentException("--parallelism must be positive or auto"); } return Integer.toString(parsed); } private static List parseCsvBarCounts(String value, String flag) { if (value == null || value.isBlank()) { return List.of(); } List values = Arrays.stream(value.split(",")) .map(String::trim) .filter(part -> !part.isEmpty()) .map(part -> parseBarCount(part, flag)) .toList(); return values; } private static int parseBarCount(String value, String flag) { if ("full".equalsIgnoreCase(value)) { return 0; } int parsed = Integer.parseInt(value); if (parsed < 0) { String source = flag == null ? "bar count" : flag; throw new IllegalArgumentException(source + " values must be >= 0 or full"); } return parsed; } private static List parseCsvPositiveInts(String value, String flag) { return parseCsvBoundedInts(value, flag, 1); } private static List parseCsvNonNegativeInts(String value, String flag) { return parseCsvBoundedInts(value, flag, 0); } private static List parseCsvBoundedInts(String value, String flag, int minimum) { if (value == null || value.isBlank()) { return List.of(); } List values = Arrays.stream(value.split(",")) .map(String::trim) .filter(part -> !part.isEmpty()) .map(Integer::parseInt) .toList(); for (int parsed : values) { if (parsed < minimum) { throw new IllegalArgumentException(flag + " values must be >= " + minimum); } } return values; } private static String joinCsvInts(List values) { return values.stream().map(Object::toString).reduce((left, right) -> left + "," + right).orElse(""); } }