← Back to articles
July 3, 2026
5 min read

The Two-Axis Parameter Space: Why Most of Your Sweep Should Be Nearly Free

The Two-Axis Parameter Space: Why Most of Your Sweep Should Be Nearly Free
#algotrading
#backtest
#parameter search
#optimization
#caching
#curse of dimensionality
Part 4 of 10 · Collection
High-Performance Backtest Engines

Part of the "Backtests Without Illusions" series.

The curse of dimensionality is usually told as a warning: every parameter you add multiplies the search space, so an 18-dimensional strategy is hopeless to sweep. That framing quietly assumes every dimension costs the same to evaluate. It doesn't. In our dual/triple-timeframe engine, two-thirds of the parameters are almost free to sweep, and one-third carries essentially the entire compute bill. Once you see that split, "the search space is too big" stops being the right complaint. The right question is: which dimensions are you paying for?

The dimension that didn't need recomputing

Our parameter-search benchmark runs a multi-timeframe momentum strategy: a Hull moving average and its triple-smoothed variant (HMA3) on two or three timeframes, with a directional-separation gate that decides when a crossover is "clean" enough to act on. The full search space for the triple-TF variant is eighteen dimensions — per-timeframe period and HMA length, plus a set of separation thresholds for entry and exit on each timeframe.

The naive way to sweep this is a single loop: pick a parameter vector, build every indicator from scratch, run the simulation, score it, repeat. We started there. It was slow, and it was slow for a reason that turned out to be embarrassing once we named it: for the overwhelming majority of adjacent trials, we were recomputing indicators that had not changed.

Nudge the entry-separation threshold from 0.03 to 0.035 and re-run. The Hull MA over a year of 1-minute bars is bit-for-bit identical to the previous trial — the threshold does not appear anywhere in its definition. Yet the naive loop rebuilds it anyway, over half a million bars, three timeframes deep, every single time. We were spending almost all of our compute budget re-deriving quantities that were invariant to the parameter we were actually varying.

That observation is the whole article. The moment you notice that some parameters change the indicators and some only change the decision rule applied to fixed indicators, the parameter space stops being a flat cloud of dimensions and becomes two nested layers with wildly different price tags.

Two axes, not one

Two axes of a parameter space at different price tags: a sparse expensive indicator grid whose cells each unlock a dense, nearly free sheet of threshold configurations

Split the parameters by what re-evaluating them forces you to recompute:

  • The expensive axis — indicator parameters. Timeframe period and HMA length. Change either and you must rebuild the indicator across the entire price series: resample the higher timeframe, run the Hull weighting, recompute the crossover series and the separation at each cross. This is an O(n) sweep per bar of history, and you pay it in full for every distinct combination. In the triple-TF strategy this axis is six parameters: (period, hma_length) for each of the high, mid, and low timeframes.

  • The cheap axis — decision thresholds. The directional-separation gates that decide, given the already-computed crossover and separation series, whether to enter or exit. Change a threshold and no indicator moves. You only re-run a single O(n) pass over the precomputed signals, checking the gates and booking fills. In the triple-TF strategy this axis is twelve parameters (four separation thresholds — entry-buy, entry-sell, exit-buy, exit-sell — on each of three timeframes). The dual-TF variant has eight (four thresholds on each of two timeframes).

So the real shape of the space is: 6 expensive + 12 cheap = 18 for triple-TF, and 4 expensive + 8 cheap = 12 for dual-TF. Two-thirds of the dimensionality lives on the cheap axis. And the two axes don't just differ in count — they differ in unit cost by more than three orders of magnitude, which is the number the rest of this article is about.

The structure is strictly nested. Each point on the expensive axis — one concrete (period, hma_length) per timeframe — defines a set of fixed signal arrays. On top of that fixed foundation sits an entire sheet of the cheap axis: thousands of threshold vectors, each a fast pass over the same arrays. You pay for the foundation once and amortize it over the whole sheet.

Why indicators are invariant to thresholds

The caching only works because of a mathematical fact, not a lucky implementation detail, and it is worth stating precisely because it is the load-bearing assumption. The indicator arrays are a function of (period, hma_length) alone. The separation thresholds appear nowhere in their definition.

Concretely, for each timeframe the engine precomputes four aligned arrays over the base index: the Hull MA hma, its triple-smoothed hma3, the crossover series cross (+1 buy / −1 sell / 0), and the separation percentage at each cross. Every one of these is derived from prices and the two indicator parameters. The threshold — the number you compare separation against — is applied later, at decision time. It never touches the arrays.

This is not a trivially safe rearrangement. It is safe only because we were careful about when information becomes available, which is the concern of the look-ahead bias work elsewhere in this series. The higher-timeframe indicator on base bar i is computed from closed higher-TF candles plus a forming candle whose running close equals the current base close close[i] — a value known at bar i. Nothing from the future leaks in. So the precomputed signal at bar i is exactly what a live bot would have seen at bar i, and it stays valid no matter which threshold you later test against it. Caching a leaky signal would just cache the leak; caching a causal signal caches something you can actually trade.

The invariance gives us a factorization. Write the evaluation of one full parameter vector θ = (indicator_params, threshold_params) as:

signals   = build_indicators(indicator_params)      # EXPENSIVE, depends only on indicator_params
score     = simulate(signals, threshold_params)     # CHEAP, reuses signals across all thresholds

build_indicators does not read threshold_params. That single fact is what licenses the cache: hold indicator_params fixed, vary threshold_params freely, and signals is a constant you compute once.

The architecture: compute once, sweep many

A signal cache keyed by indicator parameters feeding a fast batch sweep: one indicator build fans out into a wide row of threshold evaluations sharing the same cached arrays

The engine (scripts/engine_multitf.py, commit bfc8aaa in our backtester) implements the factorization with two pieces whose names say exactly what they do.

SignalCache — the expensive axis, memoized. It is a dictionary keyed by (period_bars, hma_length). Ask it for a timeframe's signals and it returns the cached TFSignals if that indicator combo has been built before, otherwise it builds them once and stores them. Because the key is only the indicator parameters, every threshold config that shares an indicator combo — and in a dense threshold sweep that is thousands of them — hits the same cache entry. The higher timeframes especially reward this: a coarse grid of, say, four candidate periods times a handful of HMA lengths is a small number of distinct expensive builds, and each one is reused across the entire threshold sheet stacked on top of it.

sweep_separations — the cheap axis, batched. It takes the cached signal arrays and a matrix of threshold vectors (sps, shape [m, 12]) and runs them all through a single compiled kernel. Each row is one O(n) pass: walk the bars, apply the gates, book leak-free fills at open[i+1], tally PnL and time-in-position. No indicator is rebuilt inside this loop — it reads cross and separation straight from the cache. The inner simulation is JIT-compiled (Numba), so once warmed the per-config cost is dominated by the single linear scan over the bars, not by Python overhead.

The two pieces compose into the natural nested search: the outer loop walks the expensive axis (each iteration builds and caches one indicator combo), and the inner loop fans out a wide batch across the cheap axis on those cached arrays. In code the shape is exactly that — a small expensive loop wrapping a wide cheap batch:

cache = SignalCache(base_close, base_ts)           # keyed by (period, hma_length)

for htf_p, htf_h, mtf_p, mtf_h, ltf_p, ltf_h in indicator_grid:   # EXPENSIVE axis (coarse)
    htf = cache.get(htf_p, htf_h)                  # built once, then a cache hit forever
    mtf = cache.get(mtf_p, mtf_h)
    ltf = cache.get(ltf_p, ltf_h)

    sps = sample_thresholds(m=4000)                # CHEAP axis: [m, 12] threshold vectors
    pnl, n_trades, bars_in_pos = sweep_separations(  # one compiled batch, no indicator work
        base_close, base_open, htf, ltf, sps, mtf=mtf)

You touch the expensive builder as few times as the indicator grid allows, and you let the cheap sweep do the volume — thousands of sps rows against arrays that never move. That is the entire optimization — no approximation, no lost fidelity, just refusing to recompute what didn't change.

What "free" actually costs: the numbers

A bar comparison of per-configuration cost: a tall column for recompute-per-config beside a sliver for the cached threshold pass, annotated with the roughly 1,600x gap

We measured the two axes on the demo workload: a full year of 1-minute ETHUSDT bars (~527k bars), triple-TF, indicators warmed and JIT compiled out of the timing.

On the cheap axis, sweep_separations sustains ~5,600 threshold-configs per second. That is a complete simulation — gates, fills, PnL, exposure — for each config, over half a million bars, at roughly 180 microseconds per config. The reason it can be that fast is that it does zero indicator work: every config reads the same cached cross and separation arrays.

Now price the alternative. Building the triple-TF indicator set once takes on the order of a few hundred milliseconds (~0.3 s) — resampling three timeframes, Hull weighting, crossover and separation extraction over the whole year. If you recomputed indicators inside the config loop — the naive single-loop design — every one of those 5,600 configs per second would instead pay the full indicator build. The per-config cost balloons from ~180 microseconds to ~0.3 seconds:

Cost per config Configs/sec
Cheap axis (cached signals) ~180 µs ~5,600
Recompute indicators per config ~0.3 s ~3.4

The ratio is ~1,600x. Sweeping the cheap axis on cached signals is roughly three orders of magnitude cheaper than the naive design that rebuilds indicators for every threshold vector. Put concretely: a batch of a few thousand threshold configs that finishes in under a second on the cached path would take the better part of an hour if each config rebuilt its indicators. Same results, same fidelity, no shortcuts in the math — the only difference is that one of them recomputes an invariant and the other doesn't.

This is not a micro-optimization you sprinkle on at the end. It changes what searches are feasible. At 5,600 cfg/s the threshold axis is dense enough to explore properly — you can afford a fine grid or a long random/QMC sample per indicator combo — while the expensive axis stays a deliberately coarse handful of builds. The compute budget flows to where the parameters actually cost something.

The curse of dimensionality, re-priced

A flat uniform grid of parameters relabeled into a two-tier price map: a few costly cells outlined heavily, the rest shaded as nearly free

Go back to the framing this article opened with. The curse of dimensionality says the search space grows exponentially in the number of parameters, so more dimensions is categorically worse. That is true about the size of the grid. It is misleading about the cost of covering it, because it prices every dimension identically.

Once you separate the axes, the count reads differently. The triple-TF strategy is 18-dimensional, but only 6 of those dimensions are expensive. The other 12 are cheap-axis dimensions that expand the grid without expanding the compute bill in any meaningful way — you can throw thousands of configs at them for pennies. The dual-TF strategy is 12-dimensional with only 4 expensive dimensions and 8 cheap ones. In both cases the majority of the "curse" is concentrated on the axis that costs almost nothing to sweep.

So the honest way to reason about search cost is not "how many parameters" but "how many expensive parameters, and how coarse can their grid be." The expensive axis is where the exponential actually hurts, and it is where you want a small, well-chosen grid — a few candidate periods, a modest range of HMA lengths — possibly refined coarse-to-fine, in the spirit of the adaptive-resolution drill-down we use elsewhere. The cheap axis is where you can be lavish, because each additional threshold config is 180 microseconds.

This re-pricing generalizes well beyond our HMA strategy. The pattern — some parameters change the features, most parameters change the rule applied to fixed features — recurs everywhere in systematic trading. Indicator lengths, resampling frequencies, and lookback windows are expensive; entry/exit thresholds, stop distances, position-sizing multipliers, and confirmation gates are cheap. Any time a parameter only reshapes the decision boundary over already-computed signals, it belongs on the cheap axis, and it should be swept there. Related caching wins from the same family show up in multi-timeframe parquet caching and in the broader engine speed ladder.

Where the free lunch has a bill

The cheap axis is cheap in compute. It is not cheap in statistics, and conflating the two is how you turn a performance win into an overfitting machine.

Every threshold config you evaluate is a trial, and when you run thousands of trials on one dataset the best of them looks good partly by luck. Making those trials 1,600x cheaper does not make the luck go away — it makes it easier to accumulate. A dense threshold sweep is precisely the situation where multiple-testing inflation bites hardest: many candidates, one history, and a selection rule that reports the maximum. The fastest engine in the world will happily hand you a threshold vector that fits the noise of your test window beautifully and fails out of sample.

So the discipline has to scale with the speed. The moment the compute cost of a trial drops toward zero, the statistical accounting becomes the binding constraint, and you have to pay it explicitly:

  • Deflate for the trial count. Score the winner against how many configs you tried, not against zero. The Deflated Sharpe Ratio and the probability of backtest overfitting exist for exactly this — they turn "we tried 4,000 threshold vectors" into a haircut on the reported edge.
  • Validate out of sample, per fold. A cheap sweep must still be run inside an honest walk-forward split; a threshold that only wins in-sample is worthless no matter how fast you found it. Our engine keeps the fills leak-free (open[i+1]) precisely so the cheap axis can't buy performance by peeking.
  • Prefer plateaus to peaks. Because the threshold axis is dense and fast, you can map its response surface, not just its argmax. A broad region of thresholds that all work is a real edge; a single spiky maximum is a fitted artifact — the plateau-vs-peak distinction, made affordable by the very speed we're describing.

Read the two-axis structure the right way: it does not buy you more confidence, it buys you more trials at the same confidence cost per trial. That is genuinely valuable — dense coverage of the cheap axis is what lets you find plateaus and characterize the surface — but only if you keep the statistical ledger honest. Speed removes the compute excuse for not searching thoroughly; it does not remove the obligation to discount what thorough searching finds.

How to structure your own search

To apply this to your own strategy, the work is mostly classification — sorting your parameters onto the right axis — followed by a nested loop:

  1. Label every parameter by what it forces you to recompute. If changing it changes an indicator/feature array, it is expensive. If it only changes a comparison, threshold, or sizing rule applied to fixed arrays, it is cheap. When in doubt, ask: does this parameter appear anywhere inside the indicator's definition? If not, it's cheap.
  2. Key a cache on the expensive axis only. Memoize feature construction by its indicator parameters (as SignalCache does by (period, hma_length)). Adjacent trials that share an indicator combo then reuse the same arrays for free.
  3. Batch the cheap axis over cached features. Run threshold configs as a tight compiled loop over the precomputed signals, not as full re-evaluations. This is where your throughput — and in our case the ~5,600 cfg/s — comes from.
  4. Nest the loops: expensive outer, cheap inner. Keep the expensive grid small and deliberate (coarse, or coarse-to-fine); let the cheap sweep be dense. Spend the budget where the parameters cost something.
  5. Budget trials against overfitting, not against the clock. Now that the clock isn't the limit, let the deflated Sharpe / PBO be. Decide how many cheap-axis trials you can afford statistically, and validate the winner out of sample.

The engineering payoff and the statistical guardrail are two sides of one idea: separating the axes lets you search the cheap dimensions exhaustively, which is exactly why you must then discount how exhaustively you searched.

Takeaways

  1. Not all dimensions cost the same. A strategy's parameters split into an expensive axis (indicators — recomputed over the whole series) and a cheap axis (thresholds — an O(n) pass over precomputed signals). In our engine that's 6 expensive + 12 cheap for triple-TF, 4 + 8 for dual-TF.
  2. Indicators are invariant to thresholds, and that invariance is the whole optimization. Feature construction depends only on indicator parameters, so you build once, cache by (period, hma_length), and reuse across every threshold config that shares the combo.
  3. The cheap axis runs ~1,600x cheaper. ~5,600 threshold-configs/sec (~180 µs each) on cached signals, versus ~0.3 s per config if you rebuild indicators every time. Same fidelity — the only difference is refusing to recompute an invariant.
  4. The curse of dimensionality is really a curse of expensive dimensionality. Most of the parameter count lives on the axis that's nearly free to sweep. Keep the expensive grid coarse; be lavish on the cheap one.
  5. Speed shifts the binding constraint from compute to statistics. A dense, fast sweep is a multiple-testing engine. Deflate for the trial count, validate per fold, and prefer plateaus to peaks — the free lunch is real, but the statistical bill is not optional.

The full engine — SignalCache, sweep_separations, the leak-free multi-TF simulation, and the parity test that pins it to the live running-candle semantics — lives in scripts/engine_multitf.py (commit bfc8aaa) in our backtester. The next time someone tells you an 18-parameter strategy is too big to search, ask them how many of those parameters actually move the indicators. Usually it's a third of them, and the rest are nearly free.

Disclaimer: The information provided in this article is for educational and informational purposes only and does not constitute financial, investment, or trading advice. Trading cryptocurrencies involves significant risk of loss.

Authors

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

Stay Ahead of the Market

Subscribe to our newsletter for exclusive AI trading insights, market analysis, and platform updates.

We respect your privacy. Unsubscribe at any time.