2026-09-23 13:24:57 -04:00
# 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 <ini>...` 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_<magic>` 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_<magic>` , `DipZ_halt_<magic>` ): one trip flattens and halts every chart,
and survives a restart. To re-arm, delete `DipZ_halt_<magic>` (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.
feat(warrior): the vol-gated dip-buy book, ported into Warrior_EA
Warrior's defaults are now the validated book: DIP_ZSCORE alone, long only, H4,
risk 0.25%, one chart per index with a shared Magic.
- System/BarCache.mqh: whole-history closed bars, Wilder ATR, GK sigma and the
expanding vol percentile (no 1024-bar stdlib ceiling)
- System/AccountGuard.mqh: open-risk cap, kill switch, cross-chart lock and
Friday flat, shared through terminal globals by Magic
- CWarriorExpert: guard on every tick; a transient open failure retries the bar
- CWarriorSignal::SetupStop: the dip owns its 3 x Wilder ATR stop from the bid
- SignalDipBuy: no entry vote while holding (a still-dipping time exit never
closed, and Processing re-entered on the exit bar); no entry on a stop bar
- WarriorMoney sizes on equity; WARRIOR_RISK allows fractional risk
- TradeLog + research/compare_ea.py: trade-for-trade check vs WarriorDipZ -
SP500/US30/DAX40 identical to the cent, NAS100 96.9% (stale-quote timer fills)
- research/nn_cross_index.py: pre-registered cross-index NN meta-label - FAIL
(AUC 0.564, CI [0.498, 0.630]); DipMetaCut stays off
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 21:14:53 -04:00
---
## 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_<SYMBOL>.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_<magic>` (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.