Files
mymt5opp/05-config-and-inputs.md
2026-06-26 18:47:35 +08:00

181 lines
8.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.