回测基本一致

This commit is contained in:
2026-06-26 20:50:07 +08:00
parent 49be922517
commit 0dcbfe0781
58 changed files with 4843 additions and 40 deletions
+43 -20
View File
@@ -67,8 +67,13 @@ engine.run(
sl_prices, # array — the stop price for an entry on that bar (NaN if none)
tp_prices, # array — the target price
instrument, # InstrumentConfig — all symbol mechanics
lot / money_mode, # position sizing inputs
sizing, # SizingInputs — position sizing inputs
initial_deposit,
*, # keyword-only from here
m1_bars=None, # OPTIONAL: M1 bars for tick-level exit simulation
# REQUIRED if the EA moves its SL intra-trade (BE / trailing /
# basket trailing) — bar-level mode is untrustworthy for that
# class (doc 03 §7 failure mode, doc 03 §8 target gates).
) -> Result
```
@@ -76,6 +81,12 @@ engine.run(
> never decides *where* a stop goes — only *whether* price touched it. This is the seam that
> separates "the strategy" from "the simulator". Change your strategy → you change the caller and the
> arrays you hand in; the engine is untouched.
>
> **The `m1_bars` parameter is not optional for trailing/BE strategies.** When the EA updates its SL
> during a trade, the bar-level engine can produce a 40% to 50% net gap vs MT5 (doc 03 §7 measured
> failure mode). Pass M1 bars and the engine switches to tick-level exit simulation, dropping the
> gap to ~5%. Bar-level mode remains the right (and faster) choice for clean-directional setups that
> don't move the SL.
The engine's output is equally generic:
@@ -95,32 +106,44 @@ Factor, Win Rate, max Balance/Equity Drawdown, Sharpe, APR, trade count.
## 3. Data flow of one backtest
```
data/<symbol>/<SYM>_M1_<years>.parquet
│ load_bars() → DataFrame with per-bar spread column
caller: resample M1 → the signal timeframe (e.g. H1/H4/D1)
│ indicators.* on the resampled frame
caller: edge-detect signals (True only on the bar the condition first flips, not every bar after)
+ optional gates (regime/time filters AND-ed into the signal)
caller: compute SL/TP price arrays from params (ATR stop, % stop, indicator band, …)
engine.run(bars, signals, sl/tp, instrument, sizing, deposit)
Result → compute_metrics → dict
▼ (finalists only)
MT5 bridge: build .set from the same params → run real tester → parse report → compare
data/<symbol>/<SYM>_M1_<years>.parquet data/<symbol>/<SYM>_M5_<years>.parquet
│ load_bars() → M1 DataFrame │ load_bars() → signal-TF DataFrame
│ │ (resampled from M1 if needed)
│ ▼
caller: indicators.* on the signal timeframe
│ │
│ ▼
caller: edge-detect signals (True only on the bar
│ │ the condition first flips, not every bar after)
│ │ + optional gates (regime/time filters AND-ed in)
│ caller: compute SL/TP price arrays from params
│ (ATR stop, % stop, indicator band, …)
└──────────────┐ ┌──────────────────────────┘
▼ ▼
engine.run(signal_bars, signals, sl/tp, instrument, sizing, deposit,
m1_bars=m1_bars) ← M1 is passed back to the engine
│ for tick-level exit simulation when the EA
│ moves its SL intra-trade (BE / trailing). Without
│ it, the bar-level engine over-credits BE exits
│ (doc 03 §7 failure mode).
Result → compute_metrics → dict
▼ (finalists only)
MT5 bridge: build .set from the same params → run real tester → parse report → compare
```
Two subtleties that cause most bugs if missed (both explained in doc 03):
Three subtleties that cause most bugs if missed (the first two explained in doc 03):
- **Edge detection.** Signals must be `True` only on the *transition* bar, not forward-filled, or the
engine re-enters every bar.
- **Timeframe alignment.** Indicators computed on a higher timeframe must be mapped back onto the M1
bars correctly (no look-ahead — a daily value is only known after that day closes).
- **M1 must reach the engine for trailing/BE EAs.** Downloading M1 only to resample it up to the signal
timeframe is **not** enough if the EA moves its SL during a trade. Pass the M1 bars to `engine.run`
(the `m1_bars=` kwarg); otherwise the bar-level exit simulation produces a 40% to 50% net gap
(doc 03 §7) that looks like a "fidelity issue" but is actually a missing-input bug.
---