Files

181 lines
8.9 KiB
Markdown
Raw Permalink Normal View History

2026-06-26 18:47:35 +08:00
# 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.