BLUEPRINT.md reviews the feature/label layers and designs fractional differencing, Garman-Klass / Yang-Zhang targets and a 48-72h expansion label; mql5_patches/ holds the MQL5 side (FFD safe past the 1024-bar series ceiling, vol estimators, the label + veto gate, NY-time swap window). premise_test.py measured the premise on real broker bars: the headline AUC 0.75 was a day-of-week / path-length artifact (a Friday-clipped path is shorter, so it touches K*ATR less). Honest residual 0.56-0.62, mostly within noise once overlap is deflated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
13 KiB
Volatility Meta-Label Pipeline — Architectural Review & Execution Blueprint
Reviewed tree: MQL5/Shared Projects/Warrior_EA (fleet terminal 10CE948A…), 51,445 lines.
Date: 2026-09-19.
0. BLOCKER — READ FIRST: the source tree is 17 days stale and the newer work has no source
The working copy at Documents/Workspaces/Warrior_EA is empty. The only Warrior
source on this machine is MQL5/Shared Projects/Warrior_EA, and every file in it is
frozen at 2026-09-02 15:33.
The deployed binary the tester actually runs — MQL5/Experts/Warrior/Warrior_EA.ex5 —
is dated 2026-09-13 09:13. The source that produced it is gone.
Confirmed absent from disk, machine-wide:
| Module | Campaign it belongs to |
|---|---|
Signals/SignalDipBuy.mqh |
the dip-buy edge (the only surviving edge) |
System/DipMeta.mqh |
the ALGLIB meta-label |
Expert/WarriorExpert.mqh |
the session-aware Refresh() fix |
WarriorJournal.mqh |
the journal + its lookback-leak fix |
Corroborating evidence that this tree predates the Sep-11 work: System/TradeChecks.mqh
contains the TC* helpers but no TCCanOpen() — the single entry gate added on
Sep 11. A machine-wide search found no archive, zip, or backup. The .ex5 is compressed,
so no symbol names can be recovered from it (a control search for Warrior in the binary
also returned nothing, so that is inconclusive rather than proof of absence).
Consequence for this plan. Sections 1–3 below are correct against the Sep-2 architecture and every line reference is real. But the dip-buy signal, the meta-label scaffolding, and the session-aware refresh — the three things this pipeline would attach to — are not in the tree I can read. Recover or reconstruct the Sep-13 source before executing Phase 3. Phases 1 and 2 are safe to start now; they touch files that do exist.
1. Architectural review — where the premise and the code differ
Three corrections, because they change what the work actually is.
1a. There is no min-max layer, and no sigmoid time encoding
Expert/Features/FeatureBuilder.mqh (1,357 lines) is the only feature path. What it
actually emits:
| Feature group | Transform | Line |
|---|---|---|
| bar geometry | (close-open)/atr, (high-open)/atr, (low-open)/atr |
997–999 |
| trend position | donchPos20/50 — rank in range, bounded [-1,1] |
1053–1054 |
| displacement | (close - close[20])/atr, clamped ±10 |
1059 |
| mean extension | (close - SMA20)/atr, clamped ±10 |
1063 |
| leg state | dir*(close - legStart)/atr |
1086 |
| volume | vol/volBase, absorption, vol×range — all ratios |
1119–1129 |
| time | cyclical sin/cos on hour, day-of-week, month |
1134–1147 |
| ATR | atr/close, not raw ATR |
1148+ |
There is no MinMax, no Normalize, and no sigmoid anywhere in the tree (grepped).
Time is already cyclically encoded, which is the correct choice — a sigmoid on hour would
break the 23:00→00:00 wrap.
1b. The real defect is the opposite of the stated one: these inputs are OVER-differenced
Every column above is an integer-order difference, d = 1, ATR-scaled. That is
stationary — and it is memory-less. Measured on a synthetic log-price random walk
(research/fracdiff.py, validated below):
d taps obs adf_t corr-to-level
0.0 1 6000 -4.362 1.0000 <- raw level: all memory, fails ADF in general
0.3 2275 3726 -6.947 -0.0106
0.5 927 5074 -17.799 0.0020
1.0 2 5999 -53.934 -0.0004 <- WHAT THE EA FEEDS TODAY: memory ~= 0
So the network is handed a stationary series with the price level scrubbed out of it. The
one partial exception is smaExtension (close − SMA20), which is a crude low-order
memory term — and notably it is one of the four columns the fleet keep-screen voted 24/24
to retain (comment at FeatureBuilder.mqh:1018). That is weak corroboration that retained
level information is what the model was missing.
FDF is therefore the right patch — but framed as recovering memory, not as fixing non-stationarity.
1c. The majority-class trap is structural, not a tuning artifact
Expert/AIBase/Inference.mqh:147-153:
ENUM_SIGNAL CExpertSignalAIBase::Argmax3(double pBuy, double pSell, double pNeutral)
{
if(pBuy > pSell && pBuy > pNeutral) return Buy;
if(pSell > pBuy && pSell > pNeutral) return Sell;
return Neutral; // also the fallback on any tie
}
Neutral is both the modal class and the tiebreak. No loss weighting fixes a tiebreak. A 2-class expansion/chop head removes the bucket entirely.
Note the label is already a magnitude question, not a direction one —
LegRideLabel() (Labels.mqh:253) asks "does riding this leg pay ≥ LEG_LABEL_MIN_RIDE_ATR",
then signs the answer with the leg's own direction. The proposed target keeps the
magnitude question, puts it on a fixed clock, and drops the sign. This is a smaller
change than it appears.
2. Violations of the stationarity / time-horizon rules found in the tree
| # | Where | Finding | Severity |
|---|---|---|---|
| V1 | FeatureBuilder.mqh 997–1186 | Every price column is d=1 ATR-normalised → correlation to level ≈ 0. Memory destroyed. |
High |
| V2 | Inference.mqh 147 | Argmax3 ties resolve to Neutral — majority class is also the tiebreak. |
High |
| V3 | Labels.mqh 253 + LegState.mqh 60 | Label horizon is event-driven (leg flip), capped at LEG_LABEL_MAX_RIDE_BARS = 200. On H4 that is ~33 days. Nothing constrains it to 48–72h, and nothing constrains it to inside the week. |
High |
| V4 | ExpertCustom.mqh ~878 | Friday liquidation fires only if a tick lands inside a ±1 minute window (MathAbs(nowMinOfDay - targetMinOfDay) <= 1). A thin Friday close or a shut CFD session means no tick, no liquidation, position carried over the weekend. |
Critical |
| V5 | ExpertCustom.mqh ~888 | Schedule is evaluated in TimeCurrent() = broker server time. US and EU DST switch on different dates, so any NY-referenced instant drifts by an hour twice a year. |
Medium |
| V6 | tree-wide | No swap logic exists at all. DEAL_SWAP is read in the journal (TradeJournalManager.mqh:214) for P&L only. Nothing anywhere reads SYMBOL_SWAP_*. |
Medium |
| V7 | Variables/ConfidenceBridge.mqh 15–29 | Confidence is computed, published (PublishAIVote) and journalled, but gates nothing — the confidence-scaled management was removed 2026-08-25. There is a bus with no consumer. |
Info — this is the hook |
V4 is the one to fix regardless of whether the rest of this plan proceeds.
3. Execution blueprint
Phase 0 — Recover the source (blocking for Phase 3)
- Check the FLEET terminal's MetaEditor recent-files and any VCS/Dropbox history for the Sep-13 tree.
- If unrecoverable, reconstruct
SignalDipBuy.mqhfrom the recorded spec: D1, long-only,DIP_ZSCOREentry, inputsDipEntry/DipZ/DipExitMA/DipTrendMA/DipMaxBars, run alone. - Re-establish a source-of-truth location that is not only inside the terminal directory.
Phase 1 — Fractional differentiation (research/fracdiff.py, mql5_patches/FracDiff.mqh)
-
Pick
dper symbol on real data, not on one series. Runmin_ffd()over each fleet instrument and over each era separately. Take the smallestdthat passes ADF in every era, not the pooled minimum — this codebase has already been burned by pattern quality inverting across eras. -
Respect the tap budget. Measured widths:
d tau=1e-5 tau=1e-4 tau=1e-3 0.1 4076 503 62 0.3 2275 388 66 0.5 927 200 44 The stdlib series wrappers return 0.0 in silence past shift 1023. At
tau=1e-5, everyd ≤ 0.45exceeds that ceiling and would multiply real weights by silent zeros.FracDiff.mqhtherefore callsCopyClose/CopyTickVolumedirectly and never touchesm_Close.GetData(). Defaulttau = 1e-4keeps alld ≥ 0.1under ~500 taps. -
Add FDF columns alongside the existing ones first, do not replace. Extend the
names[]/widths[]table at FeatureBuilder.mqh:441 withffd_close,ffd_volume. Let the existing fleet keep-screen vote on them, exactly as it voted the ZigZag geometry columns out. Replaced=1columns only for those the screen actually drops. -
Raise
HistoryBarswarm-up byCFracDiff::WarmupBars()— the firsttapsbars have no valid output and must not be emitted as zeros.
Phase 2 — The volatility target (research/vol_target.py, mql5_patches/VolEstimators.mqh, VolMetaLabel.mqh)
- Run both targets in parallel and compare, per the brief:
- (a) Garman-Klass as a continuous regression target,
- (b) the binary 48–72h expansion label. Score both with walk-forward refits. Require the refit series, never a first fit — a first-fit AUC of 0.68–0.77 on ~4 months has already been proven meaningless here.
- Use Yang-Zhang, not GK, for anything that gates a multi-day hold. GK assumes the bar opened where the last one closed; index CFDs gap across the daily maintenance break and the weekend, so GK understates the variance the position is actually exposed to. GK stays as the efficient intra-bar estimator and as a feature. Both are implemented.
- Wire
PathBars()into the existing overlap machinery (CLabelOverlap,EffectiveSampleSize(),PurgeBars()in Labels.mqh). A 72h label on H4 spans 18 bars; a raw N overstates significance by ≈ √18 ≈ 4.2×. - Budget for the clip. Measured on synthetic H4 with a Friday 16:00 NY flat:
43% of triggers are dropped for not fitting a 48h horizon inside the week
(78 of 85 drops). These are dropped as
VOL_UNRESOLVED, never labelled chop — labelling them 0 would manufacture a class that correlates with day-of-week, which the network already reads through its sin/cos features. It would learn the calendar. - Change the head from 3-class softmax to 2-class;
Argmax3and its Neutral tiebreak go away.
Phase 3 — The meta-label gate (mql5_patches/VolMetaLabel.mqh, SwapWindow.mqh)
- Do not add the gate as a voting signal module.
CExpertSignalhas no per-side veto, and returningEMPTY_VALUEsilences the entire ensemble rather than one side. A meta-label is a veto, so it belongs on the entry path as one gate — the same shape asTCCanOpen(). Hook:CExpertCustom::Processing()immediately before theCheckOpenLong()/CheckOpenShort()call (ExpertCustom.mqh:528). CVolMetaGate::Allowed()blocks whenP(expansion) < 0.70. An unavailable reading also blocks — a filter that degrades to "allow" stops filtering while the log still says it is on.- Fix V4 with
CWeeklyFlatLatch: fire on the first tick at-or-after the deadline and stay armed until actually flat, instead of a ±1-minute tick lottery. Call it fromOnTickandOnTimerso a dead tick stream delays the close rather than cancelling it. - Fix V5 with
SWNewYorkTime(): derives NY wall-clock fromTimeGMT()+ US DST rules. Verified against an independent reference implementation across 78,888 hourly instants, 2018–2026: 0 mismatches. - Do not hardcode Wednesday for the triple swap. It is per-symbol and the broker
publishes it as
SYMBOL_SWAP_ROLLOVER3DAYS; several index and metal CFDs bill on Friday.SWTripleSwapDay()reads it. SWTripleSwapCost()prices the carry perSYMBOL_SWAP_MODEand returns false on any mode it cannot price exactly — which blocks the override rather than treating drag as zero.
4. What was validated, and how
| Claim | Method | Result |
|---|---|---|
| FFD weights / recurrence | d=1 must reduce to [1,-1] |
✓ exactly |
| ADF estimator | 600-rep Monte Carlo of the null | 5th pct −2.848 vs textbook −2.86; rejection 4.67% ≈ 5% |
| Garman-Klass accuracy | 4,000 bars × 500 intraday steps, known σ | recovered −4.2% (discrete-sampling bias, caveat 3) |
| GK efficiency | variance of estimator vs close-to-close | 9.97× (theory ~7.4×) |
| GK non-negativity | algebra + 200k adversarial degenerate bars | non-negative; floor matches 0.1137·ln(C/O)² to 1e-15 |
| Expansion label horizon | synthetic H4, 3 weeks | all spans within [48,72]h; 43% clipped |
| NY DST offset | independent brute-force reference, 2018–2026 | 0 / 78,888 mismatches |
Two of my own drafting errors were caught by these checks and corrected in place: an
initial test series went negative before a log() (producing NaN-spliced false
stationarity), and my first draft claimed single-bar GK could be negative — it cannot,
and the real hazard is that a corrupt bar yields a plausible positive variance instead.
That corrected caveat is now in both the Python and MQL5 headers.
5. Files delivered
research/fracdiff.py FFD weights, fixed-width transform, ADF scan, d-selection
research/vol_target.py GK / Parkinson / Rogers-Satchell / Yang-Zhang + expansion label
mql5_patches/FracDiff.mqh CFracDiff — 1024-ceiling-safe, log-price, series-order
mql5_patches/VolEstimators.mqh GK/Parkinson/YZ with explicit EMPTY_VALUE and H<L guards
mql5_patches/VolMetaLabel.mqh CVolExpansionLabel + CVolMetaGate (the veto)
mql5_patches/SwapWindow.mqh NY time, triple-swap day/cost, CWeeklyFlatLatch
None of these have been compiled — no MQL5 toolchain run was performed, and none of them have been copied into the live tree.