Feature Search
The stage that costs CPU. Eleven feature sets, each put through a two-stage grid search, then the survivors re-verified at full budget and filtered down to five low-correlation slots. Everything downstream reads this stage's output.
What this stage does
feature_search.py --engine lr --symbol XAUUSD --tf M5 --set all --cores 14
For each of the eleven feature sets, in order:
sl_mult × rr ×
lookahead, each simulated at the reduced budget
max_iter = 600. Keeps a top-3 diverse list,
de-duplicated on the whole parameter tuple.
training_bars × retrain_interval ×
batch_lr × long_thresh, run for each of the
three stage-1 survivors — 243 runs.
max_iter = 2000 — the budget the EA
will actually use — and keep those still passing.
0.85 with one already chosen.
The search space in full
Stage 1 — 64 combinations
| Parameter | Values | What it controls |
|---|---|---|
sl_mult | 1.5, 2.0, 2.5, 3.0 | Stop distance in ATR units. |
rr | 1.0, 1.5, 2.0, 3.0 | Take-profit as a multiple of the stop distance. |
lookahead | 2, 3, 5, 8 | Label horizon, and the online-learning lag. |
These three are searched first because they change the shape of every trade, and therefore the whole equity curve. Getting them roughly right before tuning the model is far cheaper than exploring the full cross-product.
(sl_mult, rr, lookahead) tuple. Without that, the top three would often
be three near-identical points around one good region, and stage 2 would search the
same neighbourhood three times — wasting two-thirds of its budget and missing
genuinely different regimes.
Stage 2 — 81 per survivor
| Parameter | Values | What it controls |
|---|---|---|
training_bars | 500, 1000, 1500 | Rolling training window length; also sets warmup. |
retrain_interval | 250, 500, 1000 | How often the weights are re-fitted from scratch. |
batch_lr | 0.005, 0.01, 0.02 | Gradient-descent step size for batch training. |
long_thresh | 0.50, 0.55, 0.60 | Probability above which a long is taken. |
81 × 3 survivors = 243 runs, all still at max_iter = 600.
The acceptance test and the score
ok = trades >= 30 AND profit_factor > 1.0
score = net_profit / (1 + abs(max_dd_pct) / 100)
Anything failing ok is scored -1e18 and effectively
vanishes from the ranking. This is what stops the search selecting a spectacular net
profit achieved in twelve trades, or a configuration that loses more often than it
wins.
The score divides net profit by a drawdown penalty. It is a ranking device, not a risk model — a −50% drawdown only divides the score by 1.5.
Why verify at 2,000 iterations?
The search runs at 600 iterations to keep 307 configurations per set affordable. But the EA does not run at 600 — it runs at the full budget. A configuration that looked best under-trained may not hold up when the gradient descent actually converges, and one that looked mediocre may improve.
So the top 40 are re-simulated at max_iter = 2000 and filtered again.
Only survivors of that pass go into the correlation filter. The results you
see are full-budget results.
max_iter = 2000. If the search selected at 600 and the portfolio
re-simulated at 2000, the reported per-slot metrics would not match the search's
own ranking, and the two stages would be describing different models.
The correlation filter
This is the step that decides what a "portfolio" actually contains.
N_SLOTS = 5MAX_CORR = 0.851e-12, otherwise the pair is not compared at allWhy differencing is essential
Raw equity curves for any two positions on the same instrument share the price trend of that instrument. Correlating the levels would make almost everything look correlated — the measure would be dominated by gold's own trajectory rather than by anything the model is doing.
Taking the first difference (equity[i] − equity[i−1]) removes that shared
trend and leaves the per-bar behaviour: when each slot wins, loses, and sits flat.
That is the thing worth diversifying.
Progress output
The script prints structured lines as it goes. These are the ones the Optimize page parses:
engine=lr symbol=XAUUSD tf=M5 data=data/XAUUSD_M5.csv out=strategies_lr_m5 cores=14
magic_offset(+0) -> M5-style base + offset for tf=M5
calibration ok: EURUSD 0.01 lot -> $0.10 per pip (lot_mult=100000)
=== set=base9 abbr=BAS features=9 magic_base=401 ===
[base9] stage1 64 combos ...
... 25/64 done
... 50/64 done
[base9] stage1 ok=13 diverse_top3: sl=1.5 rr=1.5 la=8 NP=14 DD=-0.15% PF=1.09 | ...
[base9] stage2 243 combos ...
[base9] combined ok=57/307
[base9] verifying 40 candidates @ max_iter=2000 ...
[base9] slot magic=401 PF=1.062 NP=+9 T=142 WR=... DD=-0.12% S=...
[base9] done in 41s -> 5 verified slots
wrote strategies_lr_m5/base9/ (configs.json slots.txt summary.json equity_*.csv)
=== set=momentum abbr=MOM features=7 magic_base=411 ===
...
ALL SETS DONE -> strategies_lr_m5
The ... N/M done line prints every 25 completions, not every one — which
is why the progress bar moves in visible jumps rather than smoothly.
How the progress bar maps to these lines
| Line | Sub-progress |
|---|---|
=== set=<name> abbr=… features=… | Step counter increments: "step N / 11" |
[set] stage1 N combos | 0.15 |
[set] stage2 N combos | 0.60 |
[set] verifying N candidates | 0.90 |
[set] done in Xs -> | Set complete |
ALL SETS DONE | 1.00 |
These weights are approximations of where the time actually goes: stage 2 (243 runs) costs far more than stage 1 (64 runs), and verification of 40 candidates at triple the iteration count is a substantial fraction again.
The --set flag is stricter than you expect
all
--set all9 fails with Unknown set 'all9'. The script has
no concept of the GUI's groupings — its loop handles all internally,
and anything else must be exactly one set name.
This is why the GUI expands groups such as
all9 or core6
into one process per set, and why all stays a single
process. It is also why the progress bar has to cope with two shapes: a single
process tracking its own per-set counter, or a queue of processes tracking
group_index / group_total.
all is also the cheapest option
One process looping eleven sets avoids eleven interpreter startups and eleven
separate loads of the CSV. Prefer --set all unless you specifically
want to resume a partial run.
Running a subset
# one concrete set
feature_search.py --engine lr --symbol XAUUSD --tf M5 --set base9 --cores 14
# everything
feature_search.py --engine lr --symbol XAUUSD --tf M5 --set all --cores 14
The Optimize page also offers skip sets already on disk, which runs only the missing sets. Because those may not be contiguous, the GUI passes an explicit list of set names on the command line rather than a group key.
Outputs
Per set, under strategies_lr_m5/<set>/:
| File | Contents |
|---|---|
summary.json |
The machine-readable result. Header keys: engine, set, abbr, data, bars, magic_start, n_features, union_features, max_spread, generated. Then slots[], each with magic, set, features, params and metrics. |
configs.json |
The parameter dicts for every slot, including its magic number — the input the EA generator consumes. |
slots.txt |
A human-readable block per slot: metrics on one line, then every parameter, one per line. |
equity_<magic>.csv |
One column, equity — the slot's equity curve, used for the correlation test and the charts. |
The Optimize page
summary.json once it lands: per-set metrics, feature counts and the in-sample caveat.Cores
The search is the only stage where the core count materially changes wall-clock time.
It uses a ProcessPoolExecutor, one worker per core.
The default is cpu_count − 2, leaving a core for the UI and the OS. On a
16-core machine, 14 is the default. Setting cores to the full count is usually a net
loss — the GUI updates lag badly and there is no idle headroom for the OS.
taskkill /T; on POSIX the
child runs in its own session and the whole group is killed.
Checkpoint
ALL SETS DONE -> strategies_lr_m5appeared.- Every set you intended to run wrote a
summary.json. - Each set reported 5 verified slots. A set that found fewer produces a smaller portfolio later.
- The per-set table shows plausible trade counts — double digits at minimum, since 30 is the acceptance floor.
| Symptom | Cause | Fix |
|---|---|---|
Data file not found |
The CSV is not at data/<SYMBOL>_<TF>.csv. |
Go back to Step 1. Pass --data only if the file genuinely lives elsewhere. |
Unknown set 'all9' |
--set was given a group name. |
Use a concrete set name or the literal all. |
| Zero trades / absurd results | Bad data — most often a zero spread column or a shifted clock. | Re-run the validator and read it properly. |
| A set reports fewer than 5 slots | The acceptance test or the correlation filter was too tight for that set. | Expected on some sets. Note it, because the portfolio will be smaller than 9. |
Calibration SystemExit |
The EURUSD dollars-per-pip derivation drifted from $0.10 by more than 5%. | A units regression in the code. Do not work around it — the money model is wrong. |