forked from animatedread/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>
90 lines
5.5 KiB
Markdown
90 lines
5.5 KiB
Markdown
# NN meta-label on the vol-gated dip-z: cross-index / market-state features (pre-registration)
|
|
|
|
Written 2026-09-23, **before any model was fitted or any result looked at**. The only
|
|
thing inspected before this was the trade count per year of the gated strategy
|
|
(2021 91, 2022 238, 2023 43, 2024 60, 2025 119, 2026 102; win rate 62.6 %), which was
|
|
needed to size the walk-forward blocks. Code: `research/nn_cross_index.py`.
|
|
|
|
## Universe and label
|
|
- Trades: the production rule exactly as `vol_filter_test.collect_filtered(..., "vol pct >= 0.50")`
|
|
produces them (SP500, NAS100, US30, DAX40; H4; z20 <= -1.5; GK sigma(30) expanding
|
|
percentile >= 0.50; exit close >= SMA20 or 10 bars; stop 3 x ATR14; next-open fill;
|
|
spread from the bar file). Generator `backtest.simulate` reused unchanged.
|
|
- Label (classifier): net R-multiple > 0. Secondary label (regressor, sizing only): R.
|
|
- Primary training universe = the gated trades. Secondary (reported, NOT part of the
|
|
pass bar): train on the UNGATED dip-z trades (1,243, twice the data), score the gated ones.
|
|
|
|
## Timing / leakage control
|
|
All four H4 files sit on the same 00/04/08/12/16/20 server-time grid. For a signal bar of
|
|
symbol s opening at t (closing t+4h), another index's features are taken from its last bar
|
|
with open time <= t, which closes at or before t+4h — i.e. already closed when the signal
|
|
bar closes. When DAX is shut (US evening) this is its last closed bar (stale, causal).
|
|
Correlation/dispersion use closes forward-filled onto the union grid with the same rule.
|
|
|
|
## Feature sets (all at the signal bar i = entry_i - 1)
|
|
Per index k (fixed order SP500, NAS100, US30, DAX40): `z20_k`, `vp_k` (GK sigma30 expanding
|
|
pctile), `r5_k` = (c-c[-5])/ATR14, `r30_k` = (c-c[-30])/ATR14.
|
|
|
|
**CROSS (primary)** = the 16 per-index columns above + `breadth15` (# indices with z20<=-1.5)
|
|
+ `breadth10` (# with z20<=-1.0) + own `dd120` ((120-bar high - close)/ATR14) + own
|
|
`slope200` ((SMA200 - SMA200[-20])/ATR14) + `corr60` (mean pairwise correlation of the 4
|
|
indices' H4 log returns over the last 60 union-grid bars) + `disp20` (cross-sectional
|
|
std of the 4 indices' 20-bar log returns) + index one-hot (4). = 26 columns.
|
|
|
|
**BAR** (the old in-terminal set, baseline): z20, (c-SMA200)/ATR, ret1/ATR, ret5/ATR, down
|
|
streak, position in 20-bar range, range/ATR, close-in-bar, gap/ATR, ATR14/ATR100,
|
|
efficiency ratio(10), variance ratio (4-bar vs 1-bar over 60), regime (c>SMA200), dow,
|
|
hour + one-hot.
|
|
|
|
**BOTH** = CROSS ∪ BAR (measures the increment).
|
|
|
|
## Models (hyper-parameters fixed now, not tuned)
|
|
- **MLP (primary)**: StandardScaler -> MLPClassifier(hidden=(16,), alpha=1.0, solver=lbfgs,
|
|
max_iter=2000), mean of 5 seeds. Median imputation from the training block.
|
|
- RF: 500 trees, min_samples_leaf=15, max_features=sqrt.
|
|
- GB: GradientBoosting(150 trees, depth 2, lr 0.05, subsample 0.7).
|
|
- LR: L2 logistic, C=0.1 (linear reference).
|
|
- Regressor for the R-sizing secondary: MLPRegressor, same architecture.
|
|
|
|
## Walk-forward
|
|
Expanding window, **yearly refit** (6-monthly would give blocks of 20-60 trades).
|
|
Validation blocks: 2023, 2024, 2025, 2026-01..08. Training = every trade whose signal
|
|
bar is before the block start AND whose exit bar is >= 12 bars (of its own symbol) before
|
|
the block start (purge + embargo 12 bars). Pooled model with index one-hot.
|
|
|
|
## Statistics
|
|
- Per block: AUC and validation n. Mean AUC over blocks with n >= 100 (expected: only
|
|
2025 and 2026 qualify — this is stated in advance).
|
|
- **Pooled OOS AUC** over all 2023-26 validation rows, 95 % bootstrap CI (2,000 resamples)
|
|
and a label-permutation p-value (1,000).
|
|
- Sanity: (1) labels shuffled inside every training block -> AUC must be ~0.5;
|
|
(2) canary: CROSS + `canary` = first-bar return of the trade (c - o of the fill bar)/ATR,
|
|
i.e. future data -> AUC must be clearly high, proving the harness sees signal.
|
|
- Permutation importance of the primary model: drop in pooled OOS AUC when a column is
|
|
shuffled within its block (20 repeats).
|
|
|
|
## Portfolio (OOS 2023-01-01 .. 2026-08-31, halves split at 2024-11-01)
|
|
Rebuilt from the per-bar P (every gated signal bar gets a P from its block's model), so a
|
|
skipped signal lets the symbol take a later signal exactly as the EA would: entries =
|
|
gated & (P >= cut) -> `backtest.simulate`. Risk 0.25 %/trade, open-risk cap 0.75 %
|
|
(sum of open stop-risk). **Friday flat and swap are NOT modelled** (the Python tooling does
|
|
not; it affects all variants alike).
|
|
- (a) baseline gated.
|
|
- (b) filter P >= cut, cuts 0.45 / **0.50 (primary)** / 0.55.
|
|
- (c) sizer: multiplier 0.5 / 1.0 / 1.5 by tercile of P, tercile edges from
|
|
time-ordered 3-fold out-of-fold predictions on the training block (causal). ret/DD is
|
|
~scale-free, so average-risk drift does not bias the comparison; realized mean
|
|
multiplier is reported.
|
|
- Secondary: sizer from the MLP regressor's predicted R terciles.
|
|
Per half: trades, trades/month, return (compounded at exit), maxDD exit-based AND
|
|
mark-to-market (open positions marked at every H4 close, additive), ret/DD = return /
|
|
MTM maxDD.
|
|
|
|
## PASS BAR (primary = MLP on CROSS, gated universe)
|
|
AUC criterion: pooled OOS AUC >= 0.55 with bootstrap 95 % CI lower bound > 0.50, AND mean
|
|
per-block AUC over blocks with n >= 100 >= 0.55.
|
|
- **Filter PASS** = AUC criterion AND cut-0.50 filter ret/DD (MTM) > baseline in BOTH
|
|
halves AND >= 2 trades/month in both halves.
|
|
- **Sizer PASS** = AUC criterion AND tercile sizer ret/DD (MTM) > baseline in BOTH halves.
|
|
Anything else is FAIL. Other models / cuts / the ungated-trained variant are reported for
|
|
context and cannot turn a FAIL into a PASS.
|