Search space
Everything the optimiser searches is a search space: a set of dimensions derived from the
input.* declarations in your Pine source. This page is about how that set is built, how its
size is calculated, and where the calculation will surprise you.
From Pine to dimensions
A declaration with a positional default becomes a dimension. Its type determines the kind:
| Pine | Dimension | How values are drawn |
|---|---|---|
input.int(a, b, "Name") | Integer | A stepped grid from min to max |
input.float(a, b, "Name") | Float | A stepped grid from min to max |
input.bool(…) | Boolean | Two values |
input.string(…, options) | Categorical | The supplied choices |
input.timeframe(…) | Categorical | The supplied timeframes |
You can exclude a parameter from the search entirely — it keeps its default in every trial.
Why the positional default matters
fast_len = input.int(9, "Fast EMA Length")
// ^^^^^^^^^^^ this first argument is what marks it tunableThe tooling reads the first argument as the value to search around and derives the bounds from it. A declaration that does not follow this shape is not picked up as a dimension, and it fails quietly — the parameter simply never varies.
This is also why the Python sweep tooling has a stricter variant of the same idea, and why it silently skips aligned declarations. Two different code paths, two different matching rules, one shared habit worth keeping.
Axis enumeration
A stepped dimension is materialised as a list of values:
| Kind | Enumeration |
|---|---|
| Boolean | [true, false] |
| Categorical | the choices, copied |
| Integer | min, min + step, … while ≤ max, each truncated to an integer |
| Float | min, min + step, … while ≤ max + step × 0.5, each rounded to the step’s decimal precision |
Why floats become infinite
The choice count for a numeric dimension is
floor(span / step) + 1. If that exceeds 10 000, the dimension is reported as
infinite rather than enumerated.
This is a guard against materialising an enormous array, and it has a visible consequence: Grid and Brute refuse to start on a space containing such a dimension. A float range with too fine a step is therefore not searchable by exhaustive methods at all — you need TPE, SA, GA or Random.
Sizing the space
estimatedCombinations is the product of every included axis’s choice count.
Once the running product exceeds the feasibility limit of 50 000, it is set to
Infinity, which clears the finiteSearchSpace flag and disqualifies the space from
exhaustive search.
| Threshold | Effect |
|---|---|
| ≤ 5 000 combinations | Auto routes to Brute |
| 5 001 – 50 000 | Auto routes to Grid |
| > 50 000 | Not finite → auto routes to TPE |
| Any float axis over 10 000 values | Not finite regardless of count |
The feasibility limit is a gate, not a truncation
The 50 000 limit does not silently run the first 50 000 combinations and call it done. It
marks the space as infinite, and Grid’s initialize() then throws:
“GridOptimizer: search space is not finite — use TPE/SA/GA/Random instead.”
You get an error, not a partial answer. That is the right behaviour — a truncated exhaustive search that reported itself as exhaustive would be far more dangerous.
Sampling
Randomised draws are uniform over each axis:
- Boolean —
rng() < 0.5 - Categorical — uniform over the choices
- Integer —
min + floor(rng() × n) × step - Float —
min + rng() × (max − min), i.e. continuous;stepis not used
Two asymmetries worth knowing
1. Integers are not clamped to max. The draw is
min + floor(rng() * n) * step with no final clamp. If max is not an
exact multiple of step above min, that top value is simply never
drawn. Enumeration and sampling therefore do not cover exactly the same set.
2. Floats ignore step when sampled. A float dimension is sampled
continuously even when a step is declared. Step granularity is re-imposed afterwards where
it matters — in the identity key and when snapping a neighbour — so the two views stay
consistent, but the raw draw is continuous.
Conditional parameters
A parameter can be marked conditional — “only meaningful when another parameter has this value”. This is honoured by exactly one algorithm.
The identity key
Algorithms that must recognise a point they have already seen use a canonical key built from the values.
The key walks parameters in declaration order, skipping excluded ones, and encodes each:
| Kind | Encoding |
|---|---|
| Integer | String(Math.round(v)) |
| Float | String(snapToStep(v, step)) |
| Boolean | "1" / "0" |
| Categorical | String(v ?? "") |
The encoded title=value fragments are then sorted and joined with &.
Why sorting matters
Sorting makes the key order-independent, so two candidates that assign the same values to the same parameters collide correctly regardless of the order they were assembled in.
Snapping floats to step granularity before encoding means that two values differing only in the last bits of floating-point representation produce the same key. Without this, a de-duplication set built on raw floats would miss near-duplicates and the search would waste trials on points it had already evaluated.
Neighbourhood
Simulated annealing and the genetic algorithm both need to propose a nearby point, and both use the same neighbourhood function.
It perturbs every included dimension on every step — not one chosen dimension:
| Kind | Neighbour |
|---|---|
| Boolean | Flipped with probability ½, else unchanged |
| Categorical | 70 % “perturb” (redrawn up to 8 times until it differs; kept if it cannot), 30 % uniform over all choices |
| Integer | If the axis has fewer than 4 values and a coin lands right, a uniform jump; otherwise a Gaussian step in step-space with σ = max(1, nSteps / 6), clamped to the axis |
| Float | cur + gauss() × σ with σ = max((max − min) / 20, step), clamped to range, then snapped to step |
Reproducibility
The search is seeded, and the seed is owned in one place.
The default generator is a mulberry32 implementation; SA additionally accepts a raw
function or an object exposing next(), and wraps it so draws are clamped to
[0, 0.9999999999] and a Gaussian source is available via Box–Muller with a cached spare.
Next
- Algorithms — how each strategy uses all of this.
- Execution — who runs the trial.