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>
15 KiB
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
- 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.
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.- 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.
- 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
- Forward-test on demo first. Everything above is the tester. Live fills, slippage and the broker's real session behaviour are the untested layer.
- 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.
- 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.
Server-time assumptionsResolved 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'sCH_MARKET_CLOSE. No hour to recompute on another broker. The default 170 reproduces the validated 21:00 run to the cent.- 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.
- 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). - Attach
Warrior\WarriorDipZto each. LeaveInpMagicidentical on all four — that is what lets them share one account-level open-risk cap and one kill switch. - Keep the defaults: risk 0.25 %, open-risk cap 0.75 %, Friday flat 170 min before the symbol's Friday close, kill 4.5 %.
- 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, deleteDipZ_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.
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-runresearch/nn_cross_index.pyonce another year of trades exists.