# Vol-Gated Dip-Z — multi-symbol spec Measured on real broker H4 bars, 2021-01-04 → 2026-08-31, spreads taken from the bar data (not assumed). Code: `research/{backtest,run_screen,portfolio,vol_filter_test}.py`. ## The rule | | | |---|---| | **Instruments** | SP500, NAS100, US30, DAX40 — **equity indices only** | | **Timeframe** | H4 | | **Direction** | **Long only** | | **Entry** | z-score(20) of close ≤ −1.5, i.e. `(close − SMA20) / stdev20 ≤ −1.5` | | **Regime gate** | Garman-Klass σ(30), as a causal expanding-window percentile, **≥ 0.50** | | **Exit** | first close ≥ SMA20, else 10 bars elapsed | | **Stop** | 3 × ATR(14) from entry | | **Risk** | **0.25 % of equity per trade** at the stop distance | | **Fill** | signal on bar close, filled next bar open | Parameters are not fitted. Entry/exit/stop are the values **recovered from the surviving tester configs** of the lost Sep-13 build (`DipZ=1.5`, `DipExitMA=20`, `DipMaxBars=10`, `Direction=1`, `StopMode=3`). Only the regime gate and the risk fraction were added here. ## Measured results | window | n | trades/mo | bp/trade | CAGR | maxDD | ret/DD | screen | |---|---|---|---|---|---|---|---| | FULL 2021–26 | 653 | 10.1 | +23.8 | 2.7 % | 3.2 % | **4.79** | PASS | | OOS 2024–26 | 281 | 9.9 | +26.7 | 4.1 % | 3.2 % | **3.13** | PASS | | IS → 2024 | 372 | 11.7 | +21.5 | 1.8 % | 3.0 % | 1.63 | fails ret/DD | Screen = cadence ≥ 2/mo **and** maxDD ≤ 5 % **and** ret/DD ≥ 2. IS-weak / OOS-strong is the opposite of an overfit signature. Control: long-only random entry with matched holding period and trade count scores +1.2 to +3.5 bp/trade. The ungated strategy scores +6 to +16.5 bp *over* that control on the four indices; gated, +23.8. ## Why the gate points this way Expectancy rises monotonically with the volatility regime, in every window: | vol pctile | ≤0.30 | ≤0.50 | no gate | ≥0.50 | ≥0.70 | |---|---|---|---|---|---| | FULL bp/trade | 6.1 | 5.2 | 15.4 | 23.8 | 30.7 | | OOS bp/trade | 4.3 | **−2.4** | 11.3 | 26.7 | 29.0 | Low-volatility dips *lose money out of sample*. This is the brief's premise — "discard the pattern in a chop regime" — confirmed, but as a **regime gate on the primary signal**, not as a 70 %-accurate forecast. Every threshold in 0.35–0.60 passes the screen on FULL and OOS, so this is a plateau, not a tuned point. ## Two things the evidence rules out **Do not add forex or gold.** The gate's sign *reverses* there. High-vol minus low-vol bp/trade: SP500 5.1→26.3, NAS100 12.0→28.5, US30 2.1→12.9, DAX40 2.2→27.4 (4/4), versus EURUSD 2.6→4.2, USDJPY 9.3→**−1.4**, XAUUSD 16.4→**7.8**. **Do not expect diversification to fund larger size.** At 1 % risk the 4-index portfolio drew down 16 % against 6–8 % per symbol — the indices fall together. The portfolio buys trade *count* (10/mo vs ~4.5), not drawdown relief. Position sizing is what meets the prop limit. ## Bear-market test — RESOLVED (2026-09-22) Broker intraday history only reaches 2021, so the bear test ran on **D1, 2008–2026** (SP500/US30 from 2008-08, DAX40 from 2008-03, NAS100 from 2011-12), same rule shape, risk 0.25 %. `research/deep_test.py`. **The family survives bear markets.** Ungated D1 over 18 years: 634 trades, 2.9/mo, +59.4 bp/trade, total +21.7 %, **maxDD 2.8 %**, **ret/DD 7.74** — passes all three screens *through* 2008, 2011, 2015, 2018, 2020 and 2022. Worst years: 2008 −1.39 %, 2020 −0.38 %, 2022 −0.14 %. Losses are contained, not catastrophic; the 3×ATR stop and the small size do their job. **The vol gate raises expectancy everywhere but costs half the trades:** | | trades/mo | bp/trade | ret/DD | screen | |---|---|---|---|---| | D1 ungated 2008–26 | 2.9 | +59.4 | 7.74 | PASS | | D1 gated 2008–26 | 1.5 | +85.8 | 3.83 | fails cadence | | D1 ungated pre-2021 | 2.9 | +55.0 | 5.28 | PASS | | D1 gated pre-2021 | 1.3 | +91.6 | 2.50 | fails cadence | So the gate's *direction* replicates on D1 and in every era (85.8 vs 59.4; 91.6 vs 55.0 pre-2021) — it is a real, era-stable effect, not an artifact of the 2021–26 window. But on D1 the halved trade count drops cadence below the 2/month floor. **Conclusion: gate on H4, do not gate on D1.** H4 has 18.4 trades/mo to spend, so halving still leaves 10/mo; D1 has only 2.9 and cannot afford it. ## Open risks — read before sizing this live 1. **The H4 variant itself is still only 2021–26.** The bear evidence above is D1. The D1 and H4 rules are the same shape but not the same strategy, so treat the bear result as evidence about the *family*, not a direct test of the H4 configuration. 2. ~~**maxDD is a floor, not a ceiling.**~~ **Measured in the EA section below:** the tester's mark-to-market equity drawdown ran ~0.6 pp above the exit-based figure (3.89 % vs 3.30 %). Use the tester's Equity Drawdown Maximal from here on. 3. **Return is small by design.** 2.7–4.1 % CAGR at 0.25 % risk is consistent with the documented ~2–5 %/yr ceiling for this edge. It is a consistency vehicle, not a growth one. 4. **The 0.50 gate threshold was chosen after seeing results.** The 0.35–0.60 plateau is the defence; treat any single threshold as arbitrary within that band. --- ## THE EA — `mql5/WarriorDipZ.mq5` (2026-09-22) **Superseded design note:** the results in this section were measured with the v1 *basket* EA. Since v2.00 the EA is **one chart = one symbol** — see "How to run it" at the end of this file. The per-chart version reproduces these results (+14.7 %, 2.88 % DD across four charts). Deployed and compiled at `MQL5\Experts\Warrior\WarriorDipZ.ex5`; source of truth is this repo. `mql5/run_tests.ps1 ...` compiles, runs, verifies the launch and keeps each journal; `research/grid_summary.py` scores the runs. ### It reproduces the research Base rule vs `research/backtest.py`, 4 indices, H4, 2022-01 → 2026-08: **562 EA trades vs 562 backtest trades, 99 % matched, per-trade return correlation 0.998, stop/non-stop agreement 100 %**, mean gross +20.9 vs +21.9 bp. The four unmatched are the same trades filled one session later (the EA waits for the index CFD's 01:05 open; the backtest fills the 00:00 bar). Deterministic: re-runs give the same final balance to the cent. ### Production configuration and result (MT5 tester, 1-min OHLC, real broker swaps) | setting | value | |---|---| | basket | SP500, NAS100, US30, DAX40 | | risk / trade | 0.25 % | | open-risk cap | 0.75 % of equity across all positions | | Friday flat | **170 min before the symbol's own Friday session close** (broker session table; 23:50 server on all four here → 21:00 = 14:00 New York) — close, no entries until Monday | | kill switch | 4.5 % equity drawdown from peak — flatten and halt; **persists across restarts** | | window | trades | /mo | net | CAGR | **equity DD (mark-to-market)** | ret/DD | PF | |---|---|---|---|---|---|---|---| | 2022-01 → 2026-08 | 556 | 10.1 | **+11.6 %** | 2.4 % | **2.82 %** | **4.11** | 1.41 | IS (→2024) ret/DD 1.20, OOS (2024→) 4.21. Every index positive in both halves (SP500 +2,232, NAS100 +2,928, US30 +1,410, DAX40 +5,028). **Every calendar year positive, including 2022** (+1,675). Kill switch never fired. ### What the EA work changed, and why **Swap was the missing cost.** Index CFDs charge overnight financing; the Python backtest never modelled it. Without a Friday rule, swap consumed **23 % of gross profit**. Friday entries paid ~2.5× the swap of other days (weekend rollover) for gross that is regime noise (−2,496 before 2024, +2,497 after). **The Friday flat — your original mandate — is the single biggest improvement**, and it improves *both* halves, so it is structural, not fitted: | | net | eqDD | ret/DD | swap/gross | IS | OOS | |---|---|---|---|---|---|---| | no Friday flat | +8.5 % | 3.34 % | 2.55 | −23 % | 0.44 | 2.97 | | **flat Fri 21:00** | **+11.6 %** | **2.82 %** | **4.11** | **−10 %** | **1.20** | **4.21** | | flat Fri 17:00 | +12.1 % | 2.50 % | 4.85 | −10 % | 1.88 | 4.45 | 21:00 is the default because it is the "Friday afternoon" spec; 17:00 was slightly better but picking the better of two points on the same data is selection. **More symbols is NOT automatically better — the 7-index basket fails.** ESXEUR, F40EUR and HSIHKD passed a D1 screen (positive vs control in both eras), but on H4 they lost through 2022–23. Kill switch off, every open-risk cap: | basket | cap off | 1.0 % | 0.75 % | 0.5 % | |---|---|---|---|---| | 4 indices eqDD / ret/DD | 3.89 % / 2.42 | 3.63 % / 2.70 | 3.34 % / 2.55 | 1.97 % / 2.69 | | 7 indices eqDD / ret/DD | 6.35 % / 1.90 | 6.06 % / 1.60 | 6.94 % / 1.11 | 5.15 % / 0.69 | With the kill switch on, the 7-index basket tripped it on **2022-05-19** and never traded again. Adding correlated long index exposure adds simultaneous losses. **The open-risk cap exists for the same reason.** Per-trade risk says nothing about a selloff that stops every index at once. ### Before it goes live 1. **Forward-test on demo first.** Everything above is the tester. Live fills, slippage and the broker's real session behaviour are the untested layer. 2. **The profit is back-loaded.** 2026 (eight months) is 44 % of total net. The edge has strengthened with the era (seen in every study here) — do not size on 2026. 3. **Return is small by design.** 2.4 % CAGR at 0.25 % risk with 2.8 % drawdown. The ret/DD leaves room to raise risk toward ~0.4 % if the demo confirms the drawdown, not before. 4. ~~Server-time assumptions~~ **Resolved in v2.10:** the Friday flat is now "minutes before the symbol's own Friday session close", read from the broker's session table — the same mechanism as the original Warrior_EA's `CH_MARKET_CLOSE`. No hour to recompute on another broker. The default 170 reproduces the validated 21:00 run to the cent. 5. **Re-arming the kill switch** is manual: delete the `DipZ_halt_` global variable (F3 in the terminal). --- ## How to run it — one chart per symbol (v2.00, 2026-09-22) The EA no longer trades a basket. It trades **the chart it is attached to**. 1. Open four **H4** charts: SP500, NAS100, US30, DAX40 (the chart timeframe does not matter — the EA uses `InpTF` = H4 — but H4 makes the chart match what it trades). 2. Attach `Warrior\WarriorDipZ` to each. Leave **`InpMagic` identical on all four** — that is what lets them share one account-level open-risk cap and one kill switch. 3. Keep the defaults: risk 0.25 %, open-risk cap 0.75 %, Friday flat 170 min before the symbol's Friday close, kill 4.5 %. 4. Never attach two instances to the same symbol. How the charts cooperate without a basket: - **Open-risk cap** sums the stop-risk of *every* position with the shared magic, and the check-then-open is serialised by a terminal-global lock, so two charts rolling to the same H4 bar cannot both slip past the cap. - **Kill switch** watches *account* equity. Its peak and halt state live in terminal global variables (`DipZ_peak_`, `DipZ_halt_`): one trip flattens and halts every chart, and survives a restart. To re-arm, delete `DipZ_halt_` (F3). - Each chart only opens and closes its own symbol. Per-chart tester result (4 separate runs, recombined into one account curve by `research/combine_charts.py`, cap off): **+14.7 %, drawdown 2.88 % (3.38 % pessimistic bound), ret/DD 5.10, 11.2 trades/month, every year positive** — in line with the basket version. **Robustness:** all 36 neighbouring configurations (z −1.25…−2.0, MA 10/20/30, 5/10/15 bars) are positive out of sample (+13.8 … +51.1 bp/trade); the recovered live config sits mid-plateau. ### Real-tick validation — PASSED (2026-09-23) The same four per-chart runs, tester Model 4 (every tick based on **real** ticks). The broker has real ticks from 2022-05/10 onward (SP500 122 M ticks, NAS100 547 M, US30 183 M, DAX40 268 M; the M1-OHLC runs used ~6 M each): | | M1-OHLC | real ticks | |---|---|---| | SP500 | +2,613, DD 1.50 % | +2,582, DD 1.50 % | | NAS100 | +2,893 | +2,851 | | US30 | +3,198 | +3,247 | | DAX40 | +5,966 | +5,914 | | **account (combined)** | +14.7 %, DD 2.88 % | **+14.6 %, DD 2.88 %, ret/DD 5.07** | Same trades, same drawdown. This is the test that destroyed the forex weekend-gap fade (`research/FX_RESULTS.md`); the index strategy passes it because it trades H4 bars and holds for days, so the session-open spread is noise against the move it is paid for. --- ## NOW IN WARRIOR_EA (2026-09-23) — the product is `Warrior_EA.mq5` The rule, the account layer and the per-chart design were ported into Warrior's standard-library architecture. **Warrior's defaults are this book**: attach `Warrior\Warrior_EA` to H4 charts of SP500, NAS100, US30, DAX40, same `Magic` on all. `WarriorDipZ.mq5` stays in the repo as the reference implementation it was checked against. | piece | where | |---|---| | rule (z20 ≤ −1.5, SMA20 / 10-bar exit, vol gate) | `Signals/SignalDipBuy.mqh`, `DipEntry = DIP_ZSCORE` | | whole-history bars, Wilder ATR, GK sigma, expanding percentile | `System/BarCache.mqh` (no 1024-bar stdlib ceiling) | | stop = bid − 3 × Wilder ATR of the signal bar | `CWarriorSignal::SetupStop` → `CWarriorVote::Params` | | open-risk cap, kill switch, cross-chart lock, Friday flat | `System/AccountGuard.mqh`, driven by `CWarriorExpert` on every tick | | transient open failure retried on the next tick (bar not consumed) | `CWarriorExpert::Open` | | sizing on equity, fractional risk (`Risk = RISK_0_25`) | `Money/WarriorMoney.mqh`, `WARRIOR_RISK` | | trade journal `warrior_trades_.csv` | `System/TradeLog.mqh` | Two stdlib behaviours had to be closed for the port: a time exit on a bar that is still a dip netted entry 100 against exit 100 to a vote of 0 (never closed), and `CExpert::Processing` re-enters on the bar it just exited. The module's entry vote is now 0 while a position is held, and no entry is taken on a bar in which a stop was hit. **Trade-for-trade check against WarriorDipZ** (same tester, same day, 2022-01 → 2026-08, M1 OHLC, Friday flat 170, `research/compare_ea.py`): | | trades | matched | net Warrior | net DipZ | |---|---|---|---|---| | SP500 | 146 / 146 | 100 % | +2,613.03 | +2,613.03 | | US30 | 154 / 154 | 100 % | +3,197.74 | +3,197.74 | | DAX40 | 165 / 165 | 100 % | +5,966.11 | +5,966.11 | | NAS100 | 161 / 161 | 96.9 % | +2,917.00 | +2,893.45 | The NAS100 difference is two things, both on Warrior's side of honest: four early-2022 Friday entries that DipZ's 60-second timer filled at 01:05 on a stale quote when the history has no tick until 02:00 (Warrior acts on ticks), and three lots that round one 0.01 step differently (the broker's `OrderProfitCheck` vs tick value × distance). Re-arming the kill switch in Warrior: delete `Warrior_halt_` (F3). ### The neural networks: wired in, tested, OFF by default `DipMetaCut` puts an ALGLIB forest + MLP in front of every dip, trained walk-forward in the terminal (`System/DipMeta.mqh`). Two honest tests say it adds nothing yet, so the default stays `CONF_50` (off): - signal-bar state, 23 refits × 3 indices: AUC ~0.50 (2026-09-13); - cross-index / market-state inputs (breadth, dispersion, correlation, other indices' z and vol), pre-registered, 324 OOS trades 2023–26 (`research/NN_PLAN.md`, `NN_RESULTS.md`): MLP AUC 0.564, 95 % CI [0.498, 0.630] — **FAIL**. Its filter's ret/DD gain matches randomly skipping the same 35 % of signals (beaten 12 % of the time by chance); a different seed set gives AUC 0.529. The data is the constraint — ~300 out-of-sample trades cannot support a meta-label. Re-run `research/nn_cross_index.py` once another year of trades exists.