Warrior_EA/STRATEGY.md
AnimateDread 75d7362161 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

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

  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.


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.