Series indices are relative to now, so one new bar moves every cached bar's
index by one. EnsureBarCachesCapacity answered that by wiping the label cache,
the excursion caches, the ladder and the feature cache and rebuilding the whole
prebuild from scratch - on any timeframe where a bar closes before a run
finishes, the labels were being recomputed continuously and the training set
never held still.
The labels do not change when a candle closes. ShiftBarCaches moves every
per-bar cache up by the number of new bars, marks only those newest bars as
unfilled, and leaves the rest exactly as computed. CFirstPassageLadder gets a
matching Shift (resizing directly rather than through Allocate, which zeroes the
ages this is preserving).
Refuses, falling back to the full rebuild, when a prebuild is mid-flight: its
cursor is an index into the array being moved.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
TARGET_SWING: the direction models learn which way the next CONFIRMED SWING
PIVOT lies from the current close. Geometry-free - the label owes nothing to a
stop, target or horizon - which is what lets trade management be tuned
separately instead of being baked into what the net learns.
SwingPivotDirectionLabel reuses the ZigZag pivot the horizon and leg-size
measurement already walk, so there is ONE notion of "pivot" in the codebase. It
walks forward in time and stops at m_swingConfirmationBars: a pivot nearer than
that is still repainting, so its label is not knowable yet and the bar stays
Neutral. That boundary is the whole lookahead control for this target.
TrainingTarget input is back (TARGET_BARRIER default, unchanged behaviour) with
TARGET_FRACTAL and TARGET_SWING beside it; |TGT:SWG1 joins the fingerprint so
switching trains a separate model rather than relabelling an existing one.
ADZigZag was renamed to ZigZag throughout (30 identifiers). It has loaded
MetaTrader's stock Examples\ZigZag at its stock defaults for some time - the
migration was done, only the name was left behind, and a name that says "AD"
about a stock indicator is exactly the legacy pointer this codebase should not
carry. No behaviour change: same #resource, same params.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
First 35 eras across both charts, this run:
shipped 1.21/2.43 (SP500) and 1.26/2.52 (USDJPY): mean -0.0525R,
positive in 6 of 35 eras
best plateau after the neighbourhood guard: mean +0.0292R,
positive in only 17 of 35
most-recommended pair: 20.00/0.50, seven times - a ~40:1 lottery that is
simply the least negative cell in an all-negative grid
The recommendation jumps between opposite corners of the ladder between
consecutive eras, which is a grid fitting noise rather than a geometry worth
adopting. Two changes so the line cannot be misread:
- GEOSWEEP_MAX_TIMEOUT_SHARE (0.70): a cell where most trades never touch
EITHER barrier is not a geometry being tested, it is the horizon close being
measured. 20.00/20.00 timed out on 100% of trades and was still selected.
Excluded from SELECTION only; the cell stays filled and readable.
- When the winning plateau is <= 0 the line now says so in those words:
"NOTHING ON THE LADDER PAYS ... the pair below is the LEAST NEGATIVE cell,
not an edge."
Still measurement only - nothing reads the recommendation and no geometry moves.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Step 1 of decoupling SL/TP from training. The geometry is currently chosen
BEFORE the model exists - excursions -> stop at a quantile -> target at the
policy minimum ratio -> labels -> the net learns those labels - so it has never
been asked which pair maximises expectancy GIVEN WHAT THE MODEL CAN PREDICT.
The scan meant to answer that reports "0 ELIGIBLE candidates" on this config
(every rung disqualified by the close-all clamp), so nothing has ever compared
the shipped pair to an alternative.
This needs no retrain and no backtest. CFirstPassageLadder already stores the
first-touch AGE of every rung on both sides and OutcomeR() resolves ANY pair
exactly with the spread charged the way the fill charges it - so 14x14 pairs
over one era's OOS calls is a few thousand array reads.
- Expert/Training/GeometrySweep.mqh: CGeometrySweep accumulates (n, sumR,
sumR^2, timeouts) per rung pair from the model's own directional OOS calls.
Reads no chart, holds no net, opens no file - exercisable against a
hand-built ladder, same doctrine as SDeployVerdict.
- Best() ranks on the 3x3 NEIGHBOURHOOD mean, not the cell itself. A 14x14 grid
read at its single highest cell is a best-of-196 maximum, biased upward by
construction - the same selection problem the deploy gate corrects across
eras. A pair whose neighbours also pay is a plateau; a lone spike is a lucky
run of trades and does not survive the next window. GEOSWEEP_MIN_TRADES (30)
keeps thin cells out of the selection entirely.
- Wired into pass 3 where the call and the bar index are both in hand, reset per
era, reported at pass-3 completion beside ReportCandidateGeometry. ONE line,
and only when the recommendation CHANGES - it prints the shipped pair's
expectancy and the best pair's on the SAME trades, so "better" is a difference
rather than two numbers from two populations.
Measurement only: nothing reads the recommendation yet and no geometry moves.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
selectionScore used to be a win rate in percentage points and printed at one
decimal everywhere. Under DeployOnExpectancy it is expected value in R, so
"%.1f" rendered every real score as "0.0" - era 2's +0.05R and a genuine zero
looked identical, which makes the journal useless for watching the ranking the
plateau ladder is doing.
One formatter, DeployScoreText(), next to the score it formats: "%.3fR" under
expectancy, "%.1f%%" under significance. Routed all nine print sites through it
(ensemble era line, best-so-far, panel, regression, new-best, era-cap prompts,
the convergence line, the deploy dialog) and dropped the "%" suffixes they had
hardcoded. No new prints, no new log lines.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
TWO CHANGES, both of which turn a permanent "nothing happens" into a decision.
1. THE DEPLOY GATE ASKS THE WRONG QUESTION. tradeable required the win rate to
clear chance by EDGE_MIN_SIGMAS - "can I PROVE an edge exists" from one OOS
window. On H4 that asks ~66% against a market supplying ~53%, so it is
unreachable by construction and no run has ever deployed through it.
SDeployVerdict now also carries the economics of the geometry actually being
traded - cost-adjusted break-even and reward:risk, both from the new
CostAdjustedGeometry() so a spread convention cannot be applied to one and
missed on the other - and derives
E[R] = (p - p*) * (1 + RR)
which is exactly zero at break-even by construction, so "profitable" and
"beats break-even" can never disagree. Under DeployOnExpectancy (new input,
default ON) tradeable becomes E[R] > 0 and selectionScore ranks eras by
expectancy instead of precision. Coverage and both-sides-live still gate
both: an expectancy over a handful of one-sided calls is not tradeable.
The struct also publishes scoreSE - the SE of selectionScore IN THE SCORE'S
OWN UNITS - because the score changes units with the objective (win-rate
points vs R). Both plateau bands now read it instead of precSE, which was
right for one objective and dimensionally wrong for the other.
Setting DeployOnExpectancy=false restores the previous behaviour exactly.
2. THE FILTERED VIEW COULD NOT DRAW WHILE ANY MODEL WAS TRAINING.
HistoricalNetVote built its divisor from VoteCapableWeight(), which answers
"may this member move real money" and returns 0.0 for an AI member until the
whole run converges. So the reconstruction's divisor was zero on EVERY bar,
every bar was skipped as "nobody looked", and the chart drew nothing at all -
for the entire training run, which before the plateau noise band was forever.
Reported as "no signals drawn since the refactor".
New ReconstructionWeight(): the same weight WITHOUT the converged-run
requirement, overridden on the AI member to ModuleWeight() gated on
SelfRanked() only. The overlay is a picture of what the vote WOULD have
shown, which a mid-training model can answer - the chart HUD already says so
with its "(trn)" marker. Live Direction() still uses VoteCapableWeight(), so
no untrained model gains a say in an order.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
TWO INDEPENDENT BLOCKERS, both of which make the EA look like it is working.
1. THE LADDER NEVER ADVANCES. isBetter/isBetterEra compared selectionScore with
a bare `>`. selectionScore is a win rate over a few hundred independent
calls, so it moves several points era to era on noise alone - measured on
SP500 H4 today: 32.8 / 32.2 / 31.6 / 29.6 / 31.4 across consecutive eras, a
~3-point spread with no trend. Any upward blip was recorded as a new best,
which reset BOTH the plateau counter and the stage, which re-armed a x5
learning-rate warm restart, which injected fresh noise and produced the next
blip. The search sustained itself on its own variance and never reached
PLATEAU_STAGE_DEPLOY - the reported "thousands of eras without converging".
A new best now has to clear the incumbent by PLATEAU_NEW_BEST_SIGMAS (2.0)
times precSE, which the deploy gate already computes. 2.0 rather than 1.0
because incumbent and challenger are both noisy, so the SE of the difference
is ~sqrt(2) x SE, and a 1-SE band was already measured too narrow in a
noise-dominated search. Applied at BOTH ranking sites - the ensemble's and
the solo member's - which are documented as the same ordering. The first
scoring era still checkpoints unconditionally.
2. THE BLANK-CHART CENSUS WAS LYING. It printed "No member has a completed era
yet (snapshots fill at each member's first pass-3 completion)" while the
members were on era 23, because it inferred the cause from m_overlayVotedBars
alone - and that counter requires BOTH a non-zero divisor AND a non-zero net.
Three different states collapsed into one sentence. Split out
m_overlayHadDataBars (divisor non-zero) so the line names which it is:
hadData == 0 -> nobody published a snapshot: publication/index
hadData > 0, voted == 0 -> members looked and abstained: calibration
voted > 0, drawn == 0 -> the vote never cleared the threshold
Diagnostic only. It does not fix the missing arrows - it identifies which of
the three is happening, which the current line actively obscures.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every era was a ~1,200-bar chunk of a 16,264-bar window, and the oldest 90% of
the history was never reached.
All four passes yield mid-chunk on the 120ms budget: each one calls
StashEraResume (the single writer of m_eraResumePending) and returns. Those
used to be returns from Train() itself. When the passes were extracted into
their own methods (08c2cec) they became returns from a void helper, and Train()
carried straight on - reporting pass 1 "done" after one budget, running pass 2
over the sliver pass 1 had queued so far, scoring an OOS slice of it, and
letting AdvanceEra count an era. The extraction moved one side of the binding
and left the reader behind.
Measured on SP500 H4 (VerboseMode, 2026-08-24 15:05-15:14):
era 0 TRAINING WINDOW = 16264 bars ... Bars(series) = 16264 <- window fine
era 1277 pass 1 done in 0s - 1144 of 1193 bars usable <- sweep is not
era 1296 pass 1 done in 0s - 3117 of 3166 bars usable
era 1318 pass 1 done in 0s - 1391 of 1440 bars usable
~1,400 eras in ten minutes, the count varying with how many bars a 120ms budget
happened to buy. Downstream: each member held a different tiny OOS slice, so
the combined vote's shared-bar intersection collapsed ("0 shared OOS bars" on
nearly every era, score 0.0), and the plateau ladder counted 46 ungraded eras
as a plateau and fired a boosted warm restart on all four models.
Train() now returns whenever m_eraResumePending is set - after pass 1 (before
ReportPass1Outcome, which has no verdict to give on a yielded sweep), pass 2,
the calibration walk and pass 3. m_modelEta is already saved inside
StashEraResume, so the early returns keep the learning-rate trajectory.
The resume machinery itself was correct and is unchanged: BeginEra's resume arm
restores the cursor, m_passWindowOk/m_passWindowFail accumulate across chunks,
and the m_isPass2Active/m_isPass2Done guard already routes a resumed call to
the right pass.
Expect era numbers to advance slowly now. That is the fix, not a new stall.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With VerboseMode on, pass 1 reported eras of 422 / 949 / 1358 / 2562 bars on
SP500 H4 - four models, same chart, same second - against a series holding
~16,264 bars, and the number moved every era (CONV: 2562, 3671, 3405, 3532,
2830). Nothing in the journal said so. ReportDetectability and the CAPACITY
line both quote EstimatedInSampleBars, which is derived from the configuration
and not from the era, so they kept reporting "11385 in-sample rows / OOS window
4874 bars" for a window that was a tenth of that.
era.bars is MathMin(Bars(symbol, PERIOD_CURRENT, dtStudied, now) + historyBars,
Bars(symbol, PERIOD_CURRENT)). A short era is therefore either a dtStudied that
is too recent or a short price series, and those need opposite fixes - so the
new line carries all three quantities plus the resolved dtStudied and
SERIES_FIRSTDATE, not just the result.
Reported on change only: an era over a warm feature cache runs in a fraction of
a second here, and a per-era line would bury the journal.
Diagnostic only - no training behaviour is changed by this commit.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two charts (SP500 H4 + USDJPY H4) ran with the pool enabled and produced no
TrainPool directory, no adopted rows and not one journal line. The pool was
inert and there was no way to tell that from "the feature is off".
It could never have fired: the fingerprint is not symbol-invariant. It hashes
NeuronsCount, which counts the alt-data columns - and those are per-symbol
(SP500 carries cot_spec_net, the FX majors cot_idx_1y/3y/chg_4w) - and the
cross-asset block appends ":IDX2" when base currency == profit currency, true
of an index and false of a pair. SP500 came out 50 features wide under
XA:6:IDX2, USDJPY 52 wide under XA:6. Compatible() gates on both, so adoption
was zero by construction.
- STrainPoolHeader::MismatchReason() replaces the bare Compatible() predicate
and names the mismatch; Compatible() now delegates to it, so "may I adopt"
and "why not" can never drift apart.
- CTrainPoolReader::Adopt() reports its own verdict - adopted, alone, or every
peer rejected with the reason per file - and reports it on CHANGE only. An
era over a warm feature cache runs in a fraction of a second here, so a
per-era line would bury the journal. The duplicate Print in RunPass2 is gone;
pool state is now reported from exactly one place.
- CTrainPoolWriter::Publish() rate-limits to TRAINPOOL_MIN_PUBLISH_SEC (300s).
Every era re-derives the same rows from the same in-sample span, so per-era
publishing rewrote a multi-megabyte file continuously for no new information.
The first publish is never delayed.
Compile-verified in the staging copy: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Peer rows join m_isTrainQueue as NEGATIVE sentinels before the shuffle, so they interleave with
this chart's samples instead of training in a block at one end. A block would be a curriculum:
whatever the optimizer saw last would decide where it landed.
TrainPoolStep is a separate path on purpose. Everything in pass 2's local branch after the
forward pass reaches for something indexed by a LOCAL bar - m_labelCache, m_winLongCache, the
excursion target, the arrow cache, m_Time - and a peer row has none of those. Sharing the path
would mean inventing values for all of them, which is how another instrument's outcomes end up
inside m_cumIsCorrect and the operating point gets fitted to them. The IS-vs-OOS gap is read as
THE overfitting signal, so polluting the IS side would not crash anything; it would just quietly
stop meaning what it says.
The purge key reuses the label walk's own two bounds - the horizon and NextScheduledCloseAll -
rather than approximating with a bar offset. A second horizon model here would drift from the
real one, and this project already measured that the close-all, not the nominal horizon, is what
actually terminates labels. Cutoff is the OLDEST OOS BAR'S TIME, in wall clock, because bar
indices cannot be compared across instruments that each have their own calendar.
Contribution happens while the window is still in TempData and before the forward pass
overwrites it, and is gated to direction models: the meta head trains a different target on a
wider input, which the fingerprint gate alone would NOT catch, since a meta model's fingerprint
matches its own peers perfectly well.
Use_Training_Pool ships false and does nothing until a second chart runs a matching fingerprint.
Compile-verified against a BASELINE of the same tree without the wiring: both produce 12
errors, all error 313 invalid-resource-path from #resource directives that cannot resolve in a
headless staged build (stock Controls res\*.bmp, plus the pre-existing Network.cl). Code errors
0, warnings 0, identical to baseline. Staging copy and junctions removed; the live .ex5 was
never touched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
RSI, MACD, Ichimoku and the five AD/Wyckoff indicators (CumulativeDelta,
ShorteningOfThrust, WyckoffEventStream, WyckoffFailedStructure,
WyckoffSignificantBarInversion). All eight inputs shipped false and each carries a
closed verdict: the three oscillators are the same patterns that measured at chance
as entries, and the Wyckoff family returned zero out-of-sample on five independent
instruments - which is what closed the context score.
RETRAIN-NEUTRAL, and this one is worth stating precisely because the change looks
larger than it is. Every removed group contributed `flag ? N : 0` to the input
width, and every flag was false, so the width was ALREADY zero for all eight: no
.nnw's input layer changes. On the fingerprints, UseRSI and the five AD flags were
hashed unconditionally and become literal 0 legacy slots (the convention the
m_focalGamma slot above them already uses); UseMACD/UseIchimoku were appended only
when enabled, so their segments simply never appear - byte-identical to every
fingerprint ever produced, since neither ever shipped on.
CADIndicatorTuner IS DELIBERATELY NOT SHRUNK. Its flat parameter array is persisted
inside every .nnw, and Unflatten() rejects a size mismatch by falling back to
constructor defaults - so dropping the dead fields would silently revert the tuned
MA period of every model on disk while keeping its trained weights. That is the
feature/weight mismatch this project has already paid for twice, and it is not
worth 200 lines. AD_TUNE_PARAM_COUNT stays 42, the dead slots are still written and
read, and AutoTune's ParamOwner gate now matches only owner 5 (MA) so nothing
searches them. The class comment says all of this at the declaration.
Also renamed ReInitADIndicators -> ReInitTunableIndicators: it rebuilds exactly one
indicator now, and a name saying "AD" for the MA handle is the kind of stale label
that gets believed later. Its release-AFTER-recreate ordering is untouched - that
is a documented fix, not bookkeeping.
Compile-verified in the stage copy: 0 errors, 0 warnings, against the same 0/0
baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AcquireConfigLock/ReleaseConfigLock moved off CExpertSignalAIBase into
Expert/ConfigLock/CConfigLock, same view+adapter shape as BarrierHorizon/ExcursionHead.
Stateful: m_configLockName is exclusive (grep-verified, nothing outside Lifecycle.mqh's
old body touched it). Pure relocation - same FNV-1a hash, same owner-liveness check,
same log wording. Left uncommitted mid-campaign; independently compile-verified in
isolation now (0 errors/0 warnings) before this commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
FeatureScreen.mqh's MI/permutation-null diagnostics (mean/best-col
report, excursion report, lag-profile family-wise test, barrier-
geometry scan) and AutoTune.mqh's TuneIndicatorsByFilter install gate
each spelled out the add-one-smoothed Monte-Carlo p-value
(1+atLeast)/(draws+1) independently. Added PermutationPValue(atLeast,
draws) to System/BinomialStats.mqh (returns 1.0 for draws<=0, matching
every existing call site's own guard) and replaced all six inline
expressions with a call to it. Pure arithmetic substitution, no
control-flow change.
ApplyClassificationSoftmax/AdjustedSignalFromSoftmax/DirectionalMargin each
re-derived `pBuy > pSell && pBuy > pNeutral` (and the Sell mirror)
independently, one of them documenting the duplication by comment rather
than eliminating it. Added Argmax3() as the single derivation (ties to
Neutral); all three now branch on its ENUM_SIGNAL result instead of
re-testing the comparison. Pure relocation, statement-by-statement
equivalent - verified by diff.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Expert/AIBase/Labels.mqh (1807 lines) exclusivity-grepped almost entirely
SHARED: the label/win/excursion/ladder caches and the geometry-derivation/
prebuild state are touched with real per-bar array logic by Training.mqh's
hot era loop (m_labelCacheBuy/Sell/HasValue at 22+ sites), by FeatureScreen.mqh's
geometry scan (direct writes to m_barrierScanSlMult/TpMult, m_geometryAdopted,
m_geometryCfgSaved), by AutoTune.mqh and by SignalMETA.mqh - moving that state
into a collaborator would mean wrapping dense hot-loop array indexing behind
method calls across 5 files for no coupling reduction (same judgment already
recorded for AutoTune.mqh's remainder / Inference.mqh).
One genuinely closed sub-cluster survived the grep: the scheduled close-all
budget and the horizon ladder snap (NextScheduledCloseAll, MeasureCloseAllBudget,
EffectiveHorizonMax, RequiredHorizonBars, SnapHorizonToLadder, GrantedHorizonBars).
Only 2 fields are exclusive (m_closeAllCycleBars/m_closeAllMeanBudget - grep-
verified, Lifecycle.mqh's touch was constructor-init-list only) and NONE of the
6 methods has any external caller outside Labels.mqh (grep-verified whole-repo),
so nothing needed rewiring. New Expert/BarrierHorizon/: IBarrierHorizonView.mqh
(abstract, 4 accessors, 3 reused from the signal's existing Chart* getters, 1
new HorizonSwingMedianBars() wrapper) + AIBaseBarrierHorizonView.mqh/
AIBaseBarrierHorizonViewImpl.mqh (the adapter) + BarrierHorizon.mqh (CBarrierHorizon,
STATEFUL - owns the 2 exclusive fields as real members). Every method body is a
verbatim relocation (diffed programmatically against git HEAD modulo the field->
view substitutions - identical except one comment-wording update). The original
6 declarations on CExpertSignalAIBase became one-line forwards at their existing
position; Labels.mqh's own callers of these six needed zero changes since they
call them unqualified, which now resolves through the forwards.
Labels.mqh: 1807 -> 1643 lines. The rest of the file (label-cache population,
TripleBarrierLabel, DeriveBarrierGeometry, StartLabelCachePrebuild/
AdvanceLabelCachePrebuild, exit-policy simulation) is deliberately left as a
raw-include partial - not separable without relocating Training.mqh's era-loop
coupling, not reducing it.
Self-compiled 0 errors, 0 warnings (_claude_stage, ~94s).
Expert/AIBase/Features.mqh (2017 lines, 38 methods) split by exclusivity grep
(whole-repo, not just Expert/): 30 methods -> Expert/Features/FeatureBuilder.mqh
(CFeatureBuilder + CFeaturesView/CAIBaseFeaturesView), 8 stay behind as a much
smaller raw partial.
CFeatureBuilder is STATEFUL, same shape as Excursion/OnlineLearning: owns the
10 feature-only indicator handles (m_Volumes/m_MA/m_RSI/m_MACDFeature/
m_Ichimoku/5 AD* CiCustom indicators - grep-verified touched nowhere else in
the repo, only their bare declarations) plus the depth-probe/handle-repair/
spread-series/detectability-latch scalars (exclusive, Lifecycle.mqh ctor-init
only elsewhere). m_Open/m_Close/m_High/m_Low/m_Time/m_ATR/m_ADZigZag stay
signal-owned - Labels.mqh/AutoTune.mqh/Training.mqh read them directly - and
are reached read-only through the view (FeatureOpenAt/FeatureHighAt/
FeatureLowAt/ChartBarClose/ChartBarTime/OnlineAtrMain, all reused where a
forward already existed).
Deliberately did NOT move InitOpen/InitClose/InitHigh/InitLow/InitTime/
InitADZigZag/ResizeBuffers/RefreshData: they manage the 7 shared indicators'
Create/BufferResize/Refresh lifecycle, which would need a pure-relay wrapper
per operation per indicator for zero coupling benefit - same judgment as
Topology's boot sequence. They stay in Expert/AIBase/Features.mqh and reach
CFeatureBuilder's 10 owned indicators through 20 new Feature*BufferResize()/
Feature*Refresh() forwards (signal calling into its own owned collaborator
directly, no view needed in that direction).
Whole-repo grep (not just Expert/) caught a real external miss the campaign's
own doctrine warns about: Signals/SignalMETA.mqh read m_spreadSeries/
m_spreadSeriesBars directly as an inherited protected field (a subclass, not
an AIBase/*.mqh partial) - fixed with two new FeatureSpreadSeriesBars()/
FeatureSpreadSeriesAt() forwards.
Verified: if(/for(/while( counts identical between the original file and the
new split (269/20/1); return-count delta (+12) fully accounted for by the 12
new trivial one-line forwards added (10 indicator BufferResize + 2 spread-
series getters); quoted-string-literal diff empty except two doc-comment
paraphrases. Self-compiled 0 errors, 0 warnings.
Expert/AIBase/Topology.mqh (1191 lines) held two genuinely different jobs: the
fingerprint/derived-shape/BuildFreshTopology math, and InitNeuralNetwork/
InitFeatureIndicators - the network boot sequence (config-lock, tester-cache
seeding, load/save the .cfg, net-load backend fallback, chart/persistence/
online-learning orchestration).
Extracted the first job to Expert/Topology/ as CTopology + CTopologyView/
CAIBaseTopologyView (20 methods: BuildModelFingerprint, the Estimated*/Compute*
budget math, the Conv*/Lstm* shape helpers, Add*Stage, BuildFreshTopology).
STATELESS, like ModelPersistence - grep-verified zero exclusive fields, every
member these methods touch is shared elsewhere in the signal. Reused ~15
existing Data*/Chart*/Persist*/Exc* getters per the established convention;
added ~20 new getter overloads next to their existing setters (UseVolumes(),
MinDirectionalRecall(), etc. - same pattern as SignalClusterWindow) and ~16
new Topology*() wrappers for fields with no prior accessor. The Net-pointer
swap in BuildFreshTopology is one consolidated view call
(TopologyReplaceNetFromTopology), same doctrine as Persistence's
RunCpuInferenceSelfCheck - irreducible pointer work, not signal state.
Deliberately did NOT extract InitNeuralNetwork/InitFeatureIndicators: they
orchestrate nearly every other collaborator (chart, persistence, online-
learning, cross-asset, config-lock) rather than deriving a shape, so moving
them would just relocate a hub, not reduce coupling - same judgment call as
Inference.mqh (assessed, not extracted). They stay in the AIBase/Topology.mqh
partial, byte-identical to before (diffed against git HEAD to confirm), and
now call the extracted math through the same public forwards every other
caller already used.
Verified: string- and numeric-literal diff of the old file's 20 method bodies
against the new CTopology methods (0 differences), InitNeuralNetwork/
InitFeatureIndicators byte-identical, self-compiled 0 errors/0 warnings.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Expert\AIBase\OnlineLearning.mqh (595 lines) -> Expert\OnlineLearning\:
IOnlineLearningView.mqh (abstract, ~50 accessors) + AIBaseOnlineLearningView.mqh/
AIBaseOnlineLearningViewImpl.mqh (the adapter) + OnlineLearning.mqh (COnlineLearning).
STATEFUL, unlike CModelPersistence: grep-verified the shadow net, the OOS
continual-learning simulation state and the pattern-database backfill state are
genuinely exclusive to this file's own methods - Training.mqh/Topology.mqh/
Lifecycle.mqh/the signal's own header only ever CHECKED or RESET this state at
era/lifecycle boundaries, never owned it, so it moved onto the collaborator as
real members (same doctrine as Excursion). Those external touch points became
consolidated view/forward calls instead of raw field pokes - AbortSimIfActive()
replaces THREE separate copies of the same delete/null/false triple (Training.mqh's
stop path, FlushTrainRun, ResetWeights), matching the geometry-scan duplicate-reset
precedent in project memory. ResetForFreshTopology() replaces Topology.mqh's five-
field reset block, DeployNet() replaces the shadow-preferred net selection duplicated
in Inference.mqh and ChartScoreBarForRescan, and BlendTowardNet() replaces the era-end
blend Training.mqh used to poke m_shadowNet for directly.
Reused the signal's existing Data*()/Chart*()/Persist*() getters wherever one already
answered the question (labels/outcome/history/horizon/priors/servable-bars/etc.);
added ~30 new Online*() wrappers only for what nothing else exposed yet. The three
PersistOnline*() get/set pairs (WST3 .stats fields) now forward through the owning
member instead of touching the field directly - CModelPersistence is unaffected.
Every method body is a pure relocation of the original's statements in original
order; verified against `git show HEAD~1:Expert/AIBase/Excursion.mqh`-style diff
against the pre-extraction file kept in the working tree until this commit.
Compiled 0 errors, 0 warnings (stage mirror + MetaEditor64 /compile, ~91s).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Expert/AIBase/Excursion.mqh was 13 method bodies of CExpertSignalAIBase,
#include'd after its declaration - same "not a module" problem already
fixed for ChartUI (S2) and Persistence (S3). Extracted to
Expert/Excursion/CExcursionHead behind CExcursionHeadView/
CAIBaseExcursionHeadView, same view+adapter shape.
STATEFUL, unlike Persistence (0 exclusive fields): grep-verified 26
fields (m_excNet and every accumulator/trailing-ring field) touched
nowhere else in Expert\ except Lifecycle.mqh's old ctor-init-list
defaults and destructor deletes (now moved onto CExcursionHead's own
ctor/dtor). m_geo (SGeometryScan) and m_ladder (CFirstPassageLadder)
stay on the signal - both are genuinely shared with Labels.mqh/
Training.mqh at era boundaries - and are reached only through 15 new
Exc*() view wrappers, including one consolidated
ExcGeometryScanAccumulate() call (same doctrine as Persistence's
RunCpuInferenceSelfCheck) rather than field-by-field pokes.
All 13 original public methods stay at their same declaration point as
one-line forwards to m_excursionHead. Training.mqh's 2 raw m_excUs
reads now go through the new ExcursionMicroseconds() forward. Every
method body is a pure relocation, verified statement-by-statement
against the original (git show HEAD~1:Expert/AIBase/Excursion.mqh).
Compile-verified: 0 errors, 0 warnings.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Expert/AIBase/Persistence.mqh (598 lines, 8 methods) -> Expert/Persistence/:
IPersistenceView.mqh (abstract, 68 read+write accessors) + AIBasePersistenceView.mqh/
AIBasePersistenceViewImpl.mqh (the adapter) + ModelPersistence.mqh (CModelPersistence,
the real collaborator - signal owns m_modelPersistence and binds it to m_persistenceView,
same shape as ChartUI's S2).
Grep-verified before starting: every field these 8 methods touch is ALSO touched
elsewhere in the class (Training/Lifecycle/OnlineLearning/Topology/FeatureScreen/
Labels.mqh) or already exposed via ChartView. Zero exclusive state, unlike ChartUI's
arrow-restore/rescan queues - CModelPersistence is stateless, holding only the
borrowed view pointer, operating entirely through 68 Persist*/PersistSet*() accessors
on the signal.
ValidateCpuInference's Net-pointer/throwaway-clone core is ONE consolidated view call
(PersistRunCpuInferenceSelfCheck) rather than field-by-field - irreducible pointer/
object work, not signal state, same doctrine as ChartScoreBarForRescan.
LoadNetWithRetry keeps its original CheckPointer(Net)-free Net.Load() call unchanged
(no guard added - would change failure behaviour on what must be a pure relocation).
This code writes the actual on-disk .cfg/.stats binary layouts every deployed model
depends on (explicit "DO NOT REORDER" comment in the original), so beyond compiling
clean (0 errors, 0 warnings) this was verified with a positional field-order diff:
every FileWrite*/FileRead* call's target field, extracted and normalized from both
the original and the new file, matches 1:1 in the same order (43/43 on the write
side covering SaveModelStats+SaveTopologyConfiguration, 17/17 on LoadModelStats'
read side; LoadAndCompareTopologyConfiguration's local-variable read block was
copied verbatim, untouched, so nothing to diff there). The magic-version
conditionals (WST2-6, haveDerivedStages/haveBarrierGeometry/etc.) moved unchanged.
All 8 methods keep their exact original signatures as one-line forwards - zero
external call sites changed.
Neither ever touched a CExpertSignalAIBase member - both take everything as
parameters. Moved to System\SharedFileCopy.mqh (free functions, same doctrine as
TradeChecks.mqh's TC*), matching what they actually are instead of carrying them as
methods on a class they don't depend on. Topology.mqh's call sites are unchanged -
unqualified calls from within a class method resolve to the free function exactly
the same way. Compiled clean.
Correction to project_oop_module_pattern's Persistence(696) note: LoadNetWithRetry
is NOT a third pure utility alongside these two - it touches Net/dError/dUndefine/
dForecast/dtStudied/m_activeFileName/m_activeFileCommon/m_eraCount/m_trainingComplete.
The rest of Persistence.mqh (EnforceTopologyContract, Save/LoadModelStats,
ValidateCpuInference, Save/LoadAndCompareTopologyConfiguration, ReadAltDataPinFromCfg)
is heavily coupled to signal state - a real view+adapter extraction on the scale of
ChartUI's, not attempted here.
Expert/AIBase/ChartUI.mqh was 869 lines of method bodies of
CExpertSignalAIBase, #included after the class declaration - free to touch
any of its ~500 members. First of the eleven AIBase/*.mqh partials to come
out (fewest inbound edges - see the SOLID campaign session order), using
the same view+adapter shape already proven for CTrainingDataView.
CChartView (Expert/Chart/IChartView.mqh) is the abstract read/behaviour
surface a chart-rendering collaborator needs - identity, bar/model access,
the prediction cache, and the training/vote/meta scalars the panel and HUD
line summarise. CAIBaseChartView is the adapter the signal owns and binds
to itself (MQL5 gives a class exactly one base, so CExpertSignalAIBase
cannot implement the view directly). CChartUI is the real collaborator: it
owns the arrow-restore queue, the rescan queue/tally, the last-arrows-saved
count and the purge-mismatch latch as its own fields (verified via grep to
be touched nowhere else in Expert/), and reaches everything else - including
StartChartSignalRescan, moved in from its old inline home in the header
since it drives the exact same rescan state machine AdvanceChartSignalRescan
drains - through the view.
m_arrowSignalCache and m_signalClusterWindow stay on the signal: Training.mqh
writes the cache directly every era and the training-data view already reads
it, so moving it would mean rewriting Training.mqh's write sites too - out of
scope here. CChartUI reaches it through four bounds-checked accessors instead
of a raw member poke. All 10 public methods keep their exact signatures and
become one-line forwards on the signal, so no other file's call sites change
except Training.mqh's one era-end status refresh, which now reads
RefreshStatusLabel() rather than reaching into CChartUI's now-private
last-displayed-neuron cache directly.
Verified structurally, not compiled (never compile - the operator does, in
MetaEditor): brace balance checked on every touched/new file against HEAD,
and the view/adapter/impl method lists cross-diffed to confirm all 59
accessors match 1:1 across the interface, the adapter declaration and the
adapter body.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three backends left, as the operator specified: OpenCL, the CPU DLL,
and pure MQL5. CDirectMLMy was a two-tier wrapper (GPU via
WarriorDML.dll, CPU via WarriorCPU.dll) whose name only ever named the
tier being removed here; the CPU DLL tier - the one actually used on
the training machine (no OpenCL, no DirectML) - is untouched.
AI/NeuronDirectML.mqh -> AI/ComputeDll.mqh: dropped the DML_* #import
block and COMPUTE_TIER_GPU (checked first that nothing persists the
enum value and only one external site reads .Tier() - safe), collapsed
every tier==CPU?CPU_x():DML_x() ternary to a straight CPU_x() call.
Renamed CDirectMLMy->CComputeDll, InitDirectML()->InitComputeDll(),
member directml/DirectML->computeDll/ComputeDll across every AI/ file
that touched a neuron/net backend plus Topology.mqh/OnlineLearning.mqh.
NetBuild.mqh's InitComputeDll also lost the dead D3D12 error-code
switch and the now-impossible GPU-tier log branch.
Verified via per-file brace-balance diff against HEAD and a whole-repo
grep for every removed symbol (CDirectMLMy/InitDirectML/
COMPUTE_TIER_GPU/DML_*) - the only surviving hit is an intentional
historical-note comment in the new file's header.
DirectML\WarriorDML.cpp/.h and its build scripts are now orphaned C++
source, left in place pending an operator decision. Architecture docs
(AI_NETWORK.md, Warrior_EA_System_Overview.md, etc.) still describe the
4-backend/GPU-tier shape and are not updated in this pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Last surviving compile-time feature switch in the codebase - the same pattern
already killed for the MARKET build and DirectML tier (02766b5): one build,
configured at runtime like every other module (inputs + getters/setters, set
in ConfigureAISignal during OnInit), not a second code path that only existed
if someone remembered to define a macro before compiling.
Replaced with `input bool ExportFeaturesOnly = false` (Variables/Inputs.mqh)
and a plain m_exportFeaturesOnly member + setter, matching AutoTuneIndicators'
exact shape. Four call sites converted from #ifdef to a runtime read of the
same variable:
- Warrior_EA.mq5 OnTick() - reads the input directly (this check has to
stand before any per-signal object exists)
- Topology.mqh's config-lock skip and ExportFeatureMatrix() call - read
m_exportFeaturesOnly, now set by ConfigureAISignal before InitIndicators()
runs (same init-order guarantee AutoTuneIndicators already relies on)
- ExportFeatureMatrix()/ExportRawRates() declarations - always compiled now,
called conditionally instead of not existing as symbols
No change to what the flag does when off (the state of every build that
exists today, since the macro was never defined anywhere in-repo) or when on;
only how it's set. Verified: WARRIOR_EXPORT_FEATURES fully gone from every
#ifdef/#endif in the tree; brace and ifdef/endif counts balance in every
touched file; ConfigureAISignal runs before StepInitIndicators in OnInit's
linear init chain, so the flag reaches InitNeuralNetwork() in time.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Session C of the feature-selection/labeling refactor track. AutoTune.mqh was
two responsibilities in one 1,584-line file: SEARCH (TuneIndicatorsByFilter,
coordinate-descent over indicator settings) and MEASUREMENT (the "does this
feature vector predict this label at all" evidence screen and its three
sub-reports). Split along that seam into a new Expert/AIBase/FeatureScreen.mqh.
Moved, verbatim (diffed byte-for-byte against the pre-split content - zero
lines differ beyond the file-boundary comment headers): ReportFeatureLabel-
Information, ReportExcursionInformation, ReportFeatureLagProfile, Report-
BarrierGeometryScan, ApplyAdoptedGeometry.
Stayed in AutoTune.mqh: the MI engine (FeatureColumnMI/BuildMiSample/
ScoreMiSample) both files call - a shared dependency used by two consumers is
not itself a reason to split further; TuneIndicatorsByFilter; the export
utilities; and TuneIndicatorsAndTrain, the entry point that decides which of
the two branches a given model runs - it is the coordinator, not a member of
either side.
Still body-only method definitions of CExpertSignalAIBase, same as every
other Expert\AIBase\*.mqh file - MQL5 has no partial classes, so this is a
file-organisation move (legibility, SRP-per-file), not a coupling reduction.
The include site says order between AIBase\*.mqh files is irrelevant, so the
new include was added next to AutoTune.mqh's; the file-scope g_ensembleChart*
globals both files reference stay declared in AutoTune.mqh's header, ahead of
the new include either way.
Verified: brace counts split exactly 105 -> 57+48; every one of the 14
function definitions HEAD had in AutoTune.mqh accounted for in exactly one of
the two files, no duplicates; the WARRIOR_EXPORT_FEATURES ifdef/endif pair
(unrelated, lines 35/147) untouched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Session B of the feature-selection/labeling refactor track. Extracts the two
pieces of triple-barrier arithmetic that were genuinely duplicated or
scattered, taking price/ATR/geometry as plain arguments - no chart, no
indicator handle - so it is testable with synthetic numbers.
CTripleBarrier::ComputeLevels() replaces the fill/barrier level arithmetic
that TripleBarrierLabel() and SimulateTradeOutcome() each spelled out by
hand; their own comments already called it "IDENTICAL... deliberately and by
copy." One caller resolves both sides at once (the both-won tie-break needs
both); the other selects the side its isLong argument names. Same for
ApplyMinStopWidening(), the broker-minimum-stop floor both walks applied.
Fuzzed 200k random (entry, spread, risk, reward, minStop, isLong) tuples
against both original hand-written forms: 0 mismatches.
CLabelOverlap replaces m_labelLifespanSum/m_labelLifespanCount - two members
reset from three separate call sites (constructor, label-cache rebuild), the
exact "N loose members cleared in more than one place" shape a candidate-
geometry incident (7452bd1) turned into a live bug. One object, one Reset(),
default-constructed like every other object member. MeanLabelLifespan() and
EffectiveSampleSize() on the signal become thin forwarders with an unchanged
signature - every one of their ~15 existing callers, direct and through the
CAIBaseTrainingData adapter, is unaffected.
SnapHorizonToLadder() forwards to CTripleBarrier::SnapToLadder(), the ladder
array's one remaining copy; EffectiveHorizonMax() (the close-all cache) stays
on the signal since that state has no clean argument form.
NOT extracted: TripleBarrierLabel()'s ~200-line walk itself. It resolves both
sides simultaneously, tracks the first-passage ladder, and feeds the label
every live order is sized from; a rewrite of it cannot be checked without a
compiler, so only the two pieces provably identical to their originals moved.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Operator, 2026-08-23: retraining is a cost they absorb routinely and is never
to gate work. Two comments claimed the mask stays report-only because pruning
re-keys BuildModelFingerprint() and invalidates every .nnw. That is a real
consequence and worth stating, but it was never the reason.
The actual reason is that no report has been read yet, and selecting features
on a screen nobody has looked at is how a measurement becomes a mistake - a
hold that lifts after one compile and one attach, not one that needs a policy
decision. Comment only; no behaviour changes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CFeatureSelector keeps the per-column MI vector ScoreMiSample has always
computed and thrown away. It is fed from inside the 200 draws
ReportFeatureLabelInformation already performs, so the screen costs an array
copy per draw and not one extra mutual-information computation.
The keep-mask is cut on the single-step maxT (Westfall-Young) statistic - a
column must beat the MAXIMUM of a null draw over all columns, which is strong
family-wise control needing no Bonferroni factor, and is the same null of the
maximum the headline verdict already trusts. The uncorrected per-comparison
p is reported alongside it; the gap between the two counts IS the multiplicity
correction, shown rather than described. Checked offline at 40 columns: 0/40
noise runs keep anything, where the uncorrected rule hands back ~2 columns per
run, and a planted column is recovered 40/40.
REPORT-ONLY. Nothing reads the mask. Pruning changes m_neuronsCount, which is
in BuildModelFingerprint(), which invalidates every .nnw - that is a retrain
across every chart and an operator's call to make after reading the report.
Also fixes the block permutation, found while moving it. When blockRows did
not divide n the short last block, drawn to a non-final slot, read past the
end of the array; the read was clamped to labels[n-1], duplicating one label
and truncating whichever block landed last. 18 of the 24 possible block orders
on n=10/blockRows=3 altered the class counts. A duplicated label concentrates
the class distribution, lowering H(Y) and so the null MI those draws can reach,
so p-values leaned toward significance - the permissive direction, and
m_dirEvidence is a deploy gate. Each block now contributes exactly its own
length. The invariance the old comment asserted ("a permutation preserves the
class counts - that invariance is itself a check on the shuffle") was never
actually compared anywhere; BlockPermute now checks it and returns false, and
all six shuffled call sites already guard on a negative return.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Market rule IV forbids DLL calls, and the DLL compute tier plus the
WebRequest alt-data fetch are what make this bot work. If it is ever sold
it goes through its own channel with the DLLs intact, so a no-DLL build has
nothing left to be for (operator, 2026-08-23).
Removed:
- Warrior_EA.mq5 the //#define toggle and the #resource block behind it
- Variables/Inputs.mqh two #ifdef pairs whose market arms forced every
Use_* NN input and Meta_ExportDataset to false
- AI/NeuronDirectML.mqh the 62-line market stand-in CDirectMLMy whose every
method returned false so the chain fell through to
plain MQL5
- IndicatorResources.mqh WARRIOR_CI(name), which had already collapsed to
(name) - a switch with one position is not a switch.
Its five call sites in Features.mqh name the
indicators directly now.
Behaviour is the private build's, unchanged: bare indicator names loaded
from <MQL5>\Indicators\, all four NN votes defaulting on, dataset export on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gate-ownership commit put CMetaGate *m_metaGate on CExpertSignalCustom
while SMetaGateTelemetry m_metaGate already sat on CExpertSignalAIBase,
which derives from it - so the derived name hid the base one.
The telemetry is m_metaTelemetry now, and the declaration says why:
m_metaGate is the gate the root signal OWNS; this is the RECORD of what a
gate did. Different things, and they read differently at a glance.
Checked the rest of the chain for the same shape - no other member name is
declared in more than one of CExpertSignalCustom / CExpertSignalAIBase /
CSignalMETA.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
g_warriorMetaGate was a file-scope mutable pointer, and it did not need to
be. The root CExpertSignalCustom - the one CExpert actually calls
CheckOpenLong/Short on - now holds the gate as a member, and children reach
it through a parent back-pointer AddFilter sets on adoption.
That was the last piece of the meta veto that behaved like ambient state:
- CheckOpenPosition reads MetaGate() instead of a global.
- EnsembleEraVerdict's replay reads the same MetaGate(). It sits deep in
the training code inside an AI filter and had no route up the tree; a
global WAS that route. m_parentSignal is now, and a back-pointer is safe
for the same reason the gate adapter's owner pointer is - m_filters and
m_gates free their children, so a parent always outlives them.
- The stale-pointer hazard is gone by construction. The global had to be
hand-cleared at every re-init because an input change re-enters OnInit in
the same program instance and frees the old head; the root signal is
new'd fresh each time, so nothing survives one. That reset line is
deleted, not moved.
Note what did NOT need doing: the tree already owned the meta head itself.
AddFilter routes non-voters into m_gates, so it has been a gate child of
the root since the S3 wiring - it was only the VETO that lived outside.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five parallel arrays, a count and a two-array intrusive chain sat on
CExpertSignalAIBase - inherited by every direction model, filled and read
by exactly one subclass. CMetaCandidateStore takes all eight.
What that fixes beyond the clutter:
- THE CHAIN WAS LINKED BY HAND. MetaPrepareEra wrote next[id] = head[bar]
then head[bar] = id itself, after six ArrayResize calls it also wrote out
itself. Add() does the linking, Reset() does the sizing, and a bar off
the grid now cannot be stored at all rather than stored unreachable.
- THE BOUNDS TEST HAD FOUR SITES AND THREE IMPLEMENTATIONS.
MetaCandidateWon indexed side[] with no test at all and answered
"short" for any id out of range - the same shape as the ladder's
negative-index read (2c351a0). Side() is three-state here, IsLong() and
SideIndex() are the safe ways to ask, and the per-side era tally in
RunOosPass is now guarded exactly like the per-family one beside it,
which always was.
Like the ladder and the OOS tally, none of it needs a chart, a net or a
broker: hand it bars and rows and every answer is a function of those.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Since S3 (f64e0f8) the meta head casts no vote - it scores an entry the
consensus already cleared and vetoes the ones under the cost-adjusted
break-even. The code still said otherwise. LiveMetaGate() was a virtual on
CExpertSignalCustom, so MA, RSI, MACD, Ichimoku, the four direction nets,
the session and news filters and the risk guard each carried a meta-gate
method they had no business having; one class implemented it and a dozen
inherited it. The trading pipeline held the gate as a CExpertSignalCustom*
- a signal pointer, with a signal's two hundred other methods reachable
from the entry path.
Expert\Trading\MetaGate.mqh now owns the abstraction:
CMetaGate one pure virtual, Evaluate(), and the two static
readings of a verdict (Blocks / Scored)
META_GATE_* names for the four codes the three call sites used
to spell as bare 0/1/2 and test three different ways
(`< 0` here, `== 2` there, `else` for the rest).
Codes unchanged; only ONE of them blocks, and that
asymmetry is now stated where it lives.
SMetaGateTelemetry the five m_metaGate* members that were on the AI
signal base - inherited by every direction model,
meaningful for none of them. One lifetime, one
writer, one object; the arm latch and the two
counters are a set that clears together.
g_warriorMetaGate is a CMetaGate*. MQL5's single inheritance means the head
cannot also BE one (it already extends the AI base for the net, the era
loop, the feature windows, the label caches and persistence), so it owns a
bound CMetaGateAdapter and hands that out - the same shape CTrainingDataView
uses for the same reason. LiveMetaGate() is gone from the signal base.
Behaviour unchanged: same codes, same thresholds, same fail-open doctrine,
same live-only telemetry rule. The adapter fails open when unbound, on that
same doctrine.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The run-start block calls TrainWindowStart(StartTrainBar), and StartTrainBar
is Train()'s parameter. Moving the block into its own method left the read
behind. Now passed explicitly.
THIRD TIME THIS FAMILY HAS BILLED THIS SESSION, and the third distinct
sub-shape:
1d7ebbd a DELETED loop's variable still read by its body
d7469c6 a RENAMED field still read by its call site
here a MOVED block still reading its old enclosing scope
Same root cause each time: I verify the side I edited. What I had been
checking - statement multisets, brace balance, field-name resolution - all
passed, because none of them models SCOPE. The move was faithful; the
scope was not.
So scope is now checked too. For every CExpertSignalAIBase::Method, collect
the identifiers its body reads and subtract what can actually resolve:
names declared in the body (any type, and every name in a multi-declarator),
the method's own parameters, class members, file-scope globals and #defines.
Parameter names from OTHER declarations must NOT count as resolvable - that
is the bug in the first version of this check, which let StartTrainBar
through because Train() declares it in the header.
Validated against the broken commit before being trusted: it reports
StartTrainBar there and not here. The only residual output is MQL5 enum
members and EA inputs declared outside the scanned headers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Train() was 1,273 lines. It is now 79, of which about 35 are statements, and
they read as what the function is: preempt, begin run, begin era, four
passes, advance, complete, report, finalize.
Seven methods carry what left it:
TrainCallPreempted 107 six ways this call is not a training call at all
BeginTrainRun 130 once per run - history sync, window, one-shot walks
BeginEra 232 once per era, or resume a chunk that yielded
ReportPass1Outcome 105 what pass 1 found, said out loud
AdvanceEra 68 count the era, decide whether the RUN ends
CompleteEra 590 calibrate, gate, rank, checkpoint, ladders, persist
ReportBarrierHold 62 why this member is idle at the era barrier
ClaimCallForWalk 15 the preamble the three exclusive walks shared
TWO DRY FIXES fell out rather than being looked for. The three exclusive
walks each had to tell TWO watchdogs the same thing - the stall reporter
which branch is running, the era-barrier watchdog that this member is BUSY
rather than stuck - written out three times, so a fourth walk was three
chances to be added with only one of them. And the barrier-hold reporting
was 44 lines inline in a branch whose only other statement was resetting a
tick.
CompleteEra is lifted WHOLE and stays that way for now. Its parts share
thirty-odd locals - the recalls, the gate verdict, the better/worse flags -
and threading those through three signatures would recreate exactly the
eight-locals-across-four-passes problem STrainEra was built to end.
Splitting it needs an era-outcome object first, not more parameters.
VERIFIED AS A PURE MOVE: statement multisets, old file vs new, differ only
by the 14 `return;` that became 17 `return true;` plus 3 new returns at the
call sites, the 3 collapsed walk preambles, the 8 new signatures and their
braces. Nothing else moved, and every function closes at depth 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SDeployVerdict's bothSidesLive became twoSided when the member and ensemble
gates were unified, and the member call site kept reading gate.bothSidesLive.
SECOND TIME THIS EXACT SHAPE HAS BILLED THIS SESSION - the first was `s == 0`
surviving the deletion of the loop that declared `s` (1d7ebbd). Renaming a
declaration does not find its readers, and the compiler only finds them when
no other binding happens to fit.
So this is now checked rather than reviewed: every `instance.field` read
against the six value objects is resolved against what the struct actually
declares. Six structs, zero unresolved reads.
Also renames the ensemble local `vote` to `voteGate`. SVoteAccumulator is
already called `vote` in the base class, and two different `vote`s one
inheritance step apart is a reader trap even where the scopes do not clash.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SDeployVerdict::EvaluateRates() is now the deploy arithmetic - coverage
floor, chance + EDGE_MIN_SIGMAS x SE, tradeability, and the coverage-
discounted ranking score - and both gates call it.
The duplicate was self-documenting. The ensemble copy carried three comments
asking a reader to keep it in step with the member copy by hand: "same
intent as the member gate's coverage floor + bothSidesLive", "the two gates
have to apply the identical correction or the ensemble becomes the easier
one to clear", "same lexicographic ordering as isBetterEra". They had
already fallen out of step once - 2c443ba found the ensemble certifying a
vote the EA never casts, in the wrong currency and against the wrong
denominator.
THE TWO REAL DIFFERENCES ARE NOW ARGUMENTS, not branches:
chancePct - the ensemble filters its zero-skill reference by the
direction policy, because with shorts blocked "always short"
is not a book anyone could run.
twoSided - a member reads per-side RECALL against a floor; the vote
reads whether it actually fired both ways.
Everything else was identical and is now literally identical.
effN stays an argument so the label-overlap deflation lives where it is
measured - and so the remaining inconsistency stays visible rather than
buried: the two FAMILY-WISE selection gates still take their SE from RAW n.
Recorded in the header, deliberately not changed; tightening them is a
policy call, not a refactor.
The decision now reads no chart, holds no net, prints nothing and opens no
file, so it can be exercised against a made-up tally.
BEHAVIOUR UNCHANGED: every expression keeps its formula, its guard and its
-1 sentinel; the ensemble's chance-reference and two-sidedness rules are
passed through untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SOosTally holds this era's OOS confusion counts and the rates they imply.
The signal keeps one member where it kept twenty-one, and the era-reset
block loses twenty of its twenty-one clearing lines.
THE SHAPE THIS ENDS is the one that produced 7452bd1: a group of tallies
read together but cleared one-per-line, so a second reset path could clear
a subset and leave stale numerators over restarted denominators. Reset()
is now the only way to clear them and it clears all of them.
The pair had already started to drift. m_oosBuyFired/m_oosBuyFiredHits sat
at line 1085 and their Sell twins at line 1140 - 55 lines and an unrelated
member apart, with the Buy comment still claiming to describe both.
DERIVED RATES MOVE WITH THE DATA. `(bars > 0) ? (int)MathRound(100.0 * x /
bars) : -1` was written out twelve times, and the "-1 means not measurable,
never 0" convention re-spelled at each - a convention the deploy gate
depends on, since every caller tests `< 0` to mean "this does not block".
One rounding rule and one sentinel now.
GROUPED BY LIFETIME, NOT BY NAME. m_oosSamples looks like it belongs here
and does not: it is RUN-level, reset only with the weights, and the status
panel prints it beside dOosError which is also a run-level EMA. That pairing
is correct and stays. But the confidence-calibration block divided per-era
numerators by it, naming the results `empiricalAccuracy` and
`avgClaimedConfidence` when neither is that - the run-level denominator
cancels in their ratio, so eraScale was right and the two named
intermediates were not. Now written as the ratio it actually is, with the
cancellation stated, so nobody logs or gates on a half that decays with era
count.
BEHAVIOUR UNCHANGED: every moved expression preserves its formula, its
denominator and its sentinel.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CFirstPassageLadder owns the three caches (per-rung up/down first-touch ages
plus the terminal travel) and every question asked of them. The signal keeps
one member where it kept three arrays and a lifespan scalar.
WHAT THIS ENDS. The log-space rung snap existed THREE times: once as
LadderRungFor, twice written out inline inside LadderWinShare - and
LadderRungFor's own header said "Same rule LadderWinShare snaps with, so a
rung chosen here and a rung chosen there are the same rung". A comment asking
a reader to keep three copies equal by hand is the arrangement CMetaFamilies
was built to end. It is now one static RungFor(), so the two rungs agree by
construction.
The bounds test was spelled out at four sites and the "0 means never, tie
goes to the stop" comparison at three. Now Has() and FirstTouch(), once.
The four-site bounds test was also subtly weak: it computed
`idx * COUNT` and tested only the upper end, so a negative index slipped
through into a negative array read. Row() rejects it.
Spread and horizon are ARGUMENTS, not state. The ladder is pure travel in ATR
multiples; what a spread costs and how long the walk ran are facts the caller
supplies. Every answer is now a function of its inputs alone - which is the
point, because this is the barrier arithmetic that failed its own acceptance
test in b5e22a1 and it has never been runnable without a chart, a net and a
broker attached.
BEHAVIOUR UNCHANGED. Each moved body was checked statement-multiset against
its predecessor with the rename map reversed; the only differences are the
substitutions named above.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found by grouping, not by looking for it.
The candidate-geometry scan kept ten accumulators as ten separate
members. Era start cleared all ten in a ten-line block. The
shutdown-abort path inside the exit-policy simulation cleared
m_geoTrades and nothing else, so nine partial sums - diffSum,
diffSumSq, incSum, candSum, candSl, candTp, incOpen, candOpen,
startTick - survived the abort with the aborted era's values.
The next era then accumulated onto those sums while counting from
zero, so the paired mean is sum/trades with a numerator carrying an
extra era's worth of difference. The paired sigma is worse: diffSumSq
inherits the same contamination, so the scan reports a tighter or wider
spread than it measured depending on what the abort happened to be
holding.
That is the SAME arithmetic that failed its own acceptance test in
b5e22a1, where the reported gain turned out to be monotone in timeout
share. This is not that bug - it needs a shutdown mid-era to fire - but
it lands on the same number, and any geometry reading taken from a
session that was stopped and restarted is suspect.
SGeometryScan now owns all ten with one Reset(). Both sites call it.
A partial reset is no longer something that can be written: there is
one door, and it clears everything behind it.
The struct initialises itself, so the ten constructor-initialiser
entries in Lifecycle.mqh are gone too - MQL5 cannot list struct fields
there, which is a second reason ten loose members was the wrong shape.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PooledGate was three CExpertSignalAIBase method bodies in an #included
partial. It is now CPooledGate, a class the signal owns.
It needed NO data view. Diagnosing that first is the point: the module
reads a directory of CSV files and knows nothing about a model. The
only things it needs from its owner - the symbol's own numbers and the
ratio they were measured at - are arguments. Handing it a
CTrainingDataView would have been machinery for a dependency that does
not exist.
The owner fills SPoolRecord (the on-disk shape, which already existed)
because only it knows its symbol, its actual TargetRR and its label
lifespan. `id` is passed per call rather than bound, so there is no
init-order question about when the identity became available - m_symbol
is set by CExpertSignal::Init and ID by SetIdentity, at different
times.
m_poolWriteWarned was a one-shot latch living on the signal for a
warning only this module emits. It is m_writeWarned, private.
targetRR is now threaded into ReadPooledEvidence rather than read from
the owner. That is not plumbing for its own sake: a peer measured at a
different ratio has a different structural break-even, and only the
caller knows which ratio it is asking about.
Caught before compiling: I declared ReadPooledEvidence from memory as
(..., double &pooledEffN, const double targetRR). The real signature
ends in `string &detail`. Read the definition, aligned both ends.
Call sites in Training.mqh are untouched - PublishPoolRecord and
PooledGatePasses remain on the signal as the thin fillers that know its
geometry.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MetaCorpus was already a class, so the raw-include problem was not the
one here. The problem was that the rule for naming a signal-DB pattern
table
MetaFamilyName(f) + "_Pattern_" + p + ("_Buy" | "_Sell")
was written out FOUR times, each wrapped in its own identical
family/pattern/side triple loop: the corpus loader, the stale-DB guard,
META's row counter and META's exporter. Four chances for a rename to
leave three of them querying an absent table and reporting it as
"family disabled" - which is what that code says when a table is
missing, so the failure would have looked like normal operation.
CMetaFamilies now owns the taxonomy and that rule. Callers walk ONE
flat index over all 52 tables and never spell a name:
for(int ti = 0; CMetaFamilies::TableAt(ti, table, f, p, isBuy); ti++)
Enumeration order is unchanged - Buy then Sell within a pattern,
families in order - so the corpus is assembled in exactly the same
sequence as before.
META's OneHotSlot had a second hardcoded 0/4/8/14 ladder with a comment
reading "matches MetaFamilyPatterns' 4/4/6/12" - a note asking a reader
to keep two constants in step by hand. The ladder is now summed from
the pattern counts, so they agree by construction. The bound against
META_ONE_HOT_SLOTS stays in META: the head's input width is that
class's business, and a taxonomy grown past it must be caught rather
than silently truncated.
Caught while re-reading the rewritten loop: my first counter was `t`,
and the body declares `MqlDateTime t`. Renamed to `ti` at all four
sites before it reached a compile.
MetaCorpus.mqh moves to Expert\Training\ with the other real classes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The AIBase\*.mqh files are not modules. They are method bodies of one
3,400-line class, textually #included after its declaration. Every one
of them can touch every member of every other, which is why "move this
out" has so far meant "move the whole class".
Introduce the seam that ends that:
CTrainingDataView abstract - the ONLY thing a training-side
collaborator may see: a feature row, a label, an
outcome, an excursion, the shape they share, and
the identity to log under.
CAIBaseTrainingData the adapter. MQL5 gives a class exactly one base
and CExpertSignalAIBase is already a
CExpertSignalCustom, so it cannot implement the
view itself. It owns one of these instead.
Data*() on the the published read API the adapter forwards to.
signal MQL5 has no `friend`, so reaching in from outside
was never an option - and making it explicit is
the point rather than a workaround.
Every row accessor OWNS ITS BOUNDS TEST and answers false for a bar it
has nothing for. Thirty-odd call sites currently carry their own
ArraySize() guard; one that forgets reads past a cache that is shorter
than the bar count for the whole warm-up. The -2.0 "never scored"
sentinel on the arrow cache is folded in the same way, so it can no
longer be mistaken for a small confidence.
Nothing uses it yet - this is the seam only, kept as its own commit so
the pattern compiles before 951 lines of Baselines move onto it. The
pattern is the stdlib's own: abstract base with =0 (Canvas\DX\DXObject),
concrete override, forward-declared owner pointer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Compile error, four sites: TRAIN_TIME_BUDGET_MS was a const local to
Train(), and the four passes that yield on it are now defined above
Train(). I checked every pass for the eight locals STrainEra replaced
and missed the ninth, because it was a const rather than a variable.
It is per-call state like everything else in STrainEra, so it moves
there as budgetMs. All four sites asked the same question the same way
GetTickCount() - era.chunkStartTick >= TRAIN_TIME_BUDGET_MS
so the struct answers it once as BudgetSpent().
Swept for the same class of mistake rather than waiting for another
compile: Train() now has exactly two top-level locals left
(STABILITY_WINDOW, STABILITY_TOLERANCE) and neither appears in any
extracted body.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
673 lines after the OOS pass sat at indent 6 while being at function
level. It read as a block inside something, which is how I initially
mis-measured pass 3 as 1,178 lines when its closing brace is 504 lines
in - the indentation, not the braces, was telling the story.
Whitespace only. Verified by comparing every non-blank line of both
revisions with leading and internal whitespace collapsed, COMMENTS
INCLUDED: zero lines added, zero removed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Train() was still 2,572 lines because the four passes it runs - the
scan/queue sweep, the shuffled replay, the purged calibration walk and
the OOS scoring walk - were written inline as four consecutive blocks
sharing one scope. STrainEra removed the only obstacle to moving them.
Each pass now keeps its own guard inside its own body, so it is
self-contained: RunPass2 still tests !era.stop && era.addLoop &&
!m_isPass2Done itself rather than being called conditionally. Train()
reads as the sequence it always was.
RunOosPass MEASURES and nothing more - the recall gate, plateau ladder,
deploy gate and era checkpoint read its numbers afterwards and stay in
Train(). The first version of its header comment claimed it ran those
too; the body is 504 lines and does not, so the comment was corrected
rather than shipped.
Verified by stripping comments and whitespace from both revisions:
ZERO statements removed, sixteen added - four signatures, their eight
braces and four call sites. Every other statement byte-identical.
Train() 2,572 -> 1,282 lines.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Train() is not a function that trains a model - it is one STEP of a
resumable state machine, called again every tick until the era ends.
Eight locals carried the state from one step to the next: bars,
totalIter, oosCutoff, i, add_loop, stop, chunkStartTick and the
forward-failure latch.
Those eight are the sole reason none of the four passes could be lifted
into a method. Each would have needed eight by-reference parameters,
and a pass that takes eight parameters is not a pass - it is the same
function under another name.
Collapse them into STrainEra. Nothing else changes: no logic, no
ordering, no early return. Verified the same way as the era log - strip
comments and whitespace from both revisions, map era.X back to X, and
diff the remaining statements. The only differences are the five
declaration lines becoming one object and two assignments losing their
type. Every other statement is byte-identical.
One incidental fix: a for(int i...) loop over g_warriorEnsemble shadowed
the era counter. It ran before the era locals were declared so it was
never a live bug, but it becomes one the moment a pass moves out. It is
now mi.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Train() was 2,572 lines in one function. The largest single block in it
was ~200 lines of string building for the console line, reachable only
because twenty-one loose ints were declared at the top of the function
and read nine hundred lines later. Those declarations were the reason
the block could not move.
Introduce SEraTelemetry - one parameter object holding exactly those
twenty-one numbers, self-initialising to -1 ("not measured this era",
which is what era 0 and any stopped era report, and is not the same as
a measured zero). ReportEraProgress() takes it and renders it, guarding
on its own shouldLog so the call site is one unconditional line rather
than a 200-line branch.
Nothing is decided or measured in the moved code - it reads state and
prints. Verified by stripping comments and whitespace from both
revisions and diffing the remaining statements: the only differences
are the ten declarations collapsing into one object, the throttle test
moving inside the callee, and the new signature plus its call. Every
other statement is byte-identical.
Train() 2,572 -> 2,370 lines.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>