# 05 — Configuration & Input Rules Every input to a backtest comes from one of **four declared sources**, never scattered through the code. This is what makes runs reproducible and the engine reusable. The four: 1. **Instrument config** — everything about the symbol/broker. 2. **Search space** — every tunable parameter, its range, its step. 3. **Wizard answers** — the run settings for *this* run (period, profile, trials, deposit, …). 4. **Frozen baseline** — the parameters you are *not* optimizing, pinned to known values. Keep these separate and you can always answer "what produced this number?" exactly. --- ## 1. Instrument config — the symbol as data One object describes a symbol under one broker. The engine reads **everything** about the instrument from here; nothing about ticks, spread, or swap is ever hard-coded elsewhere. ### Fields you need | Field | Meaning | Where to find it | |-------|---------|------------------| | `name`, `symbol` | human label + broker symbol code | your broker | | `point` | smallest price increment (e.g. `0.01`) | MT5 symbol spec | | `digits` | price decimal places | MT5 symbol spec | | `tick_size`, `tick_value` | price step and **money per lot per point** | MT5 symbol spec | | `contract_size` | units per lot | MT5 symbol spec | | `volume_min`, `volume_step`, `volume_max` | lot rounding/clamping | MT5 symbol spec | | `spread_mode` | `bar_column` (real per-bar) \| `fixed_points` \| `annual_avg` | your choice per data quality | | `spread_fixed_points` | used only with `fixed_points` | broker typical spread | | `swap_mode` | `fixed_per_lot` \| `annual_pct` | broker / your model | | `swap_long`, `swap_short` | per-lot-per-day rates (fixed mode) | MT5 symbol spec | | `swap_annual_pct` | annual rate on notional (percent mode) | your model | | `triple_swap_weekday` | the day swap is charged ×3 (often Wed) | broker | > **Get these from MT5's symbol specification** (`Market Watch → right-click symbol → Specification`), > not from memory. A wrong `tick_value` or `contract_size` scales every PnL by a constant and makes > the whole backtest meaningless. ### Real / worst / best profiles Keep **three variants** of each instrument config: - **`real`** — your broker's actual conditions. The default for ranking. - **`worst_case`** — wider spread, harsher swap. A finalist that survives this is robust to cost. - **`best_case`** — tighter spread. Shows the ceiling. Cost-stress every finalist across all three; if the edge only exists in `best_case`, it isn't real. ### Two instrument traps (both have bitten real labs) - **Quote currency ≠ account currency.** For a pair quoted in a non-account currency (e.g. a JPY-quoted pair on a USD account), an EA's "money mode" lot formula can mis-scale the lot by ~the exchange rate. Fix: generate that pair's presets with an **explicit fixed lot** equal to the intended cash amount ÷ contract size, and set the money-amount input to zero. Always check the **first trade's lot** against an MT5 run before trusting the series. - **Unreliable historical spread.** For some instruments (notably crypto) the recorded per-bar spread is garbage. Use `fixed_points` with a realistic value (and a wider `worst_case`) instead of `bar_column`. --- ## 2. Search space — declare every tunable parameter in one place The parameters Optuna is allowed to vary live in **one declared structure**, with an explicit range and step for each. Nothing tunable is hidden in the strategy body. A search space is a mapping of `name → (low, high, step)`, plus a note of which names are integers: ``` SEARCH_SPACE = { "open_distance": (0.1, 1.0, 0.05), # float "grid_step": (0.8, 3.0, 0.10), # float "grid_mult": (0.5, 1.3, 0.10), # float "grid_count": (2, 9, 1), # int "break_even": (0.0, 0.3, 0.05), # float "trailing_stop": (0.1, 0.5, 0.05), # float ... } INT_PARAMS = {"grid_count"} ``` The optimizer turns each entry into an Optuna `suggest_float` / `suggest_int` (doc 06). Document the **same** space in a human-readable `parameter-space.md` next to the strategy, with the *reasoning* for each range — why this floor, why this ceiling, what you expect. That note is half the value: it stops you (or your assistant) from re-litigating ranges every iteration and makes overfit ranges obvious. ### Rules for the search space - **Ranges encode priors, not hope.** A range should bracket where the answer plausibly is, informed by the instrument's character. Absurdly wide ranges waste trials and invite overfit corners. - **Step matters.** Too fine a step explodes the space; too coarse misses optima. Match the step to what's meaningful (e.g. a 0.05% grid step, a 1-unit count). - **What is NOT in the space is a decision.** Explicitly list parameters you deliberately exclude (because prior tests showed ~0 impact, or because changing them is a *different* hypothesis). An empty exclusion list usually means you haven't thought about it. - **Timeframe is usually fixed per iteration.** Searching across timeframes in one study is "timeframe-shopping" and overfits; pick the primary timeframe in the stats phase (doc 08) and lock it. --- ## 3. Frozen baseline — pin what you don't optimize The parameters outside the search space still need *values*. Pin them to the validated baseline (your known-good preset) so the optimizer varies only the few things under test, against an otherwise-known configuration. The run script merges sampled parameters **onto** this frozen baseline before building the engine params. This keeps each study **small and interpretable** — you're measuring the effect of the few tuned knobs, not drowning in a 30-dimensional search. --- ## 4. Lot / money mode — the input that silently breaks results Position sizing deserves its own section because it is the most common source of "my Python and MT5 disagree by a constant factor" bugs. Expose money-based sizing through **two inputs**, plus the fixed-lot guard: | Input | Meaning | |-------|---------| | `*_LotAmount` | the cash base. Lot = `LotAmount / open_price / contract_size`. | | `*_LotBalance` | the account balance this base is calibrated for (the scaling window). | | `*_Lot` | a fixed lot in lots. **Must be `0`** to enable money mode — otherwise the EA uses the fixed lot and **ignores** `LotAmount`. | Behavior: | `LotAmount` | `LotBalance` | Mode | Result | |-------------|--------------|------|--------| | X | **0** | **static** (default for testing) | lot always sized from cash X, independent of balance | | X | Y | **dynamic** | lot = base × (current_balance / Y) — grows/shrinks with the account | ### Rules - **Test with the static lot by default** (`LotBalance = 0`). Compounding (dynamic) is a *production* choice; for research you want a clean, balance-independent measurement. - **Always confirm `*_Lot = 0`** when you intend money mode. A stray non-zero fixed lot is a classic silent bug — the run "works" but sizes every trade wrong. - For a grid, levels multiply from the **base** lot via the grid multiplier (or ATR/other); the base comes from the money rule above. - Mirror whatever mode you choose **identically** in the Python engine and the generated `.set`, or the two tiers won't agree. --- ## 5. Wizard — capture run settings once, save them for reproducibility Before a run, a small interactive **wizard** asks the handful of settings that change run-to-run and writes them to `wizard-answers.yaml`. That YAML *is* the reproducibility record: anyone (including future-you) can see exactly what produced an iteration's numbers. Common wizard questions: ``` period_start / period_end # the backtest window instrument_profile # real / worst_case / best_case trials # Optuna trial budget (e.g. 300 / 1000 / 5000) max_dd_ccy # hard drawdown cap for the constraint top_n_verify # how many finalists to send to MT5 fixed_lot / initial_deposit # sizing context ``` Design notes: - Give every question a sensible **default** so a fast run is just pressing Enter. - Make the wizard **extensible**: a base set of common questions plus a per-strategy hook for extra ones (e.g. a grid wizard adds depth/multiplier defaults). - **Write the answers to disk** next to the iteration. A run with no saved settings is not reproducible and shouldn't be promoted. --- ## Putting it together A single run is fully specified by: **which instrument config** (symbol + profile), **which search space** (the tunable parameters), **which frozen baseline** (the pinned rest), and **which wizard answers** (the window, trials, deposit). Four declared sources, each in its own place. That is the whole "separate inputs" discipline — and it's why any run in this lab can be explained and reproduced exactly. Next: [`06-optimization-and-robustness.md`](06-optimization-and-robustness.md) — turning the search space into an Optuna study and not fooling yourself with the result.