feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
//+------------------------------------------------------------------+
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
//| Warrior_EA |
//| AnimateDread |
//| |
2026-08-22 00:30:14 -04:00
//| Era loop, plateau ladder, checkpoint selection, deploy/finalise. |
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
//+------------------------------------------------------------------+
# ifndef WARRIOR_AIBASE_TRAINING_MQH
# define WARRIOR_AIBASE_TRAINING_MQH
//+------------------------------------------------------------------+
2026-08-22 00:30:14 -04:00
//| Does the checkpoint about to deploy survive having been CHOSEN? |
feat: gate deployment on the null of the MAXIMUM, not the per-era null
EDGE_MIN_SIGMAS is a PER-ERA test and the deployed model is the MAXIMUM
over every era a run ranks. A 2-sigma one-sided test passes on noise with
probability 0.0228 per era, so over N eras the chance at least one clears
it is 1-(1-0.0228)^N: 34% by era 18, 80% by era 70, 93% by era 112. The
gate was near-certain to open on a long run whatever the data held.
It did. HYBRID deployed 2026-08-08 at dir-precision 35.5% vs 34% chance -
+1.5pp, best of 112 eras whose per-era values wandered 30%..35.5%. At the
call counts these runs produce that is p_family 0.92..0.9999.
Every OTHER best-of-N decision here already carries this correction, and
every one REJECTS on this data: the barrier-geometry winner (null of the
maximum over 6, p=0.3902), the indicator tuner (Sidak, p=1.0000), the MI
lag profile (null of the maximum over 21 lags). The one decision that
ships a model to a live account had none.
BestCheckpointSurvivesSelection() re-tests the checkpoint that is about to
deploy:
z = (precision - chance)/SE, SE = sqrt(p0(1-p0)/n)
p_single = P(Z >= z)
p_family = 1 - (1-p_single)^N
against DEPLOY_FAMILY_WISE_ALPHA. It uses the checkpoint's OWN
snapshotted precision/chance/call-count, not the latest era's, because
the model that ships is the one that has to clear the bar.
N counts CANDIDATE eras (coverage measurable, at least one directional
call) - an era that called nothing directional could never have become
the best, so counting it would make the gate stricter than the search
that actually happened.
Conservative on purpose: consecutive eras share OOS bars and differ by
one gradient step, so they are nowhere near N independent draws and the
true family-wise error is below this bound. This gate decides what trades
real money and the house posture is reject-unless-demonstrated.
Effect at 2900 directional calls / N=112: required edge goes 1.76pp ->
2.92pp. A real edge clears it; +1.5pp does not.
Applied to BOTH automatic paths - the plateau ladder's stage-3 deploy and
the m_trainingComplete assignment - which must stay identical or the flag
persisted into the .nnw disagrees with the decision to stop, and a reload
runs inference on a model the ladder refused.
NOT applied to the two operator paths (era-cap deploy, panel Deploy
button). Those stay the operator's call; ReportSelectionGateVerdict()
logs the verdict beside them so an authorised deploy can never later be
misread as a validated one.
NormalUpperTail() is A&S 26.2.17 (|err| < 7.5e-8), self-contained rather
than pulling in Math\Stat. Verified against reference values to 6dp:
Q(1.645)=0.049985, Q(1.96)=0.024998, Q(3.0)=0.001350. Its locals are
ntB1..ntB5 because AI\Network.mqh line 79 does "#define b1 AdamBeta1".
Compiles clean: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 18:01:04 -04:00
//+------------------------------------------------------------------+
bool CExpertSignalAIBase : : BestCheckpointSurvivesSelection ( double & zObs , double & pFamily , int & nTried )
{
zObs = 0.0 ;
pFamily = 1.0 ;
nTried = MathMax ( m_deployCandidateEras , 1 ) ;
//--- No ranked era yet, or a degenerate chance rate: nothing to test, so nothing to deploy.
if ( m_bestDirCalls < = 0 | | m_bestDirPrecPct < 0.0 | | m_bestChancePrecPct < = 0.0 | | m_bestChancePrecPct > = 100.0 )
return false ;
refactor(dry): one binomial arithmetic for every "is this edge real" test
The formula p(1-p)/n was transcribed nine times across six files - the two
deploy gates, the two edge floors, the collapse recall floor, the barrier
rung ladder, the inference bin SE, the pooled inverse-variance weights and
both detectability reports. System\BinomialStats.mqh now holds it once, as
free functions with no class dependency, so the god-class declaration does
not grow to host pure math.
BinomialVar(p, n) p(1-p)/n
BinomialSEPct(p, n) 100*sqrt(p(1-p)/n)
BinomialCallsForEdge(p, edge, sigmas) the same, solved for n
NormalUpperTailQ(z) Q(z), via Math\Stat\Normal.mqh
SidakFamilyP(z, N) 1-(1-Q(z))^N
Value-preserving by construction: rates go in as probabilities so no call
site gained a *100/100 round-trip, and BinomialSEPct is written through
BinomialVar so the multiply order is the one it replaced. Every degenerate
guard each site carried (p<=0, p>=1, n<=0) now lives in one place and
returns the 0 those sites already treated as "no bar to clear".
CExpertSignalAIBase::NormalUpperTail is gone; NormalUpperTailQ replaces it.
What consolidating SURFACED, and is deliberately NOT changed here: the two
Sidak selection gates compute their SE on the RAW call count, while every
other SE in the project deflates by EffectiveSampleSize() for triple-
barrier label overlap. That makes them the most permissive test in the
codebase, by ~sqrt(mean label lifespan). Correcting it tightens a live
deploy bar, which is a policy decision, not a refactor - flagged in the
code at both sites.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:50:24 -04:00
//--- RAW calls, not EffectiveSampleSize(): alone among the SEs in this project this one is not
//--- deflated for label overlap, which makes it the most permissive test here. Left as measured
//--- rather than corrected in passing - tightening a live deploy bar is a policy change.
double se = BinomialSEPct ( m_bestChancePrecPct / 100.0 , ( double ) m_bestDirCalls ) ;
feat: gate deployment on the null of the MAXIMUM, not the per-era null
EDGE_MIN_SIGMAS is a PER-ERA test and the deployed model is the MAXIMUM
over every era a run ranks. A 2-sigma one-sided test passes on noise with
probability 0.0228 per era, so over N eras the chance at least one clears
it is 1-(1-0.0228)^N: 34% by era 18, 80% by era 70, 93% by era 112. The
gate was near-certain to open on a long run whatever the data held.
It did. HYBRID deployed 2026-08-08 at dir-precision 35.5% vs 34% chance -
+1.5pp, best of 112 eras whose per-era values wandered 30%..35.5%. At the
call counts these runs produce that is p_family 0.92..0.9999.
Every OTHER best-of-N decision here already carries this correction, and
every one REJECTS on this data: the barrier-geometry winner (null of the
maximum over 6, p=0.3902), the indicator tuner (Sidak, p=1.0000), the MI
lag profile (null of the maximum over 21 lags). The one decision that
ships a model to a live account had none.
BestCheckpointSurvivesSelection() re-tests the checkpoint that is about to
deploy:
z = (precision - chance)/SE, SE = sqrt(p0(1-p0)/n)
p_single = P(Z >= z)
p_family = 1 - (1-p_single)^N
against DEPLOY_FAMILY_WISE_ALPHA. It uses the checkpoint's OWN
snapshotted precision/chance/call-count, not the latest era's, because
the model that ships is the one that has to clear the bar.
N counts CANDIDATE eras (coverage measurable, at least one directional
call) - an era that called nothing directional could never have become
the best, so counting it would make the gate stricter than the search
that actually happened.
Conservative on purpose: consecutive eras share OOS bars and differ by
one gradient step, so they are nowhere near N independent draws and the
true family-wise error is below this bound. This gate decides what trades
real money and the house posture is reject-unless-demonstrated.
Effect at 2900 directional calls / N=112: required edge goes 1.76pp ->
2.92pp. A real edge clears it; +1.5pp does not.
Applied to BOTH automatic paths - the plateau ladder's stage-3 deploy and
the m_trainingComplete assignment - which must stay identical or the flag
persisted into the .nnw disagrees with the decision to stop, and a reload
runs inference on a model the ladder refused.
NOT applied to the two operator paths (era-cap deploy, panel Deploy
button). Those stay the operator's call; ReportSelectionGateVerdict()
logs the verdict beside them so an authorised deploy can never later be
misread as a validated one.
NormalUpperTail() is A&S 26.2.17 (|err| < 7.5e-8), self-contained rather
than pulling in Math\Stat. Verified against reference values to 6dp:
Q(1.645)=0.049985, Q(1.96)=0.024998, Q(3.0)=0.001350. Its locals are
ntB1..ntB5 because AI\Network.mqh line 79 does "#define b1 AdamBeta1".
Compiles clean: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 18:01:04 -04:00
if ( se < = 0.0 )
return false ;
zObs = ( m_bestDirPrecPct - m_bestChancePrecPct ) / se ;
refactor(dry): one binomial arithmetic for every "is this edge real" test
The formula p(1-p)/n was transcribed nine times across six files - the two
deploy gates, the two edge floors, the collapse recall floor, the barrier
rung ladder, the inference bin SE, the pooled inverse-variance weights and
both detectability reports. System\BinomialStats.mqh now holds it once, as
free functions with no class dependency, so the god-class declaration does
not grow to host pure math.
BinomialVar(p, n) p(1-p)/n
BinomialSEPct(p, n) 100*sqrt(p(1-p)/n)
BinomialCallsForEdge(p, edge, sigmas) the same, solved for n
NormalUpperTailQ(z) Q(z), via Math\Stat\Normal.mqh
SidakFamilyP(z, N) 1-(1-Q(z))^N
Value-preserving by construction: rates go in as probabilities so no call
site gained a *100/100 round-trip, and BinomialSEPct is written through
BinomialVar so the multiply order is the one it replaced. Every degenerate
guard each site carried (p<=0, p>=1, n<=0) now lives in one place and
returns the 0 those sites already treated as "no bar to clear".
CExpertSignalAIBase::NormalUpperTail is gone; NormalUpperTailQ replaces it.
What consolidating SURFACED, and is deliberately NOT changed here: the two
Sidak selection gates compute their SE on the RAW call count, while every
other SE in the project deflates by EffectiveSampleSize() for triple-
barrier label overlap. That makes them the most permissive test in the
codebase, by ~sqrt(mean label lifespan). Correcting it tightens a live
deploy bar, which is a policy decision, not a refactor - flagged in the
code at both sites.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:50:24 -04:00
pFamily = SidakFamilyP ( zObs , nTried ) ;
feat: gate deployment on the null of the MAXIMUM, not the per-era null
EDGE_MIN_SIGMAS is a PER-ERA test and the deployed model is the MAXIMUM
over every era a run ranks. A 2-sigma one-sided test passes on noise with
probability 0.0228 per era, so over N eras the chance at least one clears
it is 1-(1-0.0228)^N: 34% by era 18, 80% by era 70, 93% by era 112. The
gate was near-certain to open on a long run whatever the data held.
It did. HYBRID deployed 2026-08-08 at dir-precision 35.5% vs 34% chance -
+1.5pp, best of 112 eras whose per-era values wandered 30%..35.5%. At the
call counts these runs produce that is p_family 0.92..0.9999.
Every OTHER best-of-N decision here already carries this correction, and
every one REJECTS on this data: the barrier-geometry winner (null of the
maximum over 6, p=0.3902), the indicator tuner (Sidak, p=1.0000), the MI
lag profile (null of the maximum over 21 lags). The one decision that
ships a model to a live account had none.
BestCheckpointSurvivesSelection() re-tests the checkpoint that is about to
deploy:
z = (precision - chance)/SE, SE = sqrt(p0(1-p0)/n)
p_single = P(Z >= z)
p_family = 1 - (1-p_single)^N
against DEPLOY_FAMILY_WISE_ALPHA. It uses the checkpoint's OWN
snapshotted precision/chance/call-count, not the latest era's, because
the model that ships is the one that has to clear the bar.
N counts CANDIDATE eras (coverage measurable, at least one directional
call) - an era that called nothing directional could never have become
the best, so counting it would make the gate stricter than the search
that actually happened.
Conservative on purpose: consecutive eras share OOS bars and differ by
one gradient step, so they are nowhere near N independent draws and the
true family-wise error is below this bound. This gate decides what trades
real money and the house posture is reject-unless-demonstrated.
Effect at 2900 directional calls / N=112: required edge goes 1.76pp ->
2.92pp. A real edge clears it; +1.5pp does not.
Applied to BOTH automatic paths - the plateau ladder's stage-3 deploy and
the m_trainingComplete assignment - which must stay identical or the flag
persisted into the .nnw disagrees with the decision to stop, and a reload
runs inference on a model the ladder refused.
NOT applied to the two operator paths (era-cap deploy, panel Deploy
button). Those stay the operator's call; ReportSelectionGateVerdict()
logs the verdict beside them so an authorised deploy can never later be
misread as a validated one.
NormalUpperTail() is A&S 26.2.17 (|err| < 7.5e-8), self-contained rather
than pulling in Math\Stat. Verified against reference values to 6dp:
Q(1.645)=0.049985, Q(1.96)=0.024998, Q(3.0)=0.001350. Its locals are
ntB1..ntB5 because AI\Network.mqh line 79 does "#define b1 AdamBeta1".
Compiles clean: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 18:01:04 -04:00
return ( pFamily < = DEPLOY_FAMILY_WISE_ALPHA ) ;
}
//+------------------------------------------------------------------+
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
//| ENSEMBLE GATE - the same test as above, asked of the VOTE. |
//| See the ENSEMBLE DEPLOY GATE block in ExpertSignalAIBase.mqh for |
//| why the vote rather than the member is the thing being gated. |
//+------------------------------------------------------------------+
bool CExpertSignalAIBase : : EnsembleSurvivesSelection ( double & zObs , double & pFamily , int & nTried )
{
zObs = 0.0 ;
pFamily = 1.0 ;
feat(vote): derive the threshold instead of configuring it
Signal_ThresholdOpen becomes a seed. The era verdict now picks the HIGHEST
sweep rung whose vote still clears the whole deploy gate - coverage floor,
exact-binomial precision bar and two-sidedness together - computes the era's
verdict AT that rung, and publishes it to the live signal's m_threshold_open
so the bar the gate certifies is the bar the EA trades.
Measured on 619 era verdicts across all six live charts:
* every era on every symbol had at least one rung clearing the full gate.
At the fixed 25% the fleet was actually running, four of six symbols had
none, ever. The threshold, not the models, was the blocker.
* walk-forward (rung derived on era N, scored on era N+1): 10.2% coverage /
31.8% precision, against an oracle re-picking on N+1 of 10.3% / 31.7%.
Near-zero shrinkage - a measurement, not a fit. It holds because the
binding constraint is COVERAGE, a near-deterministic step function of the
vote distribution, not precision.
* vs a fixed 15% (best global value): +0.6pp precision, 3.4pp less coverage.
vs a fixed 20%: deployable on all six rather than four of six.
Selection on the highest PASSING rung, never on the best-precision rung - that
is a best-of-6 on a noisy statistic and this project has crowned noise that way
four times. The multiplicity that remains is paid for: nTried in
EnsembleSurvivesSelection is now eras x rungs. Costs nothing - all six charts
clear it by 6.5-12 sigma even forming z on effective rather than raw calls.
Also fixes, in the same path: the direction-policy gate is hoisted above the
per-rung tally so every rung is scored on the population the gate certifies.
Retrain-neutral: not in BuildModelFingerprint(), no .nnw re-keyed.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:02:58 -04:00
//--- ERAS x RUNGS. The deployed configuration is the maximum over every era AND over every
//--- threshold rung the derivation could have landed on (THE DERIVED THRESHOLD, this file), so
//--- the correction has to span both or the gate is testing a smaller family than was searched.
//--- Sidak is conservative under the positive dependence between nested rungs, which is the safe
//--- direction. Measured cost: nothing - all six charts clear this by 6.5-12 sigma even when the
//--- z is formed on EFFECTIVE rather than raw calls.
nTried = MathMax ( g_ensCandidateEras , 1 ) * ENS_THRESHOLD_SWEEP_N ;
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
if ( g_ensBestCalls < = 0 | | g_ensBestPrecPct < 0.0 | | g_ensBestChancePct < = 0.0 | | g_ensBestChancePct > = 100.0 )
return false ;
refactor(dry): one binomial arithmetic for every "is this edge real" test
The formula p(1-p)/n was transcribed nine times across six files - the two
deploy gates, the two edge floors, the collapse recall floor, the barrier
rung ladder, the inference bin SE, the pooled inverse-variance weights and
both detectability reports. System\BinomialStats.mqh now holds it once, as
free functions with no class dependency, so the god-class declaration does
not grow to host pure math.
BinomialVar(p, n) p(1-p)/n
BinomialSEPct(p, n) 100*sqrt(p(1-p)/n)
BinomialCallsForEdge(p, edge, sigmas) the same, solved for n
NormalUpperTailQ(z) Q(z), via Math\Stat\Normal.mqh
SidakFamilyP(z, N) 1-(1-Q(z))^N
Value-preserving by construction: rates go in as probabilities so no call
site gained a *100/100 round-trip, and BinomialSEPct is written through
BinomialVar so the multiply order is the one it replaced. Every degenerate
guard each site carried (p<=0, p>=1, n<=0) now lives in one place and
returns the 0 those sites already treated as "no bar to clear".
CExpertSignalAIBase::NormalUpperTail is gone; NormalUpperTailQ replaces it.
What consolidating SURFACED, and is deliberately NOT changed here: the two
Sidak selection gates compute their SE on the RAW call count, while every
other SE in the project deflates by EffectiveSampleSize() for triple-
barrier label overlap. That makes them the most permissive test in the
codebase, by ~sqrt(mean label lifespan). Correcting it tightens a live
deploy bar, which is a policy decision, not a refactor - flagged in the
code at both sites.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:50:24 -04:00
//--- Raw calls here too, matching the member gate above so neither is the easier one to clear.
double se = BinomialSEPct ( g_ensBestChancePct / 100.0 , ( double ) g_ensBestCalls ) ;
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
if ( se < = 0.0 )
return false ;
zObs = ( g_ensBestPrecPct - g_ensBestChancePct ) / se ;
refactor(dry): one binomial arithmetic for every "is this edge real" test
The formula p(1-p)/n was transcribed nine times across six files - the two
deploy gates, the two edge floors, the collapse recall floor, the barrier
rung ladder, the inference bin SE, the pooled inverse-variance weights and
both detectability reports. System\BinomialStats.mqh now holds it once, as
free functions with no class dependency, so the god-class declaration does
not grow to host pure math.
BinomialVar(p, n) p(1-p)/n
BinomialSEPct(p, n) 100*sqrt(p(1-p)/n)
BinomialCallsForEdge(p, edge, sigmas) the same, solved for n
NormalUpperTailQ(z) Q(z), via Math\Stat\Normal.mqh
SidakFamilyP(z, N) 1-(1-Q(z))^N
Value-preserving by construction: rates go in as probabilities so no call
site gained a *100/100 round-trip, and BinomialSEPct is written through
BinomialVar so the multiply order is the one it replaced. Every degenerate
guard each site carried (p<=0, p>=1, n<=0) now lives in one place and
returns the 0 those sites already treated as "no bar to clear".
CExpertSignalAIBase::NormalUpperTail is gone; NormalUpperTailQ replaces it.
What consolidating SURFACED, and is deliberately NOT changed here: the two
Sidak selection gates compute their SE on the RAW call count, while every
other SE in the project deflates by EffectiveSampleSize() for triple-
barrier label overlap. That makes them the most permissive test in the
codebase, by ~sqrt(mean label lifespan). Correcting it tightens a live
deploy bar, which is a policy decision, not a refactor - flagged in the
code at both sites.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:50:24 -04:00
pFamily = SidakFamilyP ( zObs , nTried ) ;
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
return ( pFamily < = DEPLOY_FAMILY_WISE_ALPHA ) ;
}
//+------------------------------------------------------------------+
2026-08-22 00:30:14 -04:00
//| JOINT CHECKPOINT: snapshot EVERY member's weights, at this one |
//| era, and commit each member's own era statistics as the stats |
//| its best checkpoint is described by. |
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
//+------------------------------------------------------------------+
void CExpertSignalAIBase : : EnsembleCommitJointCheckpoint ( const long votedEra )
{
int captured = 0 , members = 0 ;
for ( int i = 0 ; i < ArraySize ( g_warriorEnsemble ) ; i + + )
{
CExpertSignalAIBase * mm = g_warriorEnsemble [ i ] ;
if ( CheckPointer ( mm ) = = POINTER_INVALID | | mm . m_ensembleIndex < 0 )
continue ;
if ( mm . m_trainingComplete | | mm . m_trainingStopRequested | | mm . m_trainingPaused | | ! mm . m_isInitialized )
continue ;
members + + ;
//--- The member's OWN figures at the winning era. They describe this member's contribution to a
//--- checkpoint the ENSEMBLE selected, which is why they are committed from the stash rather
//--- than from a per-member ranking: no member "won" this era, the vote did.
mm . m_bestOosForecast = mm . m_eraStatBlended ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
mm . m_bestSelectionScore = mm . m_eraStatScore ;
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
mm . m_bestPassedRecall = mm . m_eraStatTradeable ;
mm . m_bestBothSidesLive = mm . m_eraStatTwoSided ;
mm . m_bestDirPrecPct = mm . m_eraStatPrecPct ;
mm . m_bestChancePrecPct = mm . m_eraStatChancePct ;
mm . m_bestDirCalls = mm . m_eraStatCalls ;
mm . m_bestDirConfThreshold = mm . m_eraStatThreshold ;
//--- In-memory snapshot, same primitive the solo path uses. A member whose capture fails keeps
//--- m_haveOosCheckpoint false and is reported - it would otherwise deploy whatever weights it
//--- happens to hold at the end of the run, silently breaking the "deploy what was measured"
//--- guarantee this whole mechanism exists for.
if ( CheckPointer ( mm . Net ) ! = POINTER_INVALID & & mm . Net . CaptureWeights ( ) )
{
mm . m_haveOosCheckpoint = true ;
mm . m_checkpointEra = votedEra ; // the deploy gate cross-checks this against the winning era
captured + + ;
}
else
Print ( mm . ID + " : WARNING - joint ensemble checkpoint capture FAILED at era " +
IntegerToString ( ( int ) votedEra ) + " . This member cannot contribute the weights the vote "
" was measured with; the ensemble will not deploy a checkpoint it cannot reproduce. " ) ;
//--- a new joint best retires the shared ladder for everyone
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
mm . m_erasSinceBest = 0 ;
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
mm . m_plateauStage = 0 ;
mm . m_restartBoostErasLeft = 0 ;
mm . m_consecutiveRegressions = 0 ;
}
2026-08-22 00:25:52 -04:00
//--- PARTIAL CAPTURE IS NOT A CHECKPOINT. Rolling it back lets the run carry on and simply find
//--- its best again.
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
if ( captured < members )
{
g_ensBestScore = -1.0 ;
g_ensBestTradeable = false ;
g_ensBestTwoSided = false ;
g_ensBestCalls = 0 ;
g_ensBestEra = -1 ;
2026-08-26 01:14:51 -04:00
//--- Cleared with the rest: a refusal must not describe an era that is no longer the best.
g_ensBestCoveragePct = -1.0 ;
g_ensBestMinCoverPct = -1.0 ;
g_ensBestEdgeFloorPct = -1.0 ;
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
Print ( " AI ensemble: joint checkpoint INCOMPLETE at era " + IntegerToString ( ( int ) votedEra ) +
" ( " + IntegerToString ( captured ) + " of " + IntegerToString ( members ) + " members captured) "
" - discarding this era as the best; the search continues from no joint checkpoint. " ) ;
}
}
//+------------------------------------------------------------------+
2026-08-22 00:30:14 -04:00
//| Once per era, on the LAST still-training member to finish its |
//| pass-3 scan: score the combined vote, rank the era, checkpoint, |
//| advance the shared plateau ladder, and decide deployment. |
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
//+------------------------------------------------------------------+
void CExpertSignalAIBase : : EnsembleEraVerdict ( const int needMask , const long votedEra , double & etaLocal )
{
fix(gate): the ensemble gate certified a vote the EA never casts
g_ensembleVoteThreshold's comment claims the combined-vote scorer "fires on
the same criterion the live trade does". It did not. Two independent
mismatches, both silent:
CURRENCY. Each member contributed its raw signed confidence x100 - a 33..100
number straight off the softmax head. Live contributes m_weight x the tier's
pattern weight, and BOTH of those are rewritten from the signal DB by
UpdateSignalsWeights(). A head output and a DB-ranked win-rate weight share
an axis and nothing relates them, so the same bar was one number to the gate
and a different one to the order path. Same shape as the 2026-08-09 geometry
incident: certified on one game, paid on another.
DENOMINATOR. The gate divided by the member count, so an abstaining member
pulled the average toward zero. CExpertSignalCustom::Direction() skips a zero
contribution in BOTH the sum and the count (`if(direction == 0) continue;`
before `number++`) - live is a mean over VOTERS. The gate was therefore
scoring a strictly more agreement-heavy set of bars than the EA trades. The
contribution hook's own comment asserted the opposite ("abstentions dilute
the average exactly as they do in the live vote"), while the AI_CHOICE enum
20 lines away correctly documented union semantics.
LiveVoteContribution() is now the single definition of "what this member
votes", called from the gate; the live path reaches the same arithmetic
through LongCondition/ShortCondition. g_ensVoteVoterMask records who actually
voted, separately from who evaluated the bar, because those are the divisor
and the shared-population test respectively.
ConfidenceTier() is split into ConfidenceTierFor(signal) plus a thin live-bar
wrapper - the OOS scan holds the scanned bar's decision in a local, and
dPrevSignal is a different bar.
NOT changed, deliberately: the gate still does not model live NMS
declustering, and the per-member solo gate still scores every directional
call rather than threshold-clearing ones. Both are selection-metric changes
and this codebase has twice been bitten by switching one blind.
Also corrects a stale paragraph in m_pattern_0's declaration block quoting
80/87/93/100 as the tier defaults. The constructor is 25/50/75/100 and has
been since the confidence floor and alternation gate were removed; the block
carried both tables at once, and the dead one was quoted back as fact.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 15:23:33 -04:00
int members = EnsembleBitCount ( needMask ) ;
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
//--- One trainer left (the others deployed, paused or stopped) is not an ensemble read: the
//--- "vote" would be that member's own signal and the gate would silently become the solo gate
//--- under an ensemble label. Members keep training; nothing is ranked or deployed from here.
if ( members < 2 )
return ;
//--- SHARED BARS ONLY. A bar one member skipped (feature-window failure) has an average over a
//--- different membership, which is a different quantity - averaging it in would make the score
//--- depend on which member happened to fail where.
int shared = 0 , fired = 0 , wins = 0 , firedLong = 0 , firedShort = 0 ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
int dirLabelBars = 0 , labelBuyBars = 0 , labelSellBars = 0 ;
feat(vote): derive the threshold instead of configuring it
Signal_ThresholdOpen becomes a seed. The era verdict now picks the HIGHEST
sweep rung whose vote still clears the whole deploy gate - coverage floor,
exact-binomial precision bar and two-sidedness together - computes the era's
verdict AT that rung, and publishes it to the live signal's m_threshold_open
so the bar the gate certifies is the bar the EA trades.
Measured on 619 era verdicts across all six live charts:
* every era on every symbol had at least one rung clearing the full gate.
At the fixed 25% the fleet was actually running, four of six symbols had
none, ever. The threshold, not the models, was the blocker.
* walk-forward (rung derived on era N, scored on era N+1): 10.2% coverage /
31.8% precision, against an oracle re-picking on N+1 of 10.3% / 31.7%.
Near-zero shrinkage - a measurement, not a fit. It holds because the
binding constraint is COVERAGE, a near-deterministic step function of the
vote distribution, not precision.
* vs a fixed 15% (best global value): +0.6pp precision, 3.4pp less coverage.
vs a fixed 20%: deployable on all six rather than four of six.
Selection on the highest PASSING rung, never on the best-precision rung - that
is a best-of-6 on a noisy statistic and this project has crowned noise that way
four times. The multiplicity that remains is paid for: nTried in
EnsembleSurvivesSelection is now eras x rungs. Costs nothing - all six charts
clear it by 6.5-12 sigma even forming z on effective rather than raw calls.
Also fixes, in the same path: the direction-policy gate is hoisted above the
per-rung tally so every rung is scored on the population the gate certifies.
Retrain-neutral: not in BuildModelFingerprint(), no .nnw re-keyed.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:02:58 -04:00
//--- PER-RUNG TALLIES, accumulated in the row loop below. No longer a side diagnostic: the era's
//--- own verdict is read out of these at the DERIVED rung (see THE DERIVED THRESHOLD below), so
//--- these ARE the era's numbers. Long/short are split because the anti-degenerate test needs to
//--- know whether the rung fired both ways, and a rung that only ever fired one side is not an
//--- operating point anyone can trade however precise it looked.
2026-08-26 04:17:27 -04:00
int sweepFired [ ENS_THRESHOLD_SWEEP_N ] , sweepWins [ ENS_THRESHOLD_SWEEP_N ] ;
feat(vote): derive the threshold instead of configuring it
Signal_ThresholdOpen becomes a seed. The era verdict now picks the HIGHEST
sweep rung whose vote still clears the whole deploy gate - coverage floor,
exact-binomial precision bar and two-sidedness together - computes the era's
verdict AT that rung, and publishes it to the live signal's m_threshold_open
so the bar the gate certifies is the bar the EA trades.
Measured on 619 era verdicts across all six live charts:
* every era on every symbol had at least one rung clearing the full gate.
At the fixed 25% the fleet was actually running, four of six symbols had
none, ever. The threshold, not the models, was the blocker.
* walk-forward (rung derived on era N, scored on era N+1): 10.2% coverage /
31.8% precision, against an oracle re-picking on N+1 of 10.3% / 31.7%.
Near-zero shrinkage - a measurement, not a fit. It holds because the
binding constraint is COVERAGE, a near-deterministic step function of the
vote distribution, not precision.
* vs a fixed 15% (best global value): +0.6pp precision, 3.4pp less coverage.
vs a fixed 20%: deployable on all six rather than four of six.
Selection on the highest PASSING rung, never on the best-precision rung - that
is a best-of-6 on a noisy statistic and this project has crowned noise that way
four times. The multiplicity that remains is paid for: nTried in
EnsembleSurvivesSelection is now eras x rungs. Costs nothing - all six charts
clear it by 6.5-12 sigma even forming z on effective rather than raw calls.
Also fixes, in the same path: the direction-policy gate is hoisted above the
per-rung tally so every rung is scored on the population the gate certifies.
Retrain-neutral: not in BuildModelFingerprint(), no .nnw re-keyed.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:02:58 -04:00
int sweepLong [ ENS_THRESHOLD_SWEEP_N ] , sweepShort [ ENS_THRESHOLD_SWEEP_N ] ;
2026-08-26 04:17:27 -04:00
ArrayInitialize ( sweepFired , 0 ) ;
ArrayInitialize ( sweepWins , 0 ) ;
feat(vote): derive the threshold instead of configuring it
Signal_ThresholdOpen becomes a seed. The era verdict now picks the HIGHEST
sweep rung whose vote still clears the whole deploy gate - coverage floor,
exact-binomial precision bar and two-sidedness together - computes the era's
verdict AT that rung, and publishes it to the live signal's m_threshold_open
so the bar the gate certifies is the bar the EA trades.
Measured on 619 era verdicts across all six live charts:
* every era on every symbol had at least one rung clearing the full gate.
At the fixed 25% the fleet was actually running, four of six symbols had
none, ever. The threshold, not the models, was the blocker.
* walk-forward (rung derived on era N, scored on era N+1): 10.2% coverage /
31.8% precision, against an oracle re-picking on N+1 of 10.3% / 31.7%.
Near-zero shrinkage - a measurement, not a fit. It holds because the
binding constraint is COVERAGE, a near-deterministic step function of the
vote distribution, not precision.
* vs a fixed 15% (best global value): +0.6pp precision, 3.4pp less coverage.
vs a fixed 20%: deployable on all six rather than four of six.
Selection on the highest PASSING rung, never on the best-precision rung - that
is a best-of-6 on a noisy statistic and this project has crowned noise that way
four times. The multiplicity that remains is paid for: nTried in
EnsembleSurvivesSelection is now eras x rungs. Costs nothing - all six charts
clear it by 6.5-12 sigma even forming z on effective rather than raw calls.
Also fixes, in the same path: the direction-policy gate is hoisted above the
per-rung tally so every rung is scored on the population the gate certifies.
Retrain-neutral: not in BuildModelFingerprint(), no .nnw re-keyed.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:02:58 -04:00
ArrayInitialize ( sweepLong , 0 ) ;
ArrayInitialize ( sweepShort , 0 ) ;
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
for ( int r = 0 ; r < g_ensVoteRows ; r + + )
{
if ( ( g_ensVoteMask [ r ] & needMask ) ! = needMask )
continue ;
shared + + ;
if ( g_ensVoteDirLabel [ r ] )
dirLabelBars + + ;
//--- zero-skill reference, measured over EVERY shared bar (see chancePrecPct's derivation in
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//--- the era-end block): what always-Buy and always-Sell would have scored here
if ( g_ensVoteLabelBuy [ r ] )
labelBuyBars + + ;
if ( g_ensVoteLabelSell [ r ] )
labelSellBars + + ;
2026-08-22 00:25:52 -04:00
//--- THE LIVE AGGREGATION, reproduced exactly (CExpertSignalCustom::Direction(), pass 2 plus
//--- the `result /= number` normalization): sum the members' signed votes, divide by how many
//--- of them ACTUALLY VOTED, and compare the magnitude against Signal_ThresholdOpen on the
//--- same 0..100 scale the tier weights already live on.
fix(gate): the ensemble gate certified a vote the EA never casts
g_ensembleVoteThreshold's comment claims the combined-vote scorer "fires on
the same criterion the live trade does". It did not. Two independent
mismatches, both silent:
CURRENCY. Each member contributed its raw signed confidence x100 - a 33..100
number straight off the softmax head. Live contributes m_weight x the tier's
pattern weight, and BOTH of those are rewritten from the signal DB by
UpdateSignalsWeights(). A head output and a DB-ranked win-rate weight share
an axis and nothing relates them, so the same bar was one number to the gate
and a different one to the order path. Same shape as the 2026-08-09 geometry
incident: certified on one game, paid on another.
DENOMINATOR. The gate divided by the member count, so an abstaining member
pulled the average toward zero. CExpertSignalCustom::Direction() skips a zero
contribution in BOTH the sum and the count (`if(direction == 0) continue;`
before `number++`) - live is a mean over VOTERS. The gate was therefore
scoring a strictly more agreement-heavy set of bars than the EA trades. The
contribution hook's own comment asserted the opposite ("abstentions dilute
the average exactly as they do in the live vote"), while the AI_CHOICE enum
20 lines away correctly documented union semantics.
LiveVoteContribution() is now the single definition of "what this member
votes", called from the gate; the live path reaches the same arithmetic
through LongCondition/ShortCondition. g_ensVoteVoterMask records who actually
voted, separately from who evaluated the bar, because those are the divisor
and the shared-population test respectively.
ConfidenceTier() is split into ConfidenceTierFor(signal) plus a thin live-bar
wrapper - the OOS scan holds the scanned bar's decision in a local, and
dPrevSignal is a different bar.
NOT changed, deliberately: the gate still does not model live NMS
declustering, and the per-member solo gate still scores every directional
call rather than threshold-clearing ones. Both are selection-metric changes
and this codebase has twice been bitten by switching one blind.
Also corrects a stale paragraph in m_pattern_0's declaration block quoting
80/87/93/100 as the tier defaults. The constructor is 25/50/75/100 and has
been since the confidence floor and alternation gate were removed; the block
carried both tables at once, and the dead one was quoted back as fact.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 15:23:33 -04:00
int voters = EnsembleBitCount ( g_ensVoteVoterMask [ r ] & needMask ) ;
feat(vote): thresholds become confidence percentages, on ONE scale everywhere
User request: "the entry/exit thresholds are manual numbers, I would like
them to be confidence percentages, so the current 20 would be only 20%
confidence in a profitable trade."
WHY 20 WAS EVER SENSIBLE. Under UseDatabaseRanking both factors of a filter's
contribution are win rates: the pattern weight is that pattern's measured win
rate (UpdateSignalsWeights -> ApplyPatternWeight) and m_weight is the filter's
average win rate over its patterns, /100. Dividing the sum by the VOTER COUNT
therefore produced a mean of PRODUCTS of two win rates - a genuinely
60%-accurate filter firing a 60% pattern scored 0.60 x 60 = 36. The number was
never on a probability scale, so its magnitude meant nothing on its own.
Dividing by Sum(m_weight) instead makes it a weighted MEAN of win rates, which
is a win rate: result = Sum(w_i*p_i)/Sum(w_i). Every voter at 60% now reads 60;
MACD's double-divergence pattern (weight 100) voting alone reads 100. m_weight
stops being a discount on the probability and becomes how much a filter's
opinion COUNTS - which is what a module weight should always have been.
Default Min_Vote_Open 20 -> 50: not a tightening, the same bar re-expressed.
ONE SCALE, EVERYWHERE - the part that made this bigger than a rescale. Three
other places compared against a 0..1 softmax confidence and would each have
become a fresh currency mismatch the moment the input changed meaning:
* the AI early-exit route (LiveSignedConfidence vs m_ai_exit_threshold) now
reads m_lastAiVote - the AI filters' own weighted mean, undiluted by the
classic side, which is the only reason that route exists - against the
same m_threshold_close the averaged vote uses. m_ai_exit_threshold is
retired rather than left dangling.
* m_oosDecisionSeries now carries the vote, not the confidence, so the exit
SIMULATION stops modelling a close rule the EA does not run.
* ExitPolicy() clamped anything > 1.0 to zero. Passing the unscaled input
through that would have silently switched vote exits off in the
simulation while live went on running them - found before it shipped;
the bound now tracks the scale.
LiveSignedConfidence() is deliberately untouched and still 0..1: MM sizing,
SL/TP scaling and the intelligent trailing want a model confidence, not a win
rate.
CALIBRATION CAVEAT, stated in the code where the claim is made: this is only a
real probability to the extent the pattern weights are. A pattern with fewer
than MIN_TRADES_FOR_WIN_RATE journaled trades keeps its DEFAULT weight - a
designed prior (25/50/75/100 for the AI tiers), not a measurement. Until the
signal DB fills, "60" means "the designed conviction of the patterns that
fired". Closing that gap is the next commit.
Also corrects VOTE_CLOSE_PRESETS' comment, which documented the two scales
this removes.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 15:52:08 -04:00
if ( voters < = 0 | | g_ensVoteWeightSum [ r ] < = 0.0 )
fix(gate): the ensemble gate certified a vote the EA never casts
g_ensembleVoteThreshold's comment claims the combined-vote scorer "fires on
the same criterion the live trade does". It did not. Two independent
mismatches, both silent:
CURRENCY. Each member contributed its raw signed confidence x100 - a 33..100
number straight off the softmax head. Live contributes m_weight x the tier's
pattern weight, and BOTH of those are rewritten from the signal DB by
UpdateSignalsWeights(). A head output and a DB-ranked win-rate weight share
an axis and nothing relates them, so the same bar was one number to the gate
and a different one to the order path. Same shape as the 2026-08-09 geometry
incident: certified on one game, paid on another.
DENOMINATOR. The gate divided by the member count, so an abstaining member
pulled the average toward zero. CExpertSignalCustom::Direction() skips a zero
contribution in BOTH the sum and the count (`if(direction == 0) continue;`
before `number++`) - live is a mean over VOTERS. The gate was therefore
scoring a strictly more agreement-heavy set of bars than the EA trades. The
contribution hook's own comment asserted the opposite ("abstentions dilute
the average exactly as they do in the live vote"), while the AI_CHOICE enum
20 lines away correctly documented union semantics.
LiveVoteContribution() is now the single definition of "what this member
votes", called from the gate; the live path reaches the same arithmetic
through LongCondition/ShortCondition. g_ensVoteVoterMask records who actually
voted, separately from who evaluated the bar, because those are the divisor
and the shared-population test respectively.
ConfidenceTier() is split into ConfidenceTierFor(signal) plus a thin live-bar
wrapper - the OOS scan holds the scanned bar's decision in a local, and
dPrevSignal is a different bar.
NOT changed, deliberately: the gate still does not model live NMS
declustering, and the per-member solo gate still scores every directional
call rather than threshold-clearing ones. Both are selection-metric changes
and this codebase has twice been bitten by switching one blind.
Also corrects a stale paragraph in m_pattern_0's declaration block quoting
80/87/93/100 as the tier defaults. The constructor is 25/50/75/100 and has
been since the confidence floor and alternation gate were removed; the block
carried both tables at once, and the dead one was quoted back as fact.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 15:23:33 -04:00
continue ; // every member abstained: no vote, no trade, not a fired bar
feat(vote): thresholds become confidence percentages, on ONE scale everywhere
User request: "the entry/exit thresholds are manual numbers, I would like
them to be confidence percentages, so the current 20 would be only 20%
confidence in a profitable trade."
WHY 20 WAS EVER SENSIBLE. Under UseDatabaseRanking both factors of a filter's
contribution are win rates: the pattern weight is that pattern's measured win
rate (UpdateSignalsWeights -> ApplyPatternWeight) and m_weight is the filter's
average win rate over its patterns, /100. Dividing the sum by the VOTER COUNT
therefore produced a mean of PRODUCTS of two win rates - a genuinely
60%-accurate filter firing a 60% pattern scored 0.60 x 60 = 36. The number was
never on a probability scale, so its magnitude meant nothing on its own.
Dividing by Sum(m_weight) instead makes it a weighted MEAN of win rates, which
is a win rate: result = Sum(w_i*p_i)/Sum(w_i). Every voter at 60% now reads 60;
MACD's double-divergence pattern (weight 100) voting alone reads 100. m_weight
stops being a discount on the probability and becomes how much a filter's
opinion COUNTS - which is what a module weight should always have been.
Default Min_Vote_Open 20 -> 50: not a tightening, the same bar re-expressed.
ONE SCALE, EVERYWHERE - the part that made this bigger than a rescale. Three
other places compared against a 0..1 softmax confidence and would each have
become a fresh currency mismatch the moment the input changed meaning:
* the AI early-exit route (LiveSignedConfidence vs m_ai_exit_threshold) now
reads m_lastAiVote - the AI filters' own weighted mean, undiluted by the
classic side, which is the only reason that route exists - against the
same m_threshold_close the averaged vote uses. m_ai_exit_threshold is
retired rather than left dangling.
* m_oosDecisionSeries now carries the vote, not the confidence, so the exit
SIMULATION stops modelling a close rule the EA does not run.
* ExitPolicy() clamped anything > 1.0 to zero. Passing the unscaled input
through that would have silently switched vote exits off in the
simulation while live went on running them - found before it shipped;
the bound now tracks the scale.
LiveSignedConfidence() is deliberately untouched and still 0..1: MM sizing,
SL/TP scaling and the intelligent trailing want a model confidence, not a win
rate.
CALIBRATION CAVEAT, stated in the code where the claim is made: this is only a
real probability to the extent the pattern weights are. A pattern with fewer
than MIN_TRADES_FOR_WIN_RATE journaled trades keeps its DEFAULT weight - a
designed prior (25/50/75/100 for the AI tiers), not a measurement. Until the
signal DB fills, "60" means "the designed conviction of the patterns that
fired". Closing that gap is the next commit.
Also corrects VOTE_CLOSE_PRESETS' comment, which documented the two scales
this removes.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 15:52:08 -04:00
double net = g_ensVoteSum [ r ] / g_ensVoteWeightSum [ r ] ;
2026-08-26 04:17:27 -04:00
//--- THRESHOLD SWEEP - PURE DIAGNOSTIC, CHANGES NOTHING. What this same era's vote would have
//--- scored at the other Signal_ThresholdOpen rungs, measured on these exact rows rather than
//--- modelled. It exists because the threshold is the one parameter this gate cannot reason
//--- about from its own output: the refusal can say "coverage too low" but not "and here is
//--- what it would be one rung down", and MT5 stores the input PER CHART (profiles\Charts\*
//--- \chart*.chr), so an operator cannot cheaply A/B it either - an already-attached EA
//--- ignores a changed default entirely. Accumulated before the live threshold test below so
//--- the sweep sees every scored row, and gated by the same direction policy so its numbers
//--- are comparable with the ones the gate actually certifies.
feat(consistency): the five review flaws fixed - training wears the live constraints, the gate wears the policy
1. Labels and the exit simulator go through the broker's stop-distance
check: risk/reward widen to SYMBOL_TRADE_STOPS_LEVEL exactly as
TCAdjustStops does at order time - the M5/tight-ATR case where live
trades ran wider geometry than training measured. Current stops level
stands in for history (like the spread); measured quantity, so it
does not key the fingerprint.
2. The Intelligent drift verdict moved into RefreshDriftVerdict(), which
RESCANS the label cache and now runs at every era end beside
RankTiersFromOos - era-cadence instead of waiting for rare full
rebuilds. Prints only on change.
3. Session filter is any-broker: sessions defined on their financial
centres' civil clocks (London 08-16 Europe/London, NY 08-17
America/New_York, Tokyo 09-18 Asia/Tokyo), converted to UTC by each
centre's own computed DST rule (EU last-Sun-Mar/Oct, US
2nd-Sun-Mar/1st-Sun-Nov), then to broker time by the MEASURED
server-vs-GMT offset (half-hour brokers included). Windows may wrap
midnight in broker time - the interval test handles it. Replaces the
EET-hardcoded anchors, which were correct on exactly one broker and
got Tokyo wrong by an hour each European summer.
4. The current-session-table-for-history caveat resolved by analysis:
the bars bound the error - a too-late assumed close meets no bars
(zero error), a too-early one truncates conservatively (<=1h, never
optimistic, cannot manufacture edge). Documented at the site.
5. The ensemble deploy gate mirrors the direction policy: blocked-side
fires are not fired bars (certified == traded), the zero-skill
reference uses only ACHIEVABLE baselines (always-short is not a
strategy a long-only book can run), and one-sidedness BY POLICY is
not degeneracy - the two-sided requirement applies only when both
sides are allowed. Sell predictions keep their other jobs (exit
triggers, consensus dilution) untouched.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 12:14:01 -04:00
//--- THE DIRECTION POLICY IS PART OF WHAT GETS CERTIFIED (2026-08-19). Under LONG_ONLY/
2026-08-24 22:11:50 -04:00
//--- SHORT_ONLY blocks live from ever placing the other side's trades - scoring them here
//--- would certify a vote the EA does not cast, the exact certified!=traded defect this
feat(vote): derive the threshold instead of configuring it
Signal_ThresholdOpen becomes a seed. The era verdict now picks the HIGHEST
sweep rung whose vote still clears the whole deploy gate - coverage floor,
exact-binomial precision bar and two-sidedness together - computes the era's
verdict AT that rung, and publishes it to the live signal's m_threshold_open
so the bar the gate certifies is the bar the EA trades.
Measured on 619 era verdicts across all six live charts:
* every era on every symbol had at least one rung clearing the full gate.
At the fixed 25% the fleet was actually running, four of six symbols had
none, ever. The threshold, not the models, was the blocker.
* walk-forward (rung derived on era N, scored on era N+1): 10.2% coverage /
31.8% precision, against an oracle re-picking on N+1 of 10.3% / 31.7%.
Near-zero shrinkage - a measurement, not a fit. It holds because the
binding constraint is COVERAGE, a near-deterministic step function of the
vote distribution, not precision.
* vs a fixed 15% (best global value): +0.6pp precision, 3.4pp less coverage.
vs a fixed 20%: deployable on all six rather than four of six.
Selection on the highest PASSING rung, never on the best-precision rung - that
is a best-of-6 on a noisy statistic and this project has crowned noise that way
four times. The multiplicity that remains is paid for: nTried in
EnsembleSurvivesSelection is now eras x rungs. Costs nothing - all six charts
clear it by 6.5-12 sigma even forming z on effective rather than raw calls.
Also fixes, in the same path: the direction-policy gate is hoisted above the
per-rung tally so every rung is scored on the population the gate certifies.
Retrain-neutral: not in BuildModelFingerprint(), no .nnw re-keyed.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:02:58 -04:00
//--- gate was rebuilt to end (2c443ba). Hoisted above the tally (it used to sit below the
//--- sweep) so EVERY rung is scored on the same population the gate will certify.
feat(consistency): the five review flaws fixed - training wears the live constraints, the gate wears the policy
1. Labels and the exit simulator go through the broker's stop-distance
check: risk/reward widen to SYMBOL_TRADE_STOPS_LEVEL exactly as
TCAdjustStops does at order time - the M5/tight-ATR case where live
trades ran wider geometry than training measured. Current stops level
stands in for history (like the spread); measured quantity, so it
does not key the fingerprint.
2. The Intelligent drift verdict moved into RefreshDriftVerdict(), which
RESCANS the label cache and now runs at every era end beside
RankTiersFromOos - era-cadence instead of waiting for rare full
rebuilds. Prints only on change.
3. Session filter is any-broker: sessions defined on their financial
centres' civil clocks (London 08-16 Europe/London, NY 08-17
America/New_York, Tokyo 09-18 Asia/Tokyo), converted to UTC by each
centre's own computed DST rule (EU last-Sun-Mar/Oct, US
2nd-Sun-Mar/1st-Sun-Nov), then to broker time by the MEASURED
server-vs-GMT offset (half-hour brokers included). Windows may wrap
midnight in broker time - the interval test handles it. Replaces the
EET-hardcoded anchors, which were correct on exactly one broker and
got Tokyo wrong by an hour each European summer.
4. The current-session-table-for-history caveat resolved by analysis:
the bars bound the error - a too-late assumed close meets no bars
(zero error), a too-early one truncates conservatively (<=1h, never
optimistic, cannot manufacture edge). Documented at the site.
5. The ensemble deploy gate mirrors the direction policy: blocked-side
fires are not fired bars (certified == traded), the zero-skill
reference uses only ACHIEVABLE baselines (always-short is not a
strategy a long-only book can run), and one-sidedness BY POLICY is
not degeneracy - the two-sided requirement applies only when both
sides are allowed. Sell predictions keep their other jobs (exit
triggers, consensus dilution) untouched.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 12:14:01 -04:00
if ( ! WarriorDirectionAllows ( net > 0.0 ) )
continue ;
feat(vote): derive the threshold instead of configuring it
Signal_ThresholdOpen becomes a seed. The era verdict now picks the HIGHEST
sweep rung whose vote still clears the whole deploy gate - coverage floor,
exact-binomial precision bar and two-sidedness together - computes the era's
verdict AT that rung, and publishes it to the live signal's m_threshold_open
so the bar the gate certifies is the bar the EA trades.
Measured on 619 era verdicts across all six live charts:
* every era on every symbol had at least one rung clearing the full gate.
At the fixed 25% the fleet was actually running, four of six symbols had
none, ever. The threshold, not the models, was the blocker.
* walk-forward (rung derived on era N, scored on era N+1): 10.2% coverage /
31.8% precision, against an oracle re-picking on N+1 of 10.3% / 31.7%.
Near-zero shrinkage - a measurement, not a fit. It holds because the
binding constraint is COVERAGE, a near-deterministic step function of the
vote distribution, not precision.
* vs a fixed 15% (best global value): +0.6pp precision, 3.4pp less coverage.
vs a fixed 20%: deployable on all six rather than four of six.
Selection on the highest PASSING rung, never on the best-precision rung - that
is a best-of-6 on a noisy statistic and this project has crowned noise that way
four times. The multiplicity that remains is paid for: nTried in
EnsembleSurvivesSelection is now eras x rungs. Costs nothing - all six charts
clear it by 6.5-12 sigma even forming z on effective rather than raw calls.
Also fixes, in the same path: the direction-policy gate is hoisted above the
per-rung tally so every rung is scored on the population the gate certifies.
Retrain-neutral: not in BuildModelFingerprint(), no .nnw re-keyed.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:02:58 -04:00
double mag = MathAbs ( net ) ;
bool swHit = ( net > 0.0 ) ? g_ensVoteLabelBuy [ r ] : g_ensVoteLabelSell [ r ] ;
for ( int s = 0 ; s < ENS_THRESHOLD_SWEEP_N ; s + + )
if ( mag > = g_ensThresholdSweep [ s ] )
{
sweepFired [ s ] + + ;
if ( swHit )
sweepWins [ s ] + + ;
if ( net > 0.0 )
sweepLong [ s ] + + ;
else
sweepShort [ s ] + + ;
}
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
}
bool measurable = ( shared > 0 & & dirLabelBars > 0 ) ;
feat(consistency): the five review flaws fixed - training wears the live constraints, the gate wears the policy
1. Labels and the exit simulator go through the broker's stop-distance
check: risk/reward widen to SYMBOL_TRADE_STOPS_LEVEL exactly as
TCAdjustStops does at order time - the M5/tight-ATR case where live
trades ran wider geometry than training measured. Current stops level
stands in for history (like the spread); measured quantity, so it
does not key the fingerprint.
2. The Intelligent drift verdict moved into RefreshDriftVerdict(), which
RESCANS the label cache and now runs at every era end beside
RankTiersFromOos - era-cadence instead of waiting for rare full
rebuilds. Prints only on change.
3. Session filter is any-broker: sessions defined on their financial
centres' civil clocks (London 08-16 Europe/London, NY 08-17
America/New_York, Tokyo 09-18 Asia/Tokyo), converted to UTC by each
centre's own computed DST rule (EU last-Sun-Mar/Oct, US
2nd-Sun-Mar/1st-Sun-Nov), then to broker time by the MEASURED
server-vs-GMT offset (half-hour brokers included). Windows may wrap
midnight in broker time - the interval test handles it. Replaces the
EET-hardcoded anchors, which were correct on exactly one broker and
got Tokyo wrong by an hour each European summer.
4. The current-session-table-for-history caveat resolved by analysis:
the bars bound the error - a too-late assumed close meets no bars
(zero error), a too-early one truncates conservatively (<=1h, never
optimistic, cannot manufacture edge). Documented at the site.
5. The ensemble deploy gate mirrors the direction policy: blocked-side
fires are not fired bars (certified == traded), the zero-skill
reference uses only ACHIEVABLE baselines (always-short is not a
strategy a long-only book can run), and one-sidedness BY POLICY is
not degeneracy - the two-sided requirement applies only when both
sides are allowed. Sell predictions keep their other jobs (exit
triggers, consensus dilution) untouched.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 12:14:01 -04:00
//--- The zero-skill reference must be ACHIEVABLE under the direction policy: with shorts
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//--- blocked, always-Sell is not a strategy anyone could run, and ranking the vote against it
refactor(gate): the member gate and the ensemble gate were one rule written twice
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>
2026-08-23 14:19:44 -04:00
//--- would score a long-only book against a baseline the policy forbids. This is one of the two
//--- places the vote genuinely differs from a member - see DeployGate.mqh.
feat(consistency): the five review flaws fixed - training wears the live constraints, the gate wears the policy
1. Labels and the exit simulator go through the broker's stop-distance
check: risk/reward widen to SYMBOL_TRADE_STOPS_LEVEL exactly as
TCAdjustStops does at order time - the M5/tight-ATR case where live
trades ran wider geometry than training measured. Current stops level
stands in for history (like the spread); measured quantity, so it
does not key the fingerprint.
2. The Intelligent drift verdict moved into RefreshDriftVerdict(), which
RESCANS the label cache and now runs at every era end beside
RankTiersFromOos - era-cadence instead of waiting for rare full
rebuilds. Prints only on change.
3. Session filter is any-broker: sessions defined on their financial
centres' civil clocks (London 08-16 Europe/London, NY 08-17
America/New_York, Tokyo 09-18 Asia/Tokyo), converted to UTC by each
centre's own computed DST rule (EU last-Sun-Mar/Oct, US
2nd-Sun-Mar/1st-Sun-Nov), then to broker time by the MEASURED
server-vs-GMT offset (half-hour brokers included). Windows may wrap
midnight in broker time - the interval test handles it. Replaces the
EET-hardcoded anchors, which were correct on exactly one broker and
got Tokyo wrong by an hour each European summer.
4. The current-session-table-for-history caveat resolved by analysis:
the bars bound the error - a too-late assumed close meets no bars
(zero error), a too-early one truncates conservatively (<=1h, never
optimistic, cannot manufacture edge). Documented at the site.
5. The ensemble deploy gate mirrors the direction policy: blocked-side
fires are not fired bars (certified == traded), the zero-skill
reference uses only ACHIEVABLE baselines (always-short is not a
strategy a long-only book can run), and one-sidedness BY POLICY is
not degeneracy - the two-sided requirement applies only when both
sides are allowed. Sell predictions keep their other jobs (exit
triggers, consensus dilution) untouched.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 12:14:01 -04:00
double chancePct = -1.0 ;
if ( measurable )
{
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
double chanceL = 100.0 * labelBuyBars / shared ;
double chanceS = 100.0 * labelSellBars / shared ;
feat(consistency): the five review flaws fixed - training wears the live constraints, the gate wears the policy
1. Labels and the exit simulator go through the broker's stop-distance
check: risk/reward widen to SYMBOL_TRADE_STOPS_LEVEL exactly as
TCAdjustStops does at order time - the M5/tight-ATR case where live
trades ran wider geometry than training measured. Current stops level
stands in for history (like the spread); measured quantity, so it
does not key the fingerprint.
2. The Intelligent drift verdict moved into RefreshDriftVerdict(), which
RESCANS the label cache and now runs at every era end beside
RankTiersFromOos - era-cadence instead of waiting for rare full
rebuilds. Prints only on change.
3. Session filter is any-broker: sessions defined on their financial
centres' civil clocks (London 08-16 Europe/London, NY 08-17
America/New_York, Tokyo 09-18 Asia/Tokyo), converted to UTC by each
centre's own computed DST rule (EU last-Sun-Mar/Oct, US
2nd-Sun-Mar/1st-Sun-Nov), then to broker time by the MEASURED
server-vs-GMT offset (half-hour brokers included). Windows may wrap
midnight in broker time - the interval test handles it. Replaces the
EET-hardcoded anchors, which were correct on exactly one broker and
got Tokyo wrong by an hour each European summer.
4. The current-session-table-for-history caveat resolved by analysis:
the bars bound the error - a too-late assumed close meets no bars
(zero error), a too-early one truncates conservatively (<=1h, never
optimistic, cannot manufacture edge). Documented at the site.
5. The ensemble deploy gate mirrors the direction policy: blocked-side
fires are not fired bars (certified == traded), the zero-skill
reference uses only ACHIEVABLE baselines (always-short is not a
strategy a long-only book can run), and one-sidedness BY POLICY is
not degeneracy - the two-sided requirement applies only when both
sides are allowed. Sell predictions keep their other jobs (exit
triggers, consensus dilution) untouched.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 12:14:01 -04:00
bool allowL = WarriorDirectionAllows ( true ) ;
bool allowS = WarriorDirectionAllows ( false ) ;
chancePct = ( allowL & & allowS ) ? MathMax ( chanceL , chanceS )
: ( allowL ? chanceL : ( allowS ? chanceS : MathMax ( chanceL , chanceS ) ) ) ;
}
refactor(gate): the member gate and the ensemble gate were one rule written twice
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>
2026-08-23 14:19:44 -04:00
//--- The other genuine difference: the vote's anti-degenerate test reads whether it actually
//--- FIRED both ways, where a member reads its per-side recalls.
feat(consistency): the five review flaws fixed - training wears the live constraints, the gate wears the policy
1. Labels and the exit simulator go through the broker's stop-distance
check: risk/reward widen to SYMBOL_TRADE_STOPS_LEVEL exactly as
TCAdjustStops does at order time - the M5/tight-ATR case where live
trades ran wider geometry than training measured. Current stops level
stands in for history (like the spread); measured quantity, so it
does not key the fingerprint.
2. The Intelligent drift verdict moved into RefreshDriftVerdict(), which
RESCANS the label cache and now runs at every era end beside
RankTiersFromOos - era-cadence instead of waiting for rare full
rebuilds. Prints only on change.
3. Session filter is any-broker: sessions defined on their financial
centres' civil clocks (London 08-16 Europe/London, NY 08-17
America/New_York, Tokyo 09-18 Asia/Tokyo), converted to UTC by each
centre's own computed DST rule (EU last-Sun-Mar/Oct, US
2nd-Sun-Mar/1st-Sun-Nov), then to broker time by the MEASURED
server-vs-GMT offset (half-hour brokers included). Windows may wrap
midnight in broker time - the interval test handles it. Replaces the
EET-hardcoded anchors, which were correct on exactly one broker and
got Tokyo wrong by an hour each European summer.
4. The current-session-table-for-history caveat resolved by analysis:
the bars bound the error - a too-late assumed close meets no bars
(zero error), a too-early one truncates conservatively (<=1h, never
optimistic, cannot manufacture edge). Documented at the site.
5. The ensemble deploy gate mirrors the direction policy: blocked-side
fires are not fired bars (certified == traded), the zero-skill
reference uses only ACHIEVABLE baselines (always-short is not a
strategy a long-only book can run), and one-sidedness BY POLICY is
not degeneracy - the two-sided requirement applies only when both
sides are allowed. Sell predictions keep their other jobs (exit
triggers, consensus dilution) untouched.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 12:14:01 -04:00
bool bothAllowed = ( WarriorDirectionAllows ( true ) & & WarriorDirectionAllows ( false ) ) ;
feat(vote): derive the threshold instead of configuring it
Signal_ThresholdOpen becomes a seed. The era verdict now picks the HIGHEST
sweep rung whose vote still clears the whole deploy gate - coverage floor,
exact-binomial precision bar and two-sidedness together - computes the era's
verdict AT that rung, and publishes it to the live signal's m_threshold_open
so the bar the gate certifies is the bar the EA trades.
Measured on 619 era verdicts across all six live charts:
* every era on every symbol had at least one rung clearing the full gate.
At the fixed 25% the fleet was actually running, four of six symbols had
none, ever. The threshold, not the models, was the blocker.
* walk-forward (rung derived on era N, scored on era N+1): 10.2% coverage /
31.8% precision, against an oracle re-picking on N+1 of 10.3% / 31.7%.
Near-zero shrinkage - a measurement, not a fit. It holds because the
binding constraint is COVERAGE, a near-deterministic step function of the
vote distribution, not precision.
* vs a fixed 15% (best global value): +0.6pp precision, 3.4pp less coverage.
vs a fixed 20%: deployable on all six rather than four of six.
Selection on the highest PASSING rung, never on the best-precision rung - that
is a best-of-6 on a noisy statistic and this project has crowned noise that way
four times. The multiplicity that remains is paid for: nTried in
EnsembleSurvivesSelection is now eras x rungs. Costs nothing - all six charts
clear it by 6.5-12 sigma even forming z on effective rather than raw calls.
Also fixes, in the same path: the direction-policy gate is hoisted above the
per-rung tally so every rung is scored on the population the gate certifies.
Retrain-neutral: not in BuildModelFingerprint(), no .nnw re-keyed.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:02:58 -04:00
//==================================================================================================
// THE DERIVED THRESHOLD
//==================================================================================================
//--- THE RULE: the HIGHEST rung whose vote still clears the WHOLE deploy gate - coverage floor,
//--- exact-binomial precision bar and two-sidedness together. Not the rung with the best
//--- precision. That distinction is the entire safety argument and must not be "improved" away:
//---
//--- Picking the best-PRECISION rung is a best-of-6 on a noisy statistic, and this project has
//--- already crowned noise that way four separate times ([[project_family_wise_gate_rule]]).
//--- Picking the highest rung that PASSES orders the candidates by a fixed, data-independent
//--- key (the threshold itself) and asks one pass/fail question per rung. The multiplicity is
//--- real but bounded and known, so it is PAID FOR below rather than ignored: nTried in
//--- EnsembleSurvivesSelection() is now eras x rungs, not eras.
//---
//--- MEASURED, on 619 era verdicts across all six live charts (2026-08-26 overnight run):
//--- * every era on every symbol had at least one rung that cleared the full gate. At a FIXED
//--- 25% - what the fleet actually ran - four of six symbols had none, ever. The threshold,
//--- not the models, was the entire reason nothing deployed.
//--- * WALK-FORWARD (rung derived on era N, then scored on era N+1) lands at 10.2% coverage /
//--- 31.8% precision against an ORACLE that re-picks on era N+1 itself of 10.3% / 31.7%.
//--- Near-zero shrinkage, which is what says this is a measurement and not a fit. It holds
//--- because the binding constraint is COVERAGE - a near-deterministic step function of the
//--- vote distribution - and not precision.
//--- * against a fixed 15% (the best single global value): +0.6pp precision for 3.4pp less
//--- coverage. Against a fixed 20%: deployable on all six instead of four of six.
//---
//--- WHY THIS IS AN OUTPUT AND NOT AN INPUT. The era verdict below is computed AT this rung, so
//--- the threshold is part of what gets certified rather than a knob applied afterwards. That is
//--- also why Warrior_EA.mq5 must push it into the live signal's m_threshold_open: certifying at
//--- one threshold and trading at another is the defect 2c443ba was written to end, and MT5
//--- stores an input PER CHART (profiles\Charts\*\chart*.chr) - so as an INPUT this number
//--- could never be corrected from source at all.
int derivedIdx = -1 , coverageIdx = -1 ;
if ( measurable )
{
for ( int s = ENS_THRESHOLD_SWEEP_N - 1 ; s > = 0 ; s - - )
{
if ( sweepFired [ s ] < = 0 )
continue ;
double swPrec = 100.0 * sweepWins [ s ] / sweepFired [ s ] ;
bool swTwo = bothAllowed ? ( sweepLong [ s ] > 0 & & sweepShort [ s ] > 0 ) : ( sweepFired [ s ] > 0 ) ;
SDeployVerdict rungGate ;
rungGate . EvaluateRates ( sweepFired [ s ] , shared , dirLabelBars , swPrec , chancePct ,
EffectiveSampleSize ( ( double ) sweepFired [ s ] ) , swTwo ) ;
//--- Remembered on the way down so the fallback below can reach for it without a second scan.
//--- FIRST WRITE WINS, and the guard is load-bearing: this loop runs HIGH rung to LOW and
//--- coverage only rises as the threshold falls, so without it every clearing rung would
//--- overwrite the last and the fallback would end up holding the LOWEST clearing rung -
//--- the exact opposite of what it is documented to do.
if ( coverageIdx < 0 & & rungGate . coveragePct > = rungGate . minCoveragePct )
coverageIdx = s ;
if ( rungGate . tradeable )
{
derivedIdx = s ;
break ;
}
}
}
//--- FALLBACK, when no rung clears the gate. Take the highest rung that at least clears COVERAGE,
//--- so the era fails on precision - the informative failure, and the one the refusal text can act
//--- on - rather than on a thin population that inflates its own bar. If not even the lowest rung
//--- covers enough, take the lowest: maximum evidence is the only thing left worth having.
if ( derivedIdx < 0 )
derivedIdx = ( coverageIdx > = 0 ) ? coverageIdx : 0 ;
2026-08-26 16:36:20 -04:00
//--- FOLLOW UNTIL PINNED, PIN THEREAFTER. Once a checkpoint exists the live rung belongs to it and
//--- is set in the isBetter block below. BEFORE that there is nothing to protect, and leaving the
//--- chart on the Signal_ThresholdOpen seed is strictly worse than following the latest era's own
//--- measurement.
//---
//--- IT WAS ALSO A LIVE DEFECT, from combining the pin with the burn-in: no era below
//--- ENSEMBLE_CHECKPOINT_MIN_ERA may checkpoint, so nothing published until era 20 and the chart
//--- sat on the seed until then. Under the edge-over-chance currency that seed (25, an ABSOLUTE
//--- win rate) is above what the vote can even reach - USDJPY's strongest vote was 19.3% against
//--- it - so those charts drew ZERO arrows and would have placed zero trades. Reported as "all 3
//--- forex charts are visibly quiet" while their OOS coverage read 17-18%.
if ( measurable & & g_ensBestEra < 0 )
{
g_ensDerivedThreshold = g_ensThresholdSweep [ derivedIdx ] ;
g_ensembleVoteThreshold = g_ensDerivedThreshold ;
}
//--- Every era derives its own rung - that is how the best one is found - but once a checkpoint
//--- exists the rung the LIVE SIGNAL trades is PINNED to the checkpointed era's, in the isBetter
feat(vote): exit-on-reversal boolean, pin the threshold, retry the atomic rename
THE EXIT KNOB. Exit_On_Reversal_Vote (default false) replaces the deleted
Signal_ThresholdClose with one boolean: false pins the close threshold to an
arithmetically unreachable 101, true pins it to the SAME threshold the entry
uses - the seed at first, then the derived value, republished together whenever
it moves. A second threshold was always redundant; "the bot now says the other
way" is one question.
It also arms CExpertSignalCustom::m_holdToBarrier, which was DEAD CODE:
HoldToBarrier(bool) had no caller anywhere in the build, so the flag had been
permanently false and the disabled close threshold was carrying the whole
hold-to-barrier policy alone. Both halves now move together.
Default stays false because the reason is statistical: the gate certifies
P(label agrees | vote fired) against a label that runs to the barrier, so an
early close trades something never measured. Turning it on is a different
strategy, not a tightening of this one.
THE PIN. The live threshold now moves only when an era's weights become the
checkpoint, and freezes once g_ensDeployApproved. Every era still derives its own
rung - that is how the best one is found - but the rung that TRADES belongs to
the checkpoint, exactly as the weights do. Two reasons, one measured and one
structural: the per-era rung moves on 6-34% of steps (the live run flapped
SP500 15 -> 10 -> 15 within a minute of starting), and without the pin a later
era's rung could end up applied to an earlier era's deployed model. A ladder
restart releases the pin, since clearing the checkpoint clears what it pinned.
The era line now prints the rung its own numbers came from, so it stays honest
when that differs from the pinned one.
THE ATOMIC RENAME retried zero times. Six charts share the TrainPool and AltData
directories, so a publish regularly lands while a peer chart holds the
destination open and FileMove returns 5004 - 27 times in one day on the live
fleet. Nothing was lost (the temp keeps the new content, the old file stays
intact) but the row did not update until the next publish. Now four attempts at
25ms, on the FAILURE PATH ONLY - a successful rename never sleeps - and skipped
in the tester, where the contention cannot happen and Sleep would distort a pass.
A rescued retry is logged, so worsening contention is visible.
Retrain-neutral. Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:53:56 -04:00
//--- block. Measured over 619 eras, the per-era rung moves on 6-34% of steps (always by one rung);
//--- publishing each era's would make the deployed operating point chase noise between eras that
//--- were never chosen, and the live run showed exactly that within a minute of starting
//--- (SP500 15 -> 10 -> 15). The threshold belongs to the CHECKPOINT, like the weights do.
feat(vote): derive the threshold instead of configuring it
Signal_ThresholdOpen becomes a seed. The era verdict now picks the HIGHEST
sweep rung whose vote still clears the whole deploy gate - coverage floor,
exact-binomial precision bar and two-sidedness together - computes the era's
verdict AT that rung, and publishes it to the live signal's m_threshold_open
so the bar the gate certifies is the bar the EA trades.
Measured on 619 era verdicts across all six live charts:
* every era on every symbol had at least one rung clearing the full gate.
At the fixed 25% the fleet was actually running, four of six symbols had
none, ever. The threshold, not the models, was the blocker.
* walk-forward (rung derived on era N, scored on era N+1): 10.2% coverage /
31.8% precision, against an oracle re-picking on N+1 of 10.3% / 31.7%.
Near-zero shrinkage - a measurement, not a fit. It holds because the
binding constraint is COVERAGE, a near-deterministic step function of the
vote distribution, not precision.
* vs a fixed 15% (best global value): +0.6pp precision, 3.4pp less coverage.
vs a fixed 20%: deployable on all six rather than four of six.
Selection on the highest PASSING rung, never on the best-precision rung - that
is a best-of-6 on a noisy statistic and this project has crowned noise that way
four times. The multiplicity that remains is paid for: nTried in
EnsembleSurvivesSelection is now eras x rungs. Costs nothing - all six charts
clear it by 6.5-12 sigma even forming z on effective rather than raw calls.
Also fixes, in the same path: the direction-policy gate is hoisted above the
per-rung tally so every rung is scored on the population the gate certifies.
Retrain-neutral: not in BuildModelFingerprint(), no .nnw re-keyed.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:02:58 -04:00
fired = sweepFired [ derivedIdx ] ;
wins = sweepWins [ derivedIdx ] ;
firedLong = sweepLong [ derivedIdx ] ;
firedShort = sweepShort [ derivedIdx ] ;
double votePrecPct = ( fired > 0 ) ? 100.0 * wins / fired : -1.0 ;
bool twoSided = bothAllowed ? ( firedLong > 0 & & firedShort > 0 ) : ( fired > 0 ) ;
refactor(gate): the member gate and the ensemble gate were one rule written twice
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>
2026-08-23 14:19:44 -04:00
//--- THE SAME ARITHMETIC THE MEMBER GATE RUNS, on the vote's population. EFFECTIVE sample and
//--- not the raw fire count: the vote's outcomes are overlapping triple-barrier labels exactly
//--- as a member's are, and the two gates applying different corrections is precisely how the
//--- ensemble becomes the easier one to clear.
2026-08-23 14:40:29 -04:00
SDeployVerdict voteGate ;
voteGate . EvaluateRates ( fired , shared , dirLabelBars , votePrecPct , chancePct ,
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
EffectiveSampleSize ( ( double ) fired ) , twoSided ) ;
2026-08-23 14:40:29 -04:00
double coveragePct = voteGate . coveragePct ;
double minCoverPct = voteGate . minCoveragePct ;
double edgeFloorPct = voteGate . edgeFloorPct ;
bool tradeableOK = voteGate . tradeable ;
double score = voteGate . selectionScore ;
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
//--- N for the family-wise correction: every era that COULD have won, mirroring the member gate's
//--- exclusion of eras with nothing to trade.
2026-08-23 14:40:29 -04:00
bool degenerate = voteGate . degenerate ;
feat(vote): edge-over-chance currency, no-skill exclusion, checkpoint burn-in
RETRAIN-FORCING and deliberately so. Two independent fixes for the same symptom -
charts that go quiet while others overtrade.
1. THE VOTE CURRENCY IS NOW EDGE OVER CHANCE, not an absolute win rate.
A tier weight is a raw win rate and a raw win rate means nothing without the
chance rate behind it: 30% is strong under a 14% base rate and catastrophic under
50%, yet both entered the mean as "30". That is why the threshold needed
re-tuning every time the label changed - 25 was permissive at ~70% win rates
under the old direction label and a near-unanimity rule at ~30% under the
pivot-event one - and why one chart's 25% was never the same statement as
another's. Subtracting the member's own chance rate makes the units percentage
points of demonstrated edge, comparable across charts, labels and regimes.
Clamped at zero: a below-chance tier is anti-informative, and contributing
negatively would act on a broken model as an inverted oracle rather than
discarding it.
2. A NO-SKILL MEMBER IS NOW ABSENT, NOT ABSTAINING.
Measured on XTIUSD: a Perceptron collapsed to B97/S6/N3, pooled win rate 11.5%
against a 14% chance rate - worse than guessing - and still voting. Three healthy
members voting Sell scored -21.06/0.77 = -27.4 and cleared; with the dead one
voting Buy it became (-21.06+1.44)/0.89 = -22.0 and was BLOCKED. It vetoed its
own ensemble on ~95% of bars, and that WAS the chart's 3.3% coverage. Neither
existing guard caught it: it IS self-ranked and its tier weights were 11-14.
The fix has to remove it from the DIVISOR, not just the sum - an abstainer
contributes weight by design, so zeroing only the contribution makes the dilution
worse. VoteCapableWeight() already means exactly "may this member's weight sit in
the denominator", so the skill test belongs there. ReconstructionWeight() and the
OOS scorer's divisor move with it or the scorer certifies a vote live does not
cast. The skill test reads the PREVIOUS era's measurement - gating this era's
vote on this era's own outcome would be circular.
3. CHECKPOINT BURN-IN (ENSEMBLE_CHECKPOINT_MIN_ERA 20).
XAUUSD deployed the checkpoint from ERA 2, XTIUSD from ERA 4, each after 69 and
65 further eras failed to beat it. Ensemble coverage measures AGREEMENT, and four
models that have barely moved off their initialisation agree almost by
construction - so coverage is inflated exactly when the models know least and
decays as they differentiate (XAUUSD 6.6% at era 8 -> 0.4% at era 75). Since
selectionScore is precision discounted by coverage, an early era outscores every
mature one and the ladder freezes on it.
INTENDED CONSEQUENCE: a chart whose MATURE coverage cannot clear the floor now
refuses to deploy rather than shipping era-2 weights. Fewer deploys, honest ones.
Burn-in eras are also kept out of g_ensCandidateEras (they could not have won, so
counting them inflates the family-wise N and raises the bar for nothing) and out
of g_ensErasSinceBest (or the run reaches "no better vote for N eras" with no
best to beat, exhausting the escalation ladder before the first era may compete).
Every pinned threshold and .stats record is in the OLD currency and is now
meaningless - this forces a fresh start on its own. Nothing needs re-tuning
because the threshold is DERIVED: the sweep re-picks the rung by itself.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 15:38:26 -04:00
//--- A BURN-IN ERA IS NOT A CANDIDATE. g_ensCandidateEras is the N the family-wise correction
//--- divides by - "how many eras could have won". An era that is barred from taking the checkpoint
//--- could not have won, so counting it would inflate N and RAISE the deploy bar for no reason.
if ( measurable & & ! degenerate & & votedEra > = ENSEMBLE_CHECKPOINT_MIN_ERA )
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
g_ensCandidateEras + + ;
//--- Same lexicographic ordering as isBetterEra: deployable outranks two-sided outranks score.
fix(training): a new best must beat the noise; the blank-chart census must name its cause
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>
2026-08-24 15:58:34 -04:00
//--- The score comparison carries a NOISE BAND - see PLATEAU_NEW_BEST_SIGMAS for why a bare
//--- `>` here is what lets a run spend thousands of eras without ever reaching the deploy stage.
//--- The first scoring era still takes the checkpoint unconditionally (nothing to beat yet).
feat(deploy): ship on positive EXPECTANCY, and let the chart draw before convergence
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>
2026-08-24 16:15:06 -04:00
double newBestBand = ( g_ensBestScore > = 0.0 ) ? PLATEAU_NEW_BEST_SIGMAS * voteGate . scoreSE : 0.0 ;
feat(vote): edge-over-chance currency, no-skill exclusion, checkpoint burn-in
RETRAIN-FORCING and deliberately so. Two independent fixes for the same symptom -
charts that go quiet while others overtrade.
1. THE VOTE CURRENCY IS NOW EDGE OVER CHANCE, not an absolute win rate.
A tier weight is a raw win rate and a raw win rate means nothing without the
chance rate behind it: 30% is strong under a 14% base rate and catastrophic under
50%, yet both entered the mean as "30". That is why the threshold needed
re-tuning every time the label changed - 25 was permissive at ~70% win rates
under the old direction label and a near-unanimity rule at ~30% under the
pivot-event one - and why one chart's 25% was never the same statement as
another's. Subtracting the member's own chance rate makes the units percentage
points of demonstrated edge, comparable across charts, labels and regimes.
Clamped at zero: a below-chance tier is anti-informative, and contributing
negatively would act on a broken model as an inverted oracle rather than
discarding it.
2. A NO-SKILL MEMBER IS NOW ABSENT, NOT ABSTAINING.
Measured on XTIUSD: a Perceptron collapsed to B97/S6/N3, pooled win rate 11.5%
against a 14% chance rate - worse than guessing - and still voting. Three healthy
members voting Sell scored -21.06/0.77 = -27.4 and cleared; with the dead one
voting Buy it became (-21.06+1.44)/0.89 = -22.0 and was BLOCKED. It vetoed its
own ensemble on ~95% of bars, and that WAS the chart's 3.3% coverage. Neither
existing guard caught it: it IS self-ranked and its tier weights were 11-14.
The fix has to remove it from the DIVISOR, not just the sum - an abstainer
contributes weight by design, so zeroing only the contribution makes the dilution
worse. VoteCapableWeight() already means exactly "may this member's weight sit in
the denominator", so the skill test belongs there. ReconstructionWeight() and the
OOS scorer's divisor move with it or the scorer certifies a vote live does not
cast. The skill test reads the PREVIOUS era's measurement - gating this era's
vote on this era's own outcome would be circular.
3. CHECKPOINT BURN-IN (ENSEMBLE_CHECKPOINT_MIN_ERA 20).
XAUUSD deployed the checkpoint from ERA 2, XTIUSD from ERA 4, each after 69 and
65 further eras failed to beat it. Ensemble coverage measures AGREEMENT, and four
models that have barely moved off their initialisation agree almost by
construction - so coverage is inflated exactly when the models know least and
decays as they differentiate (XAUUSD 6.6% at era 8 -> 0.4% at era 75). Since
selectionScore is precision discounted by coverage, an early era outscores every
mature one and the ladder freezes on it.
INTENDED CONSEQUENCE: a chart whose MATURE coverage cannot clear the floor now
refuses to deploy rather than shipping era-2 weights. Fewer deploys, honest ones.
Burn-in eras are also kept out of g_ensCandidateEras (they could not have won, so
counting them inflates the family-wise N and raises the bar for nothing) and out
of g_ensErasSinceBest (or the run reaches "no better vote for N eras" with no
best to beat, exhausting the escalation ladder before the first era may compete).
Every pinned threshold and .stats record is in the OLD currency and is now
meaningless - this forces a fresh start on its own. Nothing needs re-tuning
because the threshold is DERIVED: the sweep re-picks the rung by itself.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 15:38:26 -04:00
//--- BURN-IN. An era below ENSEMBLE_CHECKPOINT_MIN_ERA is scored and reported like any other but
//--- cannot take the checkpoint - see that constant for the era-2 deploys this prevents. Note this
//--- also gates the "first scoring era takes it unconditionally" path: the first era that may take
//--- the checkpoint is the first MATURE one, not the first one to produce a number.
bool mayCheckpoint = ( votedEra > = ENSEMBLE_CHECKPOINT_MIN_ERA ) ;
bool isBetter = mayCheckpoint & &
( ( tradeableOK & & ! g_ensBestTradeable ) | |
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
( tradeableOK = = g_ensBestTradeable & & twoSided & & ! g_ensBestTwoSided ) | |
feat(vote): edge-over-chance currency, no-skill exclusion, checkpoint burn-in
RETRAIN-FORCING and deliberately so. Two independent fixes for the same symptom -
charts that go quiet while others overtrade.
1. THE VOTE CURRENCY IS NOW EDGE OVER CHANCE, not an absolute win rate.
A tier weight is a raw win rate and a raw win rate means nothing without the
chance rate behind it: 30% is strong under a 14% base rate and catastrophic under
50%, yet both entered the mean as "30". That is why the threshold needed
re-tuning every time the label changed - 25 was permissive at ~70% win rates
under the old direction label and a near-unanimity rule at ~30% under the
pivot-event one - and why one chart's 25% was never the same statement as
another's. Subtracting the member's own chance rate makes the units percentage
points of demonstrated edge, comparable across charts, labels and regimes.
Clamped at zero: a below-chance tier is anti-informative, and contributing
negatively would act on a broken model as an inverted oracle rather than
discarding it.
2. A NO-SKILL MEMBER IS NOW ABSENT, NOT ABSTAINING.
Measured on XTIUSD: a Perceptron collapsed to B97/S6/N3, pooled win rate 11.5%
against a 14% chance rate - worse than guessing - and still voting. Three healthy
members voting Sell scored -21.06/0.77 = -27.4 and cleared; with the dead one
voting Buy it became (-21.06+1.44)/0.89 = -22.0 and was BLOCKED. It vetoed its
own ensemble on ~95% of bars, and that WAS the chart's 3.3% coverage. Neither
existing guard caught it: it IS self-ranked and its tier weights were 11-14.
The fix has to remove it from the DIVISOR, not just the sum - an abstainer
contributes weight by design, so zeroing only the contribution makes the dilution
worse. VoteCapableWeight() already means exactly "may this member's weight sit in
the denominator", so the skill test belongs there. ReconstructionWeight() and the
OOS scorer's divisor move with it or the scorer certifies a vote live does not
cast. The skill test reads the PREVIOUS era's measurement - gating this era's
vote on this era's own outcome would be circular.
3. CHECKPOINT BURN-IN (ENSEMBLE_CHECKPOINT_MIN_ERA 20).
XAUUSD deployed the checkpoint from ERA 2, XTIUSD from ERA 4, each after 69 and
65 further eras failed to beat it. Ensemble coverage measures AGREEMENT, and four
models that have barely moved off their initialisation agree almost by
construction - so coverage is inflated exactly when the models know least and
decays as they differentiate (XAUUSD 6.6% at era 8 -> 0.4% at era 75). Since
selectionScore is precision discounted by coverage, an early era outscores every
mature one and the ladder freezes on it.
INTENDED CONSEQUENCE: a chart whose MATURE coverage cannot clear the floor now
refuses to deploy rather than shipping era-2 weights. Fewer deploys, honest ones.
Burn-in eras are also kept out of g_ensCandidateEras (they could not have won, so
counting them inflates the family-wise N and raises the bar for nothing) and out
of g_ensErasSinceBest (or the run reaches "no better vote for N eras" with no
best to beat, exhausting the escalation ladder before the first era may compete).
Every pinned threshold and .stats record is in the OLD currency and is now
meaningless - this forces a fresh start on its own. Nothing needs re-tuning
because the threshold is DERIVED: the sweep re-picks the rung by itself.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 15:38:26 -04:00
( tradeableOK = = g_ensBestTradeable & & twoSided = = g_ensBestTwoSided & &
! degenerate & & score > g_ensBestScore + newBestBand ) ) ;
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
if ( isBetter )
{
g_ensBestScore = score ;
g_ensBestTradeable = tradeableOK ;
g_ensBestTwoSided = twoSided ;
g_ensBestPrecPct = votePrecPct ;
g_ensBestChancePct = chancePct ;
g_ensBestCalls = fired ;
g_ensBestEra = votedEra ;
2026-08-26 01:14:51 -04:00
//--- Kept with the rest so the stage-3 refusal can name the condition that failed instead of
//--- listing all three - see their declaration comment.
g_ensBestCoveragePct = coveragePct ;
g_ensBestMinCoverPct = minCoverPct ;
g_ensBestEdgeFloorPct = edgeFloorPct ;
feat(vote): exit-on-reversal boolean, pin the threshold, retry the atomic rename
THE EXIT KNOB. Exit_On_Reversal_Vote (default false) replaces the deleted
Signal_ThresholdClose with one boolean: false pins the close threshold to an
arithmetically unreachable 101, true pins it to the SAME threshold the entry
uses - the seed at first, then the derived value, republished together whenever
it moves. A second threshold was always redundant; "the bot now says the other
way" is one question.
It also arms CExpertSignalCustom::m_holdToBarrier, which was DEAD CODE:
HoldToBarrier(bool) had no caller anywhere in the build, so the flag had been
permanently false and the disabled close threshold was carrying the whole
hold-to-barrier policy alone. Both halves now move together.
Default stays false because the reason is statistical: the gate certifies
P(label agrees | vote fired) against a label that runs to the barrier, so an
early close trades something never measured. Turning it on is a different
strategy, not a tightening of this one.
THE PIN. The live threshold now moves only when an era's weights become the
checkpoint, and freezes once g_ensDeployApproved. Every era still derives its own
rung - that is how the best one is found - but the rung that TRADES belongs to
the checkpoint, exactly as the weights do. Two reasons, one measured and one
structural: the per-era rung moves on 6-34% of steps (the live run flapped
SP500 15 -> 10 -> 15 within a minute of starting), and without the pin a later
era's rung could end up applied to an earlier era's deployed model. A ladder
restart releases the pin, since clearing the checkpoint clears what it pinned.
The era line now prints the rung its own numbers came from, so it stays honest
when that differs from the pinned one.
THE ATOMIC RENAME retried zero times. Six charts share the TrainPool and AltData
directories, so a publish regularly lands while a peer chart holds the
destination open and FileMove returns 5004 - 27 times in one day on the live
fleet. Nothing was lost (the temp keeps the new content, the old file stays
intact) but the row did not update until the next publish. Now four attempts at
25ms, on the FAILURE PATH ONLY - a successful rename never sleeps - and skipped
in the tester, where the contention cannot happen and Sleep would distort a pass.
A rescued retry is logged, so worsening contention is visible.
Retrain-neutral. Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:53:56 -04:00
//--- THE PIN. The threshold moves only when this era's weights become the checkpoint, so the
//--- rung that trades is always the one the deployed weights were certified at - never a later
//--- era's rung applied to an earlier era's model.
//--- FROZEN ONCE DEPLOYED. After g_ensDeployApproved the certified configuration is settled and
//--- nothing may move it: a post-deployment change would be an uncertified operating point on a
//--- model that is already trading. See the walks that never ran for what "armed at
//--- convergence" costs when it is not thought through.
if ( ! g_ensDeployApproved )
{
double pinned = g_ensThresholdSweep [ derivedIdx ] ;
if ( MathAbs ( pinned - g_ensDerivedThreshold ) > 0.01 )
PrintFormat ( " AI ensemble: vote threshold PINNED to %.0f%% by the era-%d checkpoint (was %s). "
" It moves again only if a later era takes the checkpoint, and never once the "
" ensemble deploys. " , pinned , ( int ) votedEra ,
( g_ensDerivedThreshold > 0.0 ? StringFormat ( " %.0f%% " , g_ensDerivedThreshold )
: " the Signal_ThresholdOpen seed " ) ) ;
g_ensDerivedThreshold = pinned ;
g_ensembleVoteThreshold = pinned ;
}
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
g_ensErasSinceBest = 0 ;
g_ensPlateauStage = 0 ;
EnsembleCommitJointCheckpoint ( votedEra ) ;
}
else
feat(vote): edge-over-chance currency, no-skill exclusion, checkpoint burn-in
RETRAIN-FORCING and deliberately so. Two independent fixes for the same symptom -
charts that go quiet while others overtrade.
1. THE VOTE CURRENCY IS NOW EDGE OVER CHANCE, not an absolute win rate.
A tier weight is a raw win rate and a raw win rate means nothing without the
chance rate behind it: 30% is strong under a 14% base rate and catastrophic under
50%, yet both entered the mean as "30". That is why the threshold needed
re-tuning every time the label changed - 25 was permissive at ~70% win rates
under the old direction label and a near-unanimity rule at ~30% under the
pivot-event one - and why one chart's 25% was never the same statement as
another's. Subtracting the member's own chance rate makes the units percentage
points of demonstrated edge, comparable across charts, labels and regimes.
Clamped at zero: a below-chance tier is anti-informative, and contributing
negatively would act on a broken model as an inverted oracle rather than
discarding it.
2. A NO-SKILL MEMBER IS NOW ABSENT, NOT ABSTAINING.
Measured on XTIUSD: a Perceptron collapsed to B97/S6/N3, pooled win rate 11.5%
against a 14% chance rate - worse than guessing - and still voting. Three healthy
members voting Sell scored -21.06/0.77 = -27.4 and cleared; with the dead one
voting Buy it became (-21.06+1.44)/0.89 = -22.0 and was BLOCKED. It vetoed its
own ensemble on ~95% of bars, and that WAS the chart's 3.3% coverage. Neither
existing guard caught it: it IS self-ranked and its tier weights were 11-14.
The fix has to remove it from the DIVISOR, not just the sum - an abstainer
contributes weight by design, so zeroing only the contribution makes the dilution
worse. VoteCapableWeight() already means exactly "may this member's weight sit in
the denominator", so the skill test belongs there. ReconstructionWeight() and the
OOS scorer's divisor move with it or the scorer certifies a vote live does not
cast. The skill test reads the PREVIOUS era's measurement - gating this era's
vote on this era's own outcome would be circular.
3. CHECKPOINT BURN-IN (ENSEMBLE_CHECKPOINT_MIN_ERA 20).
XAUUSD deployed the checkpoint from ERA 2, XTIUSD from ERA 4, each after 69 and
65 further eras failed to beat it. Ensemble coverage measures AGREEMENT, and four
models that have barely moved off their initialisation agree almost by
construction - so coverage is inflated exactly when the models know least and
decays as they differentiate (XAUUSD 6.6% at era 8 -> 0.4% at era 75). Since
selectionScore is precision discounted by coverage, an early era outscores every
mature one and the ladder freezes on it.
INTENDED CONSEQUENCE: a chart whose MATURE coverage cannot clear the floor now
refuses to deploy rather than shipping era-2 weights. Fewer deploys, honest ones.
Burn-in eras are also kept out of g_ensCandidateEras (they could not have won, so
counting them inflates the family-wise N and raises the bar for nothing) and out
of g_ensErasSinceBest (or the run reaches "no better vote for N eras" with no
best to beat, exhausting the escalation ladder before the first era may compete).
Every pinned threshold and .stats record is in the OLD currency and is now
meaningless - this forces a fresh start on its own. Nothing needs re-tuning
because the threshold is DERIVED: the sweep re-picks the rung by itself.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 15:38:26 -04:00
//--- ONLY ONCE A CHECKPOINT IS POSSIBLE. This is the plateau counter that drives the escalation
//--- ladder and, at stage 3, the deploy decision. Letting it run through the burn-in would have
//--- the run reach "no better vote for N eras" while there was no best to beat, escalating - and
//--- potentially exhausting the ladder - before the first era was even allowed to compete.
if ( mayCheckpoint )
g_ensErasSinceBest + + ;
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
//--- SHARED PLATEAU LADDER. One counter, one stage, applied to every member at the same era, so
//--- the four nets escalate and finish together instead of drifting into different stages of
2026-08-22 00:25:52 -04:00
//--- different searches.
fix(plateau): the IS-error early stop was inert for every ensemble member
15 hours of training, and the stop that exists to END a run announced itself
1,299 consecutive times without ending anything:
SP500 ConvLSTM IN-SAMPLE ERROR PLATEAU - not improved in 1297 / 1298 / 1299
eras (best 0.2689, now 0.3269) ... era 1396, 1397, 1398
SP500 LSTM 536 eras SP500 CONV 442 eras SP500 PAI 150 eras
XAUUSD HYB 478 eras XAUUSD LSTM 296 eras XAUUSD CONV 366 eras
CAUSE: it wrote its decision into m_plateauStage, and EnsembleEraVerdict mirrors
the shared ladder onto every member - `mm.m_plateauStage = g_ensPlateauStage` -
on EVERY era, purely so each member's status line shows the collective stage. A
display mirror was silently overwriting a decision, so the stop re-armed and
re-fired the next era, forever.
This is the worst possible direction for this particular bug. Every one of those
1,299 eras was scored out of sample and joined the family the deploy gate
corrects over (Sidak, g_ensCandidateEras). The stop's entire purpose is to make
that family SMALLER; instead the run spent fifteen hours raising its own bar.
- m_isErrorPlateaued: a one-way per-member latch, cleared only by a fresh run.
Nothing in the ladder may reset it. The stop condition and the two solo deploy
conditions read the latch, not the mirrored stage.
- The orchestrator combines: EnsembleEraVerdict requires UNANIMITY across
participating members (same participation test the era barrier uses, so an
excluded or finished member cannot veto). One member still learning can still
move the combined vote, and the vote is what the gate certifies.
- Fed in as `dueStage = PLATEAU_STAGE_DEPLOY`, NOT written to g_ensPlateauStage.
The block that actually ends the run sits under `dueStage > g_ensPlateauStage`,
so assigning the stage directly makes that test false and the deploy never
happens - the same inert-write shape as the bug being fixed. Caught before
committing; raising dueStage carries it through the ladder's own path (warm
restarts skipped, family-wise vote test, measurement screen, joint checkpoint)
unchanged.
- g_ensIsPlateauAnnounced: announce once per run, not once per era.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 13:09:26 -04:00
int plateauedMembers = 0 , learningMembers = 0 ;
for ( int pi = 0 ; pi < ArraySize ( g_warriorEnsemble ) ; pi + + )
{
CExpertSignalAIBase * pm = g_warriorEnsemble [ pi ] ;
if ( CheckPointer ( pm ) = = POINTER_INVALID | | pm . m_ensembleIndex < 0 )
continue ;
//--- Same participation test the barrier uses: a member that has finished or been stopped is not
//--- something the rest should wait on, and must not veto the collective stop either.
if ( pm . m_trainingComplete | | pm . m_trainingStopRequested | | ! pm . m_isInitialized | | pm . m_barrierExcluded )
continue ;
if ( pm . m_isErrorPlateaued )
plateauedMembers + + ;
else
learningMembers + + ;
}
bool allIsPlateaued = ( plateauedMembers > 0 & & learningMembers = = 0 ) ;
if ( allIsPlateaued & & ! g_ensIsPlateauAnnounced )
{
g_ensIsPlateauAnnounced = true ;
PrintFormat ( " AI ensemble: EVERY member's IN-SAMPLE error has plateaued (%d participating members). "
" No member is still learning from the data it can see, so more eras cannot find a "
" better vote - they would only enlarge the family the deploy gate corrects over. "
" Ending the search on the joint checkpoint at the next era that does not improve it. "
" This stop never read an out-of-sample number, which is what makes the smaller family "
" legitimate rather than a peek. " , plateauedMembers ) ;
}
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
string ladderNote = " " ;
if ( ! isBetter )
{
int dueStage = g_ensErasSinceBest / TrainPlateauPatienceEras ( ) ;
2026-08-22 00:25:52 -04:00
//--- FED IN AS A DUE STAGE rather than written straight to g_ensPlateauStage, and the
//--- difference is the whole fix: the block that actually ends the run sits under `dueStage >
//--- g_ensPlateauStage`, so assigning the stage directly makes that test FALSE and the deploy
//--- never happens.
fix(gate): the plateau shortcut re-ran the deploy test every era, raising its own bar
User report: 'eras since best' in the ensemble line is always 0 (era 147, best at era 90,
'0 eras ago'). That is a control-flow bug wearing a display symptom.
Once every member's in-sample error had plateaued, the shortcut forced the ladder to its
DEPLOY stage on EVERY era. The failed-gate branch resets the stage to 0 so the ladder can
climb again - so the shortcut raised it, the branch cleared it, forever. Three consequences,
only the first of which was visible:
- g_ensErasSinceBest was reset every era, pinning the counter at 0.
- The stage-1/2 boosted warm restarts were never reached, so the one mechanism that can
un-plateau a stuck member never ran. The models sat at a WORSE error than their best
(0.2408 -> 0.3015 on PAI) with no escape.
- Every repetition ran EnsembleSurvivesSelection against an unchanged best and incremented
the candidate-era count the family-wise correction divides by. The run spent its time
RAISING ITS OWN SIDAK BAR - the same waste as the 2026-08-18 inert IS-error stop, one
layer up, and the reason a gate that needed >47.8% saw its bar climb era after era.
Fix: the shortcut fires ONCE PER BEST-ERA (g_ensGateTestedEra, stamped before the outcome
branches because it is the re-running that inflates the family, pass or fail). A refused
gate now falls back to the normal counter-driven ladder - warm restart, anneal, then a
fresh deploy test - which is the escape the shortcut was skipping.
Also, per user: the signal marks were too small to see. Span doubled (2.6 bar widths, so
the overhang either side of the candle is ~0.8 bars) and both layers thickened - 1px dotted
was invisible on a candle chart at any realistic zoom.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 15:40:52 -04:00
if ( allIsPlateaued & & g_ensGateTestedEra ! = g_ensBestEra )
fix(plateau): the IS-error early stop was inert for every ensemble member
15 hours of training, and the stop that exists to END a run announced itself
1,299 consecutive times without ending anything:
SP500 ConvLSTM IN-SAMPLE ERROR PLATEAU - not improved in 1297 / 1298 / 1299
eras (best 0.2689, now 0.3269) ... era 1396, 1397, 1398
SP500 LSTM 536 eras SP500 CONV 442 eras SP500 PAI 150 eras
XAUUSD HYB 478 eras XAUUSD LSTM 296 eras XAUUSD CONV 366 eras
CAUSE: it wrote its decision into m_plateauStage, and EnsembleEraVerdict mirrors
the shared ladder onto every member - `mm.m_plateauStage = g_ensPlateauStage` -
on EVERY era, purely so each member's status line shows the collective stage. A
display mirror was silently overwriting a decision, so the stop re-armed and
re-fired the next era, forever.
This is the worst possible direction for this particular bug. Every one of those
1,299 eras was scored out of sample and joined the family the deploy gate
corrects over (Sidak, g_ensCandidateEras). The stop's entire purpose is to make
that family SMALLER; instead the run spent fifteen hours raising its own bar.
- m_isErrorPlateaued: a one-way per-member latch, cleared only by a fresh run.
Nothing in the ladder may reset it. The stop condition and the two solo deploy
conditions read the latch, not the mirrored stage.
- The orchestrator combines: EnsembleEraVerdict requires UNANIMITY across
participating members (same participation test the era barrier uses, so an
excluded or finished member cannot veto). One member still learning can still
move the combined vote, and the vote is what the gate certifies.
- Fed in as `dueStage = PLATEAU_STAGE_DEPLOY`, NOT written to g_ensPlateauStage.
The block that actually ends the run sits under `dueStage > g_ensPlateauStage`,
so assigning the stage directly makes that test false and the deploy never
happens - the same inert-write shape as the bug being fixed. Caught before
committing; raising dueStage carries it through the ladder's own path (warm
restarts skipped, family-wise vote test, measurement screen, joint checkpoint)
unchanged.
- g_ensIsPlateauAnnounced: announce once per run, not once per era.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 13:09:26 -04:00
dueStage = PLATEAU_STAGE_DEPLOY ;
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
if ( dueStage > g_ensPlateauStage )
{
g_ensPlateauStage = dueStage ;
if ( g_ensPlateauStage = = PLATEAU_STAGE_RESTART | | g_ensPlateauStage = = PLATEAU_STAGE_ANNEAL )
{
for ( int i = 0 ; i < ArraySize ( g_warriorEnsemble ) ; i + + )
{
CExpertSignalAIBase * mm = g_warriorEnsemble [ i ] ;
if ( CheckPointer ( mm ) = = POINTER_INVALID | | mm . m_ensembleIndex < 0 )
continue ;
if ( mm . m_trainingComplete | | mm . m_trainingStopRequested | | ! mm . m_isInitialized )
continue ;
mm . m_modelEta = mm . m_etaCeiling * PLATEAU_RESTART_BOOST ;
mm . m_restartBoostErasLeft = TrainPlateauPatienceEras ( ) ;
mm . m_plateauStage = g_ensPlateauStage ;
if ( CheckPointer ( mm . Net ) ! = POINTER_INVALID )
mm . Net . ResetOptimizerState ( ) ;
2026-08-20 07:00:09 -04:00
//--- THIS member is the one still inside Train(), holding g_eta in a local that would
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
//--- overwrite m_modelEta on the way out - so its restart has to reach the local too.
if ( mm = = GetPointer ( this ) )
etaLocal = mm . m_modelEta ;
}
ladderNote = StringFormat ( " | PLATEAU stage %d: %d eras with no better vote - boosted warm "
" restart on all %d models (learning rate x%.1f, optimizer momentum "
" reset). The joint checkpoint is safe. " ,
g_ensPlateauStage , g_ensErasSinceBest , members , PLATEAU_RESTART_BOOST ) ;
}
else
if ( g_ensPlateauStage > = PLATEAU_STAGE_DEPLOY )
{
//--- EXHAUSTED. Both escapes tried, nothing better found: this is the best vote this
//--- ensemble reaches. Now the gate that matters - has the best-of-N vote survived
2026-08-22 00:25:52 -04:00
//--- having been chosen?
fix(gate): the plateau shortcut re-ran the deploy test every era, raising its own bar
User report: 'eras since best' in the ensemble line is always 0 (era 147, best at era 90,
'0 eras ago'). That is a control-flow bug wearing a display symptom.
Once every member's in-sample error had plateaued, the shortcut forced the ladder to its
DEPLOY stage on EVERY era. The failed-gate branch resets the stage to 0 so the ladder can
climb again - so the shortcut raised it, the branch cleared it, forever. Three consequences,
only the first of which was visible:
- g_ensErasSinceBest was reset every era, pinning the counter at 0.
- The stage-1/2 boosted warm restarts were never reached, so the one mechanism that can
un-plateau a stuck member never ran. The models sat at a WORSE error than their best
(0.2408 -> 0.3015 on PAI) with no escape.
- Every repetition ran EnsembleSurvivesSelection against an unchanged best and incremented
the candidate-era count the family-wise correction divides by. The run spent its time
RAISING ITS OWN SIDAK BAR - the same waste as the 2026-08-18 inert IS-error stop, one
layer up, and the reason a gate that needed >47.8% saw its bar climb era after era.
Fix: the shortcut fires ONCE PER BEST-ERA (g_ensGateTestedEra, stamped before the outcome
branches because it is the re-running that inflates the family, pass or fail). A refused
gate now falls back to the normal counter-driven ladder - warm restart, anneal, then a
fresh deploy test - which is the escape the shortcut was skipping.
Also, per user: the signal marks were too small to see. Span doubled (2.6 bar widths, so
the overhang either side of the candle is ~0.8 bars) and both layers thickened - 1px dotted
was invisible on a candle chart at any realistic zoom.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 15:40:52 -04:00
g_ensGateTestedEra = g_ensBestEra ;
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
double zBest = 0.0 , pFam = 1.0 ;
int nTried = 0 ;
bool survives = EnsembleSurvivesSelection ( zBest , pFam , nTried ) ;
bool haveJoint = true ;
for ( int i = 0 ; i < ArraySize ( g_warriorEnsemble ) ; i + + )
{
CExpertSignalAIBase * mm = g_warriorEnsemble [ i ] ;
if ( CheckPointer ( mm ) = = POINTER_INVALID | | mm . m_ensembleIndex < 0 )
continue ;
if ( mm . m_trainingComplete | | mm . m_trainingStopRequested | | mm . m_trainingPaused | | ! mm . m_isInitialized )
continue ;
//--- Era-stamped, not just present: a snapshot from an EARLIER era would make the
//--- deployed quartet one that was never measured together (see m_checkpointEra).
if ( ! mm . m_haveOosCheckpoint | | mm . m_checkpointEra ! = g_ensBestEra )
haveJoint = false ;
}
string testNote = StringFormat ( " best-of-%d test on the VOTE: edge %.1fpp (%.1f%% vs chance "
" %.1f%%) on %d fired bars = %.2f sigma, family-wise p=%.4f "
" (need <=%.2f) " ,
nTried , g_ensBestPrecPct - g_ensBestChancePct , g_ensBestPrecPct ,
g_ensBestChancePct , g_ensBestCalls , zBest , pFam ,
DEPLOY_FAMILY_WISE_ALPHA ) ;
2026-08-24 21:01:08 -04:00
if ( g_ensBestTradeable & & haveJoint & & survives )
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
{
g_ensDeployApproved = true ;
Print ( " AI ensemble: PLATEAU stage " + IntegerToString ( PLATEAU_STAGE_DEPLOY ) +
" - no better vote for " + IntegerToString ( g_ensErasSinceBest ) + " eras across " +
IntegerToString ( PLATEAU_STAGE_DEPLOY - 1 ) + " warm restarts. " + testNote +
" - CLEARS. Deploying the JOINT checkpoint from era " +
IntegerToString ( ( int ) g_ensBestEra ) + " : every model reverts to the weights it held "
" at the era whose combined vote scored best, so the ensemble that trades is "
" exactly the one that was measured. " ) ;
ladderNote = " | ENSEMBLE DEPLOY APPROVED " ;
}
else
{
//--- Restart the ladder and keep training, exactly as the solo gate does on a
2026-08-22 00:25:52 -04:00
//--- failed selection test. The era cap stays the backstop. THROTTLED
//--- (2026-08-19): the refusal repeated ~450x/day with an unchanged reason.
feat(logs): throttle the settled per-era diagnostics - measured 22MB/9.5h of confirmed-working systems
Measured from the journal (2026-08-19): the era deep-dive line (~2KB) plus
the excursion verdict, tier re-rank, calibration move, barrier hold and
selection-regressed note each printed EVERY era for EVERY member - ~940
eras/member/day - long after the systems they watch were confirmed
working. Yesterday's file was 1.3GB (70% of it the news-filter calendar
spam the sweep fix already removed).
VerboseMode returns as an INPUT (demoted 2026-08-01 for the marketplace;
that track is dead since the 2026-08-16 pivot) and gains a second job:
false throttles each settled per-era print to eras 0-3 plus every
TRAIN_LOG_EVERY_ERAS-th (25 ~= one deep-dive per ~15min per member);
true restores the per-era firehose, flippable live.
Never throttled: anything that marks a CHANGE - new bests, restores +
eta decays, plateau stage transitions, deploy approvals, warnings,
errors, the label-cache/adoption one-shots, and the combined-vote gate
line (the active system's primary telemetry, still every era).
Semantic fixes over blanket gating:
- barrier hold now ARMS silently and prints only when the hold outlasts
the 2-min report interval - a brief hold every era is the design, the
long hold is the watchdog case the line exists for;
- the ensemble deploy REFUSAL prints immediately when its reason
changes (that is a finding), on cadence when unchanged;
- the filtered-view census prints when its RESULT moves (drawn count,
or strongest vote by >=2pp) and at least every 10th sweep.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 09:36:15 -04:00
int refusalKey = ( ! g_ensBestTradeable ? 1 : ( ! haveJoint ? 2 : 3 ) ) ;
if ( refusalKey ! = m_lastEnsRefusalKey | | TrainLogDue ( ) )
Print ( " AI ensemble: PLATEAU stage " + IntegerToString ( PLATEAU_STAGE_DEPLOY ) + " - " +
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
( ! g_ensBestTradeable
2026-08-26 01:14:51 -04:00
? " no era's combined vote ever cleared the deployability floor, so there is nothing "
" safe to deploy. THE BEST ERA FAILED ON: " +
//--- NAME THE CONDITION. All three used to be listed and none identified; they
//--- have nothing in common as fixes, so the list was not a diagnosis.
( g_ensBestCoveragePct > = 0.0 & & g_ensBestMinCoverPct > 0.0 & &
g_ensBestCoveragePct < g_ensBestMinCoverPct
? StringFormat ( " COVERAGE - it fired on %.1f%% of scored bars against a %.1f%% floor "
2026-08-26 01:47:03 -04:00
" (a quarter of the directional base rate). %s The ensemble vote is a "
2026-08-26 01:14:51 -04:00
" weighted mean of member tier weights, so Signal_ThresholdOpen is "
" effectively a quorum - check it against what the members can "
" actually cast before assuming the models are at fault. " ,
2026-08-26 01:47:03 -04:00
g_ensBestCoveragePct , g_ensBestMinCoverPct ,
//--- THE TWO CASES ARE OPPOSITE DIAGNOSES and must not share a
//--- sentence. Cleared-bar means the calls were good and there were
//--- too few of them; missed-bar does NOT mean the model is simply
//--- weak, because the exact-binomial floor is computed from the
//--- INDEPENDENT call count - so thin coverage inflates the very bar
//--- it is being judged against. Reporting those as two separate
//--- failures would send a reader off to fix the model when the
//--- coverage is what moved the target.
( g_ensBestPrecPct > g_ensBestEdgeFloorPct
? StringFormat ( " Precision %.1f%% DID clear its %.1f%% bar: the calls it "
" made were good enough and there were simply too few of "
" them - the vote is too SELECTIVE, not too weak. " ,
g_ensBestPrecPct , g_ensBestEdgeFloorPct )
: StringFormat ( " Precision %.1f%% also missed its %.1f%% bar - but that "
" bar is inflated BY the thin coverage, since the exact "
" binomial floor rises as independent calls fall. These "
" are not two independent failures: fix coverage first, "
" then re-read the bar. " ,
g_ensBestPrecPct , g_ensBestEdgeFloorPct ) ) )
2026-08-26 01:14:51 -04:00
: ( ! g_ensBestTwoSided
? " ONE-SIDEDNESS - the vote never fired both long and short, so its precision "
" is a one-direction book's, not a strategy's. "
: StringFormat ( " PRECISION - %.1f%% against a %.1f%% bar (chance %.1f%% on %d "
" calls). Coverage was fine at %.1f%% against a %.1f%% floor. " ,
g_ensBestPrecPct , g_ensBestEdgeFloorPct , g_ensBestChancePct ,
g_ensBestCalls , g_ensBestCoveragePct , g_ensBestMinCoverPct ) ) )
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
: ( ! haveJoint
? " the joint checkpoint is incomplete - at least one model has no snapshot of the "
" winning era, so the measured ensemble cannot be reproduced. "
: " the best combined vote clears the per-era floor but DOES NOT clear the null of "
" the MAXIMUM over the eras it was chosen from. " + testNote +
" A best-of-N this large happens routinely when every era is a noise draw. " ) ) +
" Restarting the ladder and continuing to train; the era cap remains the backstop. " ) ;
feat(logs): throttle the settled per-era diagnostics - measured 22MB/9.5h of confirmed-working systems
Measured from the journal (2026-08-19): the era deep-dive line (~2KB) plus
the excursion verdict, tier re-rank, calibration move, barrier hold and
selection-regressed note each printed EVERY era for EVERY member - ~940
eras/member/day - long after the systems they watch were confirmed
working. Yesterday's file was 1.3GB (70% of it the news-filter calendar
spam the sweep fix already removed).
VerboseMode returns as an INPUT (demoted 2026-08-01 for the marketplace;
that track is dead since the 2026-08-16 pivot) and gains a second job:
false throttles each settled per-era print to eras 0-3 plus every
TRAIN_LOG_EVERY_ERAS-th (25 ~= one deep-dive per ~15min per member);
true restores the per-era firehose, flippable live.
Never throttled: anything that marks a CHANGE - new bests, restores +
eta decays, plateau stage transitions, deploy approvals, warnings,
errors, the label-cache/adoption one-shots, and the combined-vote gate
line (the active system's primary telemetry, still every era).
Semantic fixes over blanket gating:
- barrier hold now ARMS silently and prints only when the hold outlasts
the 2-min report interval - a brief hold every era is the design, the
long hold is the watchdog case the line exists for;
- the ensemble deploy REFUSAL prints immediately when its reason
changes (that is a finding), on cadence when unchanged;
- the filtered-view census prints when its RESULT moves (drawn count,
or strongest vote by >=2pp) and at least every 10th sweep.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 09:36:15 -04:00
m_lastEnsRefusalKey = refusalKey ;
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
g_ensErasSinceBest = 0 ;
g_ensPlateauStage = 0 ;
for ( int i = 0 ; i < ArraySize ( g_warriorEnsemble ) ; i + + )
{
CExpertSignalAIBase * mm = g_warriorEnsemble [ i ] ;
if ( CheckPointer ( mm ) ! = POINTER_INVALID & & mm . m_ensembleIndex > = 0 )
mm . m_plateauStage = 0 ;
}
ladderNote = " | ladder restarted (gate not cleared) " ;
}
}
}
}
2026-08-22 00:25:52 -04:00
//--- Mirror the shared ladder onto every member.
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
for ( int i = 0 ; i < ArraySize ( g_warriorEnsemble ) ; i + + )
{
CExpertSignalAIBase * mm = g_warriorEnsemble [ i ] ;
if ( CheckPointer ( mm ) = = POINTER_INVALID | | mm . m_ensembleIndex < 0 )
continue ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
mm . m_erasSinceBest = g_ensErasSinceBest ;
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
mm . m_plateauStage = g_ensPlateauStage ;
}
2026-08-16 21:08:41 -04:00
//--- LIFETIME ACCUMULATION - same cadence as a solo model's m_cumOosTotal (see its increment sites):
//--- every scored era adds the bars the vote fired on and how many paid, monotonically, never reset
//--- per era. `wins`/`fired` above are this era's OOS rows only; the panel reads the running total.
g_ensCumOosTotal + = fired ;
g_ensCumOosCorrect + = wins ;
fix(panel,arrows): one deploy predicate, a deployed-only readout, and persist the vote arrows
Four reported symptoms, three of them one root cause: the ensemble's
certified record was session-scoped and written ONLY at pass-3
completion. A deployed ensemble runs no further eras, so every restart
lost the aggregate win rate, the aggregate panel line and the overlay
snapshots - and could never regenerate them, because regeneration only
happens at an era end that will never come.
THE SELF-CONTRADICTION. Member rows read "Live - learning from new bars"
(from m_trainingComplete) while the line under them read "training, not
tradable yet" (from `prospective`, which means "this number came from
ProspectiveVote() rather than a real Direction() call" - what happens on
any bar where every member abstains, and which says nothing whatever
about training state). Both now resolve through one predicate:
WarriorChartModelsDeployed(), fed by members publishing their own state
on the same slot and cadence as their vote. Adds a third verdict word,
"armed (bar still open)", for a deployed model on a prospective
recompute - the case that used to claim it was training.
DEPLOYED PANEL. Once every published model is converged the per-member
rows are dropped: what ships is the aggregate vote win rate, the live
vote, and the verdict. While training the rows stay - they are the only
way a collapsed or lagging member is visible, since a collapsed member
abstains and so is invisible in the aggregate by construction.
ACCURACY NOW RESPECTS THE ENTRY THRESHOLD. The panel's "precision 65%"
came from m_cumOosCorrect/m_cumOosTotal, which counts every bar a model
called Buy or Sell - threshold-blind, and per-model rather than
per-vote. The correct number already existed (votePrecPct: bars where
|vote| >= threshold and the direction policy allows) and is now what the
panel shows, with the threshold named in the text because the number is
meaningless without it.
VOTE ARROWS PERSIST. With DrawUnfilteredSignals off - the default - the
chart shows SIG_VOTE_PREFIX arrows, and nothing saved them:
CChartUI's .arrows sidecar is member-scoped and never saw that layer.
New CVoteArrowStore mirrors them to a chart-keyed sidecar and restores
them progressively at init, on the same budgeted non-blocking path.
The header stores the open/close thresholds; a mismatch on load DISCARDS
the arrows rather than redrawing a picture of a strategy no longer
configured - stale arrows are worse than none, because none is visibly
empty and stale is confidently wrong.
Also: .stats bumped to WST7 carrying the ensemble record (guarded on
threshold match, most-complete-copy-wins), and the loader's version
tests collapsed from an or-chain to ">=" - the magics are ASCII 'WST1'..
'WST7' so they are already ordered, and a missed arm in that chain reads
the NEXT field's bytes into this one, which fails as plausible numbers
rather than as an error.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 13:00:15 -04:00
//--- PANEL + JOURNAL. Body in PublishEnsembleAccuracyLine (ExpertSignalAIBase.mqh) - it is also
//--- called at init, from the record restored out of .stats, which is what gives a reloaded
//--- deployed chart an aggregate line at all.
PublishEnsembleAccuracyLine ( votePrecPct , fired ) ;
feat(panel): one live vote line, no stale era count, no per-model HUD
Two chart-display fixes reported after watching a converged 4-model
ensemble: the ensemble panel's trailing "(era 69, 4 models,
DEPLOYING)" was frozen at whatever era the ensemble happened to
deploy on, and the separate top-right HUD (one line per model, raw
B/S/N + weight + era + error) was clutter once the vote itself is
what matters.
Root cause of the freeze: g_ensembleVoteLine is written once per era,
at pass-3 completion. A deployed/converged ensemble runs no further
eras (ScheduleTrainingIfNeeded's trainingComplete branch skips
Train() entirely), so that line could never update again - the era
count and "DEPLOYING" marker were permanent set-dressing from the
deploying era, not a live reading.
- EnsembleScoreCombinedVote() drops the era/DEPLOYING tail once
g_ensDeployApproved - nothing left there worth freezing.
- UpdateVoteReadout() (the aggregate "VOTE ..." line, previously its
own top-right chart object) now writes g_liveVoteLine instead of
drawing anything. Both status-label builders - PublishEnsembleStatus
for the ensemble panel, PublishStatus's choke point for the solo
panel - append it as one line, refreshed every tick/timer exactly
as the old HUD was, so the live vote replaces the frozen era tail
in the same visual slot.
- RefreshVoteReadout()'s per-member loop (DisplayHudLine, one
ObjectLabel per model) is deleted outright rather than folded in -
the operator asked for the aggregate only, "without telling me each
individual network".
Follow-on dead-code removal, since DisplayHudLine was the only
caller: the DispProb/DispSignal/MetaGateArmedNow/MetaHasScore/
MetaLastP/MetaLastBe/MetaApproved/MetaVetoed leg of IChartView (and
its AIBaseChartView/AIBaseChartViewImpl/ExpertSignalAIBase forwards)
had no other reader. The underlying data survives untouched -
m_metaTelemetry is still populated live by SignalMETA.mqh,
m_dispSignal still feeds ProspectiveVote - only the chart-view
forwarding that existed solely to reach the deleted HUD is gone.
Compile: 0 errors, 0 warnings (stage).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 22:27:41 -04:00
//--- ONCE DEPLOYED, DROP THE ERA TAIL RATHER THAN FREEZE IT. This line is written once per era,
//--- here at pass-3 completion, and a deployed/converged ensemble runs no further eras (see
//--- ScheduleTrainingIfNeeded's trainingComplete branch) - so a static "(era 69, DEPLOYING)"
//--- would sit on the panel forever, unrefreshed, looking live when it is not (user report
//--- 2026-08-24). g_liveVoteLine, appended right after this by PublishEnsembleStatus, is what
//--- actually refreshes every tick from here on; the era count has nothing left to say.
fix(persist): adopt the pinned threshold on load; trim the accuracy label
THE REGRESSION, mine, from c6eb908. LoadModelStats() dropped the whole ensemble
record unless the stored threshold EQUALLED the live one. That was right while
the threshold was an operator input - a record built at 25% says nothing about a
chart now running 15%. Once the threshold became derived and pinned the
comparison inverted its own meaning: at load time g_ensembleVoteThreshold is
still the Signal_ThresholdOpen SEED, so the stored derived value never matches
and the record is ALWAYS dropped. Two things died with it, silently:
* g_ensDeployApproved - a DEPLOYED ensemble came back as a training one on
every restart, discarding the family-wise deploy it had earned.
* the pinned threshold itself - PublishVoteThreshold() only fires on a positive
g_ensDerivedThreshold, so a deployed chart would have traded the .chr seed
instead of the rung its deploy was certified at. certified != traded, the
defect 2c443ba fixed, reintroduced three commits later.
Not yet observed live only because SP500 deployed at 10:20, after the last
restart at 09:54, so no restart has crossed a deployed state.
Now ADOPTED, not compared: threshold, counts and deploy flag restore together,
the only coherent state - the counts were conditional on that threshold, which is
why it is stored beside them. Same doctrine as the .cfg topology: adopt what the
model was certified with, never re-derive it underneath a checkpoint. The
most-complete-copy guard is unchanged. It now logs what it restored.
THE PANEL LABEL. "Vote win rate: 34% (338 calls at or above the 15% threshold,
this era 31%)" -> "Accuracy: 34%". The call count, threshold and this-era figure
are diagnostics, all present in the era log line, and on a panel they buried the
one number anyone reads. The threshold no longer needs naming either: it is
derived and pinned rather than an operator's choice, so it is not a caveat on the
percentage. The era/models/deployable suffix appended at era end goes with them.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 11:59:25 -04:00
//--- The era/model/deployable suffix was dropped 2026-08-26 with the rest of the line's baggage:
//--- the panel shows the accuracy, the era log line carries the diagnostics. Kept as a comment
//--- rather than deleted silently so the next reader knows where those three facts went.
2026-08-22 08:16:11 -04:00
//--- THE HIGHEST VOTE THIS ENSEMBLE CAN PRODUCE: every member voting, each at its best tier. The
//--- divisor in Direction() is the CAPABLE weight, so unanimity returns the capability-weighted mean
//--- of the tier weights - which is roughly the pooled holdout win rate. A threshold above that can
//--- NEVER fire, and "0 fired" then reads as "the models are unsure" when it means "unreachable in
//--- this configuration". Measured on USDJPY 2026-08-22: pooled win rates 15.6-19.4% against a
//--- 25% threshold, highest vote ever seen 13. Same class as the excursion head's disjoint gate.
double capSum = 0.0 , bestSum = 0.0 ;
fix(vote): unranked members voted with the stock 25/50/75/100 ladder
Two defects behind "arrows drawn while members are still mid-era".
1. THE DRAW. The filtered overlay armed on the FIRST member to finish
pass 3 and leaned on a 60 s rate limit to "collapse the burst",
assuming members finish seconds apart. They do not - on USDJPY one
member was at sample 10496 of pass 2 while another was at 2304,
minutes apart. A member with no era-end snapshot returns false from
SnapshotVoteAt, and the sweep's `if(!hasData) continue;` skips it
BEFORE `den += ModuleWeight()`, so the one finished model's tier
weight became the entire vote and was drawn as a consensus arrow.
An abstention is a member that looked at the bar and said nothing; a
missing snapshot is a member that has not looked. The first must
dilute the vote, the second must suppress the draw. The arm is now a
readiness MASK - one bit per m_ensembleIndex, set at that member's
pass-3 completion, cleared when a sweep arms - and a sweep waits for
every enrolled member. Bounded at 10 minutes so a member that stops
cannot freeze the chart, and the partial draw PRINTS which members
were missing: the be39674 lesson is that a hold must never silence
the thing that reports it.
2. THE VOTE ITSELF, which is the worse half and is not display-only.
Tier weights are not persisted in the .nnw - they exist only as the
output of a completed pass 3 - so before a member's first
RankTiersFromOos() it holds the constructor's stock 25/50/75/100.
Since 4858507 the vote currency is a WIN RATE, so an unranked tier-3
call enters the capability-weighted mean claiming a 100% win rate
beside ranked members contributing ~25. Not a strong opinion: the
wrong unit. One unranked member drags the ensemble over any
threshold, on every fresh deploy and every resume. USDJPY has a
measured ceiling of ~19 and was firing anyway.
LiveVoteContribution() now abstains until self-ranked, which drops
the member from the sum AND the divisor. One function, so live and
the gate move together (2c443ba).
Era 0 will therefore report 0 coverage until each member completes one
era. The ensemble line says so explicitly rather than leaving it to look
like the USDJPY unreachable-threshold case - the two are identical in
the coverage number and completely different problems.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 08:55:04 -04:00
int rankedMembers = 0 , enrolledMembers = 0 ;
2026-08-22 08:16:11 -04:00
for ( int ci = 0 ; ci < ArraySize ( g_warriorEnsemble ) ; ci + + )
{
CExpertSignalAIBase * cm = g_warriorEnsemble [ ci ] ;
if ( CheckPointer ( cm ) = = POINTER_INVALID | | cm . m_ensembleIndex < 0 )
continue ;
fix(vote): unranked members voted with the stock 25/50/75/100 ladder
Two defects behind "arrows drawn while members are still mid-era".
1. THE DRAW. The filtered overlay armed on the FIRST member to finish
pass 3 and leaned on a 60 s rate limit to "collapse the burst",
assuming members finish seconds apart. They do not - on USDJPY one
member was at sample 10496 of pass 2 while another was at 2304,
minutes apart. A member with no era-end snapshot returns false from
SnapshotVoteAt, and the sweep's `if(!hasData) continue;` skips it
BEFORE `den += ModuleWeight()`, so the one finished model's tier
weight became the entire vote and was drawn as a consensus arrow.
An abstention is a member that looked at the bar and said nothing; a
missing snapshot is a member that has not looked. The first must
dilute the vote, the second must suppress the draw. The arm is now a
readiness MASK - one bit per m_ensembleIndex, set at that member's
pass-3 completion, cleared when a sweep arms - and a sweep waits for
every enrolled member. Bounded at 10 minutes so a member that stops
cannot freeze the chart, and the partial draw PRINTS which members
were missing: the be39674 lesson is that a hold must never silence
the thing that reports it.
2. THE VOTE ITSELF, which is the worse half and is not display-only.
Tier weights are not persisted in the .nnw - they exist only as the
output of a completed pass 3 - so before a member's first
RankTiersFromOos() it holds the constructor's stock 25/50/75/100.
Since 4858507 the vote currency is a WIN RATE, so an unranked tier-3
call enters the capability-weighted mean claiming a 100% win rate
beside ranked members contributing ~25. Not a strong opinion: the
wrong unit. One unranked member drags the ensemble over any
threshold, on every fresh deploy and every resume. USDJPY has a
measured ceiling of ~19 and was firing anyway.
LiveVoteContribution() now abstains until self-ranked, which drops
the member from the sum AND the divisor. One function, so live and
the gate move together (2c443ba).
Era 0 will therefore report 0 coverage until each member completes one
era. The ensemble line says so explicitly rather than leaving it to look
like the USDJPY unreachable-threshold case - the two are identical in
the coverage number and completely different problems.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 08:55:04 -04:00
enrolledMembers + + ;
//--- Unranked members abstain (see LiveVoteContribution), so they are not part of the ceiling
//--- either - counting their stock 100 would put the ceiling above anything reachable.
if ( ! cm . SelfRanked ( ) )
continue ;
rankedMembers + + ;
2026-08-22 08:16:11 -04:00
double capW = cm . VoteCapableWeight ( ) ;
if ( ! MathIsValidNumber ( capW ) | | capW < = 0.0 )
continue ;
int best = 0 ;
for ( int t = 0 ; t < 4 ; t + + )
best = ( int ) MathMax ( best , cm . PatternWeightForTier ( t ) ) ;
capSum + = capW ;
bestSum + = capW * best ;
}
double voteCeiling = ( capSum > 0.0 ) ? bestSum / capSum : 0.0 ;
fix(vote): unranked members voted with the stock 25/50/75/100 ladder
Two defects behind "arrows drawn while members are still mid-era".
1. THE DRAW. The filtered overlay armed on the FIRST member to finish
pass 3 and leaned on a 60 s rate limit to "collapse the burst",
assuming members finish seconds apart. They do not - on USDJPY one
member was at sample 10496 of pass 2 while another was at 2304,
minutes apart. A member with no era-end snapshot returns false from
SnapshotVoteAt, and the sweep's `if(!hasData) continue;` skips it
BEFORE `den += ModuleWeight()`, so the one finished model's tier
weight became the entire vote and was drawn as a consensus arrow.
An abstention is a member that looked at the bar and said nothing; a
missing snapshot is a member that has not looked. The first must
dilute the vote, the second must suppress the draw. The arm is now a
readiness MASK - one bit per m_ensembleIndex, set at that member's
pass-3 completion, cleared when a sweep arms - and a sweep waits for
every enrolled member. Bounded at 10 minutes so a member that stops
cannot freeze the chart, and the partial draw PRINTS which members
were missing: the be39674 lesson is that a hold must never silence
the thing that reports it.
2. THE VOTE ITSELF, which is the worse half and is not display-only.
Tier weights are not persisted in the .nnw - they exist only as the
output of a completed pass 3 - so before a member's first
RankTiersFromOos() it holds the constructor's stock 25/50/75/100.
Since 4858507 the vote currency is a WIN RATE, so an unranked tier-3
call enters the capability-weighted mean claiming a 100% win rate
beside ranked members contributing ~25. Not a strong opinion: the
wrong unit. One unranked member drags the ensemble over any
threshold, on every fresh deploy and every resume. USDJPY has a
measured ceiling of ~19 and was firing anyway.
LiveVoteContribution() now abstains until self-ranked, which drops
the member from the sum AND the divisor. One function, so live and
the gate move together (2c443ba).
Era 0 will therefore report 0 coverage until each member completes one
era. The ensemble line says so explicitly rather than leaving it to look
like the USDJPY unreachable-threshold case - the two are identical in
the coverage number and completely different problems.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 08:55:04 -04:00
//--- "0 fired because nobody has ranked yet" and "0 fired because the threshold is unreachable"
//--- look identical in the coverage number and are completely different problems.
string rankNote = ( rankedMembers < enrolledMembers )
? StringFormat ( " | %d of %d members have NOT ranked their tiers yet and are "
" abstaining: tier weights only exist as the output of a "
" completed pass 3 and are not persisted in the .nnw, so every "
" fresh deploy and every resume starts here. Self-corrects after "
" one era per member. " ,
enrolledMembers - rankedMembers , enrolledMembers )
: " " ;
2026-08-26 04:17:27 -04:00
//--- THE SWEEP, rendered. Coverage and precision at each selectable rung, plus a marker on the
//--- one in force, so "the vote is too selective" comes with the number that would fix it. The
//--- coverage floor is the same minCoverPct the gate applies, so a rung can be read as passing
//--- or failing at a glance. Printed only when there is a floor to compare against.
string sweepNote = " " ;
if ( measurable & & minCoverPct > 0.0 )
{
sweepNote = " | THRESHOLD SWEEP (what this era's vote would score at each rung, floor " +
StringFormat ( " %.1f%% " , minCoverPct ) + " ): " ;
for ( int s = 0 ; s < ENS_THRESHOLD_SWEEP_N ; s + + )
{
double swCov = 100.0 * sweepFired [ s ] / MathMax ( shared , 1 ) ;
double swPrec = ( sweepFired [ s ] > 0 ) ? 100.0 * sweepWins [ s ] / sweepFired [ s ] : -1.0 ;
sweepNote + = StringFormat ( " %.0f%%->%s/%.1f%%cov%s " ,
g_ensThresholdSweep [ s ] ,
( swPrec > = 0.0 ? StringFormat ( " %.1f%%prec " , swPrec ) : " n/a " ) ,
swCov ,
feat(vote): derive the threshold instead of configuring it
Signal_ThresholdOpen becomes a seed. The era verdict now picks the HIGHEST
sweep rung whose vote still clears the whole deploy gate - coverage floor,
exact-binomial precision bar and two-sidedness together - computes the era's
verdict AT that rung, and publishes it to the live signal's m_threshold_open
so the bar the gate certifies is the bar the EA trades.
Measured on 619 era verdicts across all six live charts:
* every era on every symbol had at least one rung clearing the full gate.
At the fixed 25% the fleet was actually running, four of six symbols had
none, ever. The threshold, not the models, was the blocker.
* walk-forward (rung derived on era N, scored on era N+1): 10.2% coverage /
31.8% precision, against an oracle re-picking on N+1 of 10.3% / 31.7%.
Near-zero shrinkage - a measurement, not a fit. It holds because the
binding constraint is COVERAGE, a near-deterministic step function of the
vote distribution, not precision.
* vs a fixed 15% (best global value): +0.6pp precision, 3.4pp less coverage.
vs a fixed 20%: deployable on all six rather than four of six.
Selection on the highest PASSING rung, never on the best-precision rung - that
is a best-of-6 on a noisy statistic and this project has crowned noise that way
four times. The multiplicity that remains is paid for: nTried in
EnsembleSurvivesSelection is now eras x rungs. Costs nothing - all six charts
clear it by 6.5-12 sigma even forming z on effective rather than raw calls.
Also fixes, in the same path: the direction-policy gate is hoisted above the
per-rung tally so every rung is scored on the population the gate certifies.
Retrain-neutral: not in BuildModelFingerprint(), no .nnw re-keyed.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:02:58 -04:00
( s = = derivedIdx
? " [DERIVED] " : ( swCov > = minCoverPct ? " [clears floor] " : " " ) ) ) ;
2026-08-26 04:17:27 -04:00
}
feat(vote): derive the threshold instead of configuring it
Signal_ThresholdOpen becomes a seed. The era verdict now picks the HIGHEST
sweep rung whose vote still clears the whole deploy gate - coverage floor,
exact-binomial precision bar and two-sidedness together - computes the era's
verdict AT that rung, and publishes it to the live signal's m_threshold_open
so the bar the gate certifies is the bar the EA trades.
Measured on 619 era verdicts across all six live charts:
* every era on every symbol had at least one rung clearing the full gate.
At the fixed 25% the fleet was actually running, four of six symbols had
none, ever. The threshold, not the models, was the blocker.
* walk-forward (rung derived on era N, scored on era N+1): 10.2% coverage /
31.8% precision, against an oracle re-picking on N+1 of 10.3% / 31.7%.
Near-zero shrinkage - a measurement, not a fit. It holds because the
binding constraint is COVERAGE, a near-deterministic step function of the
vote distribution, not precision.
* vs a fixed 15% (best global value): +0.6pp precision, 3.4pp less coverage.
vs a fixed 20%: deployable on all six rather than four of six.
Selection on the highest PASSING rung, never on the best-precision rung - that
is a best-of-6 on a noisy statistic and this project has crowned noise that way
four times. The multiplicity that remains is paid for: nTried in
EnsembleSurvivesSelection is now eras x rungs. Costs nothing - all six charts
clear it by 6.5-12 sigma even forming z on effective rather than raw calls.
Also fixes, in the same path: the direction-policy gate is hoisted above the
per-rung tally so every rung is scored on the population the gate certifies.
Retrain-neutral: not in BuildModelFingerprint(), no .nnw re-keyed.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:02:58 -04:00
sweepNote + = " . [DERIVED] is the rung this era's verdict was computed at and the one the live "
" signal now trades: the HIGHEST rung clearing the whole gate (coverage floor, "
" exact binomial bar, both sides live), or - if none did - the highest still "
" clearing coverage, so the failure is reported on precision rather than on a "
" population too thin to judge. Nothing to set by hand: Signal_ThresholdOpen is "
" now only the seed used before the first scored era. " ;
2026-08-26 04:17:27 -04:00
}
2026-08-22 08:16:11 -04:00
string ceilingNote = ( voteCeiling > 0.0 & & voteCeiling < g_ensembleVoteThreshold )
? StringFormat ( " | THRESHOLD UNREACHABLE: the highest vote this ensemble can "
" cast is %.1f%% (every member voting at its best tier) against "
" a %.0f%% threshold. Coverage cannot rise above 0 until the "
" threshold sits below that ceiling, which IS the pooled win "
" rate - no amount of training moves it. " ,
voteCeiling , g_ensembleVoteThreshold )
: " " ;
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
Print ( StringFormat ( " AI ensemble: combined-vote era %d - %d models, %d shared OOS bars, %d fired at "
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
" vote>=%.0f%% (%.1f%% coverage, floor %.1f%%), precision %s vs chance %.1f%% (needs "
fix(deploy): print the selection score in the unit it is actually in
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>
2026-08-24 17:21:57 -04:00
" >%.1f%% at %d sigma)%s -> score %s%s%s. The vote that actually trades: each "
fix(gate): the ensemble gate certified a vote the EA never casts
g_ensembleVoteThreshold's comment claims the combined-vote scorer "fires on
the same criterion the live trade does". It did not. Two independent
mismatches, both silent:
CURRENCY. Each member contributed its raw signed confidence x100 - a 33..100
number straight off the softmax head. Live contributes m_weight x the tier's
pattern weight, and BOTH of those are rewritten from the signal DB by
UpdateSignalsWeights(). A head output and a DB-ranked win-rate weight share
an axis and nothing relates them, so the same bar was one number to the gate
and a different one to the order path. Same shape as the 2026-08-09 geometry
incident: certified on one game, paid on another.
DENOMINATOR. The gate divided by the member count, so an abstaining member
pulled the average toward zero. CExpertSignalCustom::Direction() skips a zero
contribution in BOTH the sum and the count (`if(direction == 0) continue;`
before `number++`) - live is a mean over VOTERS. The gate was therefore
scoring a strictly more agreement-heavy set of bars than the EA trades. The
contribution hook's own comment asserted the opposite ("abstentions dilute
the average exactly as they do in the live vote"), while the AI_CHOICE enum
20 lines away correctly documented union semantics.
LiveVoteContribution() is now the single definition of "what this member
votes", called from the gate; the live path reaches the same arithmetic
through LongCondition/ShortCondition. g_ensVoteVoterMask records who actually
voted, separately from who evaluated the bar, because those are the divisor
and the shared-population test respectively.
ConfidenceTier() is split into ConfidenceTierFor(signal) plus a thin live-bar
wrapper - the OOS scan holds the scanned bar's decision in a local, and
dPrevSignal is a different bar.
NOT changed, deliberately: the gate still does not model live NMS
declustering, and the per-member solo gate still scores every directional
call rather than threshold-clearing ones. Both are selection-metric changes
and this codebase has twice been bitten by switching one blind.
Also corrects a stale paragraph in m_pattern_0's declaration block quoting
80/87/93/100 as the tier defaults. The constructor is 25/50/75/100 and has
been since the confidence floor and alternation gate were removed; the block
carried both tables at once, and the dead one was quoted back as fact.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 15:23:33 -04:00
" member's DB-ranked tier weight x module weight, averaged over the members that "
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
" VOTED (abstentions excluded, as live), graded on swing-label agreement. " ,
feat(vote): exit-on-reversal boolean, pin the threshold, retry the atomic rename
THE EXIT KNOB. Exit_On_Reversal_Vote (default false) replaces the deleted
Signal_ThresholdClose with one boolean: false pins the close threshold to an
arithmetically unreachable 101, true pins it to the SAME threshold the entry
uses - the seed at first, then the derived value, republished together whenever
it moves. A second threshold was always redundant; "the bot now says the other
way" is one question.
It also arms CExpertSignalCustom::m_holdToBarrier, which was DEAD CODE:
HoldToBarrier(bool) had no caller anywhere in the build, so the flag had been
permanently false and the disabled close threshold was carrying the whole
hold-to-barrier policy alone. Both halves now move together.
Default stays false because the reason is statistical: the gate certifies
P(label agrees | vote fired) against a label that runs to the barrier, so an
early close trades something never measured. Turning it on is a different
strategy, not a tightening of this one.
THE PIN. The live threshold now moves only when an era's weights become the
checkpoint, and freezes once g_ensDeployApproved. Every era still derives its own
rung - that is how the best one is found - but the rung that TRADES belongs to
the checkpoint, exactly as the weights do. Two reasons, one measured and one
structural: the per-era rung moves on 6-34% of steps (the live run flapped
SP500 15 -> 10 -> 15 within a minute of starting), and without the pin a later
era's rung could end up applied to an earlier era's deployed model. A ladder
restart releases the pin, since clearing the checkpoint clears what it pinned.
The era line now prints the rung its own numbers came from, so it stays honest
when that differs from the pinned one.
THE ATOMIC RENAME retried zero times. Six charts share the TrainPool and AltData
directories, so a publish regularly lands while a peer chart holds the
destination open and FileMove returns 5004 - 27 times in one day on the live
fleet. Nothing was lost (the temp keeps the new content, the old file stays
intact) but the row did not update until the next publish. Now four attempts at
25ms, on the FAILURE PATH ONLY - a successful rename never sleeps - and skipped
in the tester, where the contention cannot happen and Sleep would distort a pass.
A rescued retry is logged, so worsening contention is visible.
Retrain-neutral. Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:53:56 -04:00
( int ) votedEra , members , shared , fired , g_ensThresholdSweep [ derivedIdx ] ,
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
coveragePct , minCoverPct ,
( fired > 0 ? StringFormat ( " %.1f%% " , votePrecPct ) : " n/a " ) , chancePct , edgeFloorPct ,
fix(deploy): print the selection score in the unit it is actually in
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>
2026-08-24 17:21:57 -04:00
( int ) EDGE_MIN_SIGMAS , ( tradeableOK ? " DEPLOYABLE " : " " ) , DeployScoreText ( score ) ,
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
( isBetter ? StringFormat ( " <-- NEW BEST, joint checkpoint captured (era %d) " , ( int ) votedEra )
fix(deploy): print the selection score in the unit it is actually in
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>
2026-08-24 17:21:57 -04:00
: StringFormat ( " (best %s at era %d, %d eras ago) " , DeployScoreText ( g_ensBestScore ) ,
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
( int ) g_ensBestEra , g_ensErasSinceBest ) ) ,
2026-08-26 04:17:27 -04:00
ladderNote + rankNote + ceilingNote + sweepNote ) ) ;
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
}
//+------------------------------------------------------------------+
//| Per-member era-end hook: mark this member done for the era and, |
//| when it is the last one, run the verdict above. |
//+------------------------------------------------------------------+
void CExpertSignalAIBase : : EnsembleOosPassComplete ( const long votedEra , double & etaLocal )
{
if ( ! m_ensembleMember | | m_ensembleIndex < 0 )
return ;
//--- The rows were stamped during pass 3, BEFORE this member incremented its era counter, so the
//--- buffer's era is the era that just finished. A mismatch means this member contributed nothing
//--- to the current buffer (no OOS bars scored this era) - it cannot be counted as having read the
//--- vote, or the verdict would be taken on a subset that silently excludes it.
if ( g_ensVoteEra ! = votedEra )
return ;
g_ensVoteDoneMask | = ( 1 < < m_ensembleIndex ) ;
int need = 0 ;
for ( int i = 0 ; i < ArraySize ( g_warriorEnsemble ) ; i + + )
{
CExpertSignalAIBase * mm = g_warriorEnsemble [ i ] ;
if ( CheckPointer ( mm ) = = POINTER_INVALID | | mm . m_ensembleIndex < 0 )
continue ;
if ( mm . m_trainingComplete | | mm . m_trainingStopRequested | | mm . m_trainingPaused | | ! mm . m_isInitialized )
continue ;
need | = ( 1 < < mm . m_ensembleIndex ) ;
}
if ( need = = 0 | | ( g_ensVoteDoneMask & need ) ! = need )
return ;
//--- Idempotence: one verdict per era, whatever order the members arrive in.
if ( g_ensLastVerdictEra = = votedEra )
return ;
g_ensLastVerdictEra = votedEra ;
EnsembleEraVerdict ( need , votedEra , etaLocal ) ;
}
//+------------------------------------------------------------------+
feat: gate deployment on the null of the MAXIMUM, not the per-era null
EDGE_MIN_SIGMAS is a PER-ERA test and the deployed model is the MAXIMUM
over every era a run ranks. A 2-sigma one-sided test passes on noise with
probability 0.0228 per era, so over N eras the chance at least one clears
it is 1-(1-0.0228)^N: 34% by era 18, 80% by era 70, 93% by era 112. The
gate was near-certain to open on a long run whatever the data held.
It did. HYBRID deployed 2026-08-08 at dir-precision 35.5% vs 34% chance -
+1.5pp, best of 112 eras whose per-era values wandered 30%..35.5%. At the
call counts these runs produce that is p_family 0.92..0.9999.
Every OTHER best-of-N decision here already carries this correction, and
every one REJECTS on this data: the barrier-geometry winner (null of the
maximum over 6, p=0.3902), the indicator tuner (Sidak, p=1.0000), the MI
lag profile (null of the maximum over 21 lags). The one decision that
ships a model to a live account had none.
BestCheckpointSurvivesSelection() re-tests the checkpoint that is about to
deploy:
z = (precision - chance)/SE, SE = sqrt(p0(1-p0)/n)
p_single = P(Z >= z)
p_family = 1 - (1-p_single)^N
against DEPLOY_FAMILY_WISE_ALPHA. It uses the checkpoint's OWN
snapshotted precision/chance/call-count, not the latest era's, because
the model that ships is the one that has to clear the bar.
N counts CANDIDATE eras (coverage measurable, at least one directional
call) - an era that called nothing directional could never have become
the best, so counting it would make the gate stricter than the search
that actually happened.
Conservative on purpose: consecutive eras share OOS bars and differ by
one gradient step, so they are nowhere near N independent draws and the
true family-wise error is below this bound. This gate decides what trades
real money and the house posture is reject-unless-demonstrated.
Effect at 2900 directional calls / N=112: required edge goes 1.76pp ->
2.92pp. A real edge clears it; +1.5pp does not.
Applied to BOTH automatic paths - the plateau ladder's stage-3 deploy and
the m_trainingComplete assignment - which must stay identical or the flag
persisted into the .nnw disagrees with the decision to stop, and a reload
runs inference on a model the ladder refused.
NOT applied to the two operator paths (era-cap deploy, panel Deploy
button). Those stay the operator's call; ReportSelectionGateVerdict()
logs the verdict beside them so an authorised deploy can never later be
misread as a validated one.
NormalUpperTail() is A&S 26.2.17 (|err| < 7.5e-8), self-contained rather
than pulling in Math\Stat. Verified against reference values to 6dp:
Q(1.645)=0.049985, Q(1.96)=0.024998, Q(3.0)=0.001350. Its locals are
ntB1..ntB5 because AI\Network.mqh line 79 does "#define b1 AdamBeta1".
Compiles clean: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 18:01:04 -04:00
//| Log the selection-gate verdict for a deploy the gate does NOT |
//| block - the era-cap path and the panel's Deploy button, both of |
//| which are explicit operator decisions and stay that way. The point |
//| is that "I chose to ship this" and "this cleared the bar" should |
//| never be confusable in the log afterwards. |
//+------------------------------------------------------------------+
void CExpertSignalAIBase : : ReportSelectionGateVerdict ( string context )
{
double z = 0.0 , pFam = 1.0 ;
int nTried = 0 ;
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
//--- ENSEMBLE: report the gate that actually governs this model. Quoting the member's own
//--- best-of-N here would answer a question nobody asked - the member never deploys alone, and a
//--- member-level "CLEARS" next to a vote that did not is precisely the confusion this function
//--- exists to prevent.
if ( m_ensembleMember )
{
bool okEns = EnsembleSurvivesSelection ( z , pFam , nTried ) ;
if ( g_ensBestCalls < = 0 )
{
Print ( ID + " : " + context + " - the ENSEMBLE selection gate cannot be evaluated (no era's "
" combined vote has been ranked yet). Treat this ensemble as unvalidated. " ) ;
return ;
}
Print ( ID + " : " + context + " - ENSEMBLE best-of- " + IntegerToString ( nTried ) + " test on the "
" combined VOTE: edge " + DoubleToString ( g_ensBestPrecPct - g_ensBestChancePct , 1 ) + " pp ( " +
DoubleToString ( g_ensBestPrecPct , 1 ) + " % vs chance " + DoubleToString ( g_ensBestChancePct , 1 ) +
" %) on " + IntegerToString ( g_ensBestCalls ) + " fired bars = " + DoubleToString ( z , 2 ) +
" sigma, family-wise p= " + DoubleToString ( pFam , 4 ) + " (need <= " +
DoubleToString ( DEPLOY_FAMILY_WISE_ALPHA , 2 ) + " ) - " +
( okEns ? " CLEARS. "
: " DOES NOT CLEAR. A maximum this size arises routinely when every era is a noise "
" draw, so this ensemble is being deployed on operator authority, NOT on measured "
" evidence of an edge. " ) ) ;
return ;
}
feat: gate deployment on the null of the MAXIMUM, not the per-era null
EDGE_MIN_SIGMAS is a PER-ERA test and the deployed model is the MAXIMUM
over every era a run ranks. A 2-sigma one-sided test passes on noise with
probability 0.0228 per era, so over N eras the chance at least one clears
it is 1-(1-0.0228)^N: 34% by era 18, 80% by era 70, 93% by era 112. The
gate was near-certain to open on a long run whatever the data held.
It did. HYBRID deployed 2026-08-08 at dir-precision 35.5% vs 34% chance -
+1.5pp, best of 112 eras whose per-era values wandered 30%..35.5%. At the
call counts these runs produce that is p_family 0.92..0.9999.
Every OTHER best-of-N decision here already carries this correction, and
every one REJECTS on this data: the barrier-geometry winner (null of the
maximum over 6, p=0.3902), the indicator tuner (Sidak, p=1.0000), the MI
lag profile (null of the maximum over 21 lags). The one decision that
ships a model to a live account had none.
BestCheckpointSurvivesSelection() re-tests the checkpoint that is about to
deploy:
z = (precision - chance)/SE, SE = sqrt(p0(1-p0)/n)
p_single = P(Z >= z)
p_family = 1 - (1-p_single)^N
against DEPLOY_FAMILY_WISE_ALPHA. It uses the checkpoint's OWN
snapshotted precision/chance/call-count, not the latest era's, because
the model that ships is the one that has to clear the bar.
N counts CANDIDATE eras (coverage measurable, at least one directional
call) - an era that called nothing directional could never have become
the best, so counting it would make the gate stricter than the search
that actually happened.
Conservative on purpose: consecutive eras share OOS bars and differ by
one gradient step, so they are nowhere near N independent draws and the
true family-wise error is below this bound. This gate decides what trades
real money and the house posture is reject-unless-demonstrated.
Effect at 2900 directional calls / N=112: required edge goes 1.76pp ->
2.92pp. A real edge clears it; +1.5pp does not.
Applied to BOTH automatic paths - the plateau ladder's stage-3 deploy and
the m_trainingComplete assignment - which must stay identical or the flag
persisted into the .nnw disagrees with the decision to stop, and a reload
runs inference on a model the ladder refused.
NOT applied to the two operator paths (era-cap deploy, panel Deploy
button). Those stay the operator's call; ReportSelectionGateVerdict()
logs the verdict beside them so an authorised deploy can never later be
misread as a validated one.
NormalUpperTail() is A&S 26.2.17 (|err| < 7.5e-8), self-contained rather
than pulling in Math\Stat. Verified against reference values to 6dp:
Q(1.645)=0.049985, Q(1.96)=0.024998, Q(3.0)=0.001350. Its locals are
ntB1..ntB5 because AI\Network.mqh line 79 does "#define b1 AdamBeta1".
Compiles clean: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 18:01:04 -04:00
bool ok = BestCheckpointSurvivesSelection ( z , pFam , nTried ) ;
if ( m_bestDirCalls < = 0 )
{
Print ( ID + " : " + context + " - selection gate cannot be evaluated (no ranked checkpoint with "
" directional calls). Treat this model as unvalidated. " ) ;
return ;
}
Print ( ID + " : " + context + " - best-of- " + IntegerToString ( nTried ) + " selection test: edge " +
DoubleToString ( m_bestDirPrecPct - m_bestChancePrecPct , 1 ) + " pp ( " +
DoubleToString ( m_bestDirPrecPct , 1 ) + " % vs chance " + DoubleToString ( m_bestChancePrecPct , 1 ) +
" %) on " + IntegerToString ( m_bestDirCalls ) + " directional calls = " + DoubleToString ( z , 2 ) +
" sigma, family-wise p= " + DoubleToString ( pFam , 4 ) + " (need <= " +
DoubleToString ( DEPLOY_FAMILY_WISE_ALPHA , 2 ) + " ) - " +
( ok ? " CLEARS. "
: " DOES NOT CLEAR. A maximum this size arises routinely when every era is a noise draw, so "
" this model is being deployed on operator authority, NOT on measured evidence of an edge. " ) ) ;
}
//+------------------------------------------------------------------+
2026-08-22 00:30:14 -04:00
//| Training and Signal Methods Where the TRAINING window starts: |
//| ALL available history, floored by MinTrainYear. |
fix: prebuild and era sized different windows; diag: Train() names its branch
TWO things, one incident.
1) THE BUG I SHIPPED IN 0c85c54. m_tuneStartTrainBar is declared, initialised
to 0, and NEVER ASSIGNED - the assignment existed before the God-class split
and the split dropped it, leaving a dead member. Harmless while nothing read
it; a real defect the moment 0c85c54 made StartLabelCachePrebuild() reset
dtStudied from it. Train() then computed the window as
max(StartTrainBar, floor) while the prebuild computed max(0, floor), where
StartTrainBar is the non-zero datetime OnChartEventHandler passes through from
the "New Bar" event. The two therefore disagreed about `bars`, so
EnsureBarCachesCapacity() saw a changed size at era start, wiped the caches,
and re-armed a full 38k-bar prebuild - instead of training. Restored the
assignment so both sides evaluate the identical expression.
2) THE REASON IT TOOK ALL NIGHT TO FIND. Train() is a state machine with six
early-return branches above the era loop and every one of them is silent. Four
charts burned a core each for 15 minutes with an empty journal: the pass
heartbeats (694b756) proved the era loop was never reached, no prebuild
completion line appeared either, and nothing external can see inside a single
MQL5 thread - per-thread CPU says "busy", file writes say nothing, and the VPS
has no debugger. That is an undiagnosable state, and it is the thing to fix,
not just the bug of the day.
ReportTrainStall() now names the branch Train() is taking whenever no era has
completed for 3 minutes, at most once a minute per signal, with the state that
decides the branch: run/prebuild/simOos/resume flags, era, dtStudied, and -
for the cache-invalidation branch specifically - BOTH bar counts, since two
sizings disagreeing is exactly what re-arms the prebuild forever. Silent on a
healthy run: an era completing resets the clock.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 07:32:08 -04:00
//+------------------------------------------------------------------+
void CExpertSignalAIBase : : ReportTrainStall ( const string branch )
{
const uint STALL_AFTER_MS = 180000 ; // 3 min: ~2x the slowest healthy era seen on this config
const uint STALL_REPORT_INTERVAL = 60000 ;
uint nowTick = GetTickCount ( ) ;
//--- First call ever: adopt now as the baseline rather than reporting instantly against tick 0.
if ( m_lastEraCompleteTick = = 0 )
{
m_lastEraCompleteTick = nowTick ;
return ;
}
uint since = nowTick - m_lastEraCompleteTick ;
if ( since < STALL_AFTER_MS )
return ;
if ( m_lastStallReportTick ! = 0 & & nowTick - m_lastStallReportTick < STALL_REPORT_INTERVAL )
return ;
m_lastStallReportTick = nowTick ;
PrintFormat ( " %s: TRAIN STALL - no era has completed for %.0fs and Train() is taking the '%s' branch "
" | era %d | runActive=%s prebuildActive=%s cachePrebuilt=%s simOos=%s eraResume=%s "
" paused=%s stopReq=%s | labelCacheBars=%d anchor=%s dtStudied=%s " ,
ID , since / 1000.0 , branch , ( int ) m_eraCount ,
m_trainRunActive ? " Y " : " N " , m_labelPrebuildActive ? " Y " : " N " ,
refactor(online-learning): OnlineLearning is a real collaborator, not a raw-include partial (S5)
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>
2026-08-23 22:59:02 -04:00
m_labelCachePrebuilt ? " Y " : " N " , m_onlineLearning . SimRunActive ( ) ? " Y " : " N " ,
fix: prebuild and era sized different windows; diag: Train() names its branch
TWO things, one incident.
1) THE BUG I SHIPPED IN 0c85c54. m_tuneStartTrainBar is declared, initialised
to 0, and NEVER ASSIGNED - the assignment existed before the God-class split
and the split dropped it, leaving a dead member. Harmless while nothing read
it; a real defect the moment 0c85c54 made StartLabelCachePrebuild() reset
dtStudied from it. Train() then computed the window as
max(StartTrainBar, floor) while the prebuild computed max(0, floor), where
StartTrainBar is the non-zero datetime OnChartEventHandler passes through from
the "New Bar" event. The two therefore disagreed about `bars`, so
EnsureBarCachesCapacity() saw a changed size at era start, wiped the caches,
and re-armed a full 38k-bar prebuild - instead of training. Restored the
assignment so both sides evaluate the identical expression.
2) THE REASON IT TOOK ALL NIGHT TO FIND. Train() is a state machine with six
early-return branches above the era loop and every one of them is silent. Four
charts burned a core each for 15 minutes with an empty journal: the pass
heartbeats (694b756) proved the era loop was never reached, no prebuild
completion line appeared either, and nothing external can see inside a single
MQL5 thread - per-thread CPU says "busy", file writes say nothing, and the VPS
has no debugger. That is an undiagnosable state, and it is the thing to fix,
not just the bug of the day.
ReportTrainStall() now names the branch Train() is taking whenever no era has
completed for 3 minutes, at most once a minute per signal, with the state that
decides the branch: run/prebuild/simOos/resume flags, era, dtStudied, and -
for the cache-invalidation branch specifically - BOTH bar counts, since two
sizings disagreeing is exactly what re-arms the prebuild forever. Silent on a
healthy run: an era completing resets the clock.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 07:32:08 -04:00
m_eraResumePending ? " Y " : " N " , m_trainingPaused ? " Y " : " N " ,
m_trainingStopRequested ? " Y " : " N " ,
m_labelCacheBars , TimeToString ( m_labelCacheAnchorTime ) , TimeToString ( dtStudied ) ) ;
}
//+------------------------------------------------------------------+
diag(training): report the window an era ACTUALLY trains on
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>
2026-08-24 15:12:05 -04:00
//| THE WINDOW THE ERA ACTUALLY GOT, and the three quantities that |
//| decide it. Reported on change only. |
//| |
//| barsNow is MathMin(Bars(symbol, PERIOD_CURRENT, dtStudied, now) + |
//| historyBars, Bars(symbol, PERIOD_CURRENT)), so a short era is |
//| either a dtStudied that is too RECENT or a price series that is |
//| short - and those need opposite fixes. Printing only the result |
//| ("3671 bars") cannot tell them apart, which is why all three go |
//| on the line together with the date dtStudied resolved to. |
//+------------------------------------------------------------------+
void CExpertSignalAIBase : : ReportEraWindow ( const int barsNow )
{
if ( barsNow = = m_lastEraWindowBars )
return ;
m_lastEraWindowBars = barsNow ;
PrintFormat ( " %s: era %d TRAINING WINDOW = %d bars | Bars(since dtStudied %s) = %d | Bars(series) = %d "
" | series starts %s | historyBars %d. The smaller of the first two is what this era "
" trains and scores on - NOT the in-sample estimate the CAPACITY and DETECTABILITY "
" lines quote, which is derived from the configuration. " ,
ID , ( int ) m_eraCount , barsNow , TimeToString ( dtStudied ) ,
Bars ( m_symbol . Name ( ) , PERIOD_CURRENT , dtStudied , TimeCurrent ( ) ) ,
Bars ( m_symbol . Name ( ) , PERIOD_CURRENT ) ,
TimeToString ( ( datetime ) SeriesInfoInteger ( m_symbol . Name ( ) , PERIOD_CURRENT , SERIES_FIRSTDATE ) ) ,
( int ) m_historyBars ) ;
}
//+------------------------------------------------------------------+
diag: slow eras must explain themselves - heartbeat + era time split + pass-1 paint
The 23:42 restart left all four charts grinding ~25x slower than the 18:01
baseline (era lines in 86 seconds there; 20+ minutes of nothing here), and
NOTHING could say why from outside: pass 1 logs nothing, its status paint sat
inside the !wouldQueue branch so the IS sweep - 80% of the pass, processed
FIRST - painted nothing either, the VPS has no debugger for a thread stack,
and the hourly new-bar cache invalidation cancels and restarts an unfinished
era, so a slow era can stay invisible FOREVER. Externals gave: four chart
threads at ~95% pure user-mode compute, DLL pool idle, no file writes. That
narrows it to "MQL5-side per-item work in the era passes" and no further.
So training now explains itself:
- TrainHeartbeat: one line per 4096 processed items, only after an era has
already run 60s, at most 6 lines per era - a healthy era stays exactly as
quiet as before. Reports position and the cumulative split: feature-window
builds vs net forward/backprop vs everything else. Hooked into all three
passes.
- The era summary line gains "| ERA TOOK Ns (feature windows X, net fwd/back
Y, other Z)" whenever an era exceeded 120s.
- Pass 1 paints its progress for QUEUED bars too, not just the OOS slice, so
the panel shows "learning (era N)" instead of sitting on the idle writer's
"Getting ready..." for the entire IS sweep. The label is throttled
internally; painting per bar costs nothing.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 00:10:45 -04:00
//| Speaks ONLY when an era is genuinely slow: nothing for the first |
//| 60 seconds of an era, at most 6 lines after that, one per 4096 |
//| processed items. Reports where the time actually went, split into |
//| the two candidate costs and the remainder, because "the era is |
//| slow" without the split is exactly the undiagnosable state the |
//| 2026-08-10 restart produced (see the member declarations). |
//+------------------------------------------------------------------+
2026-08-10 07:22:29 -04:00
void CExpertSignalAIBase : : TrainHeartbeat ( const string tag , int done , int total , const string shortLabel )
diag: slow eras must explain themselves - heartbeat + era time split + pass-1 paint
The 23:42 restart left all four charts grinding ~25x slower than the 18:01
baseline (era lines in 86 seconds there; 20+ minutes of nothing here), and
NOTHING could say why from outside: pass 1 logs nothing, its status paint sat
inside the !wouldQueue branch so the IS sweep - 80% of the pass, processed
FIRST - painted nothing either, the VPS has no debugger for a thread stack,
and the hourly new-bar cache invalidation cancels and restarts an unfinished
era, so a slow era can stay invisible FOREVER. Externals gave: four chart
threads at ~95% pure user-mode compute, DLL pool idle, no file writes. That
narrows it to "MQL5-side per-item work in the era passes" and no further.
So training now explains itself:
- TrainHeartbeat: one line per 4096 processed items, only after an era has
already run 60s, at most 6 lines per era - a healthy era stays exactly as
quiet as before. Reports position and the cumulative split: feature-window
builds vs net forward/backprop vs everything else. Hooked into all three
passes.
- The era summary line gains "| ERA TOOK Ns (feature windows X, net fwd/back
Y, other Z)" whenever an era exceeded 120s.
- Pass 1 paints its progress for QUEUED bars too, not just the OOS slice, so
the panel shows "learning (era N)" instead of sitting on the idle writer's
"Getting ready..." for the entire IS sweep. The label is throttled
internally; painting per bar costs nothing.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 00:10:45 -04:00
{
2026-08-10 07:22:29 -04:00
//--- Panel progress is published on EVERY call, before the 4096-item gate below: the gate exists to
//--- keep the JOURNAL quiet, and applying it to the panel too would leave the display frozen between
//--- boundaries. Two assignments, no formatting - cheap enough for a per-item path.
m_passLabel = shortLabel ;
m_passProgressPct = ( total > 0 ) ? ( int ) MathMin ( 100.0 , 100.0 * done / total ) : 0 ;
2026-08-22 00:25:52 -04:00
//--- TIME-gated, not item-gated. A diagnostic whose trigger can be outrun by the condition it
//--- watches for is worse than none - it produces confident wrong conclusions. The 255-item mask
//--- only keeps GetTickCount() off the hot path.
2026-08-10 07:44:03 -04:00
if ( ( done & 255 ) ! = 0 )
diag: slow eras must explain themselves - heartbeat + era time split + pass-1 paint
The 23:42 restart left all four charts grinding ~25x slower than the 18:01
baseline (era lines in 86 seconds there; 20+ minutes of nothing here), and
NOTHING could say why from outside: pass 1 logs nothing, its status paint sat
inside the !wouldQueue branch so the IS sweep - 80% of the pass, processed
FIRST - painted nothing either, the VPS has no debugger for a thread stack,
and the hourly new-bar cache invalidation cancels and restarts an unfinished
era, so a slow era can stay invisible FOREVER. Externals gave: four chart
threads at ~95% pure user-mode compute, DLL pool idle, no file writes. That
narrows it to "MQL5-side per-item work in the era passes" and no further.
So training now explains itself:
- TrainHeartbeat: one line per 4096 processed items, only after an era has
already run 60s, at most 6 lines per era - a healthy era stays exactly as
quiet as before. Reports position and the cumulative split: feature-window
builds vs net forward/backprop vs everything else. Hooked into all three
passes.
- The era summary line gains "| ERA TOOK Ns (feature windows X, net fwd/back
Y, other Z)" whenever an era exceeded 120s.
- Pass 1 paints its progress for QUEUED bars too, not just the OOS slice, so
the panel shows "learning (era N)" instead of sitting on the idle writer's
"Getting ready..." for the entire IS sweep. The label is throttled
internally; painting per bar costs nothing.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 00:10:45 -04:00
return ;
2026-08-10 07:44:03 -04:00
uint nowTick = GetTickCount ( ) ;
uint elapsedMs = nowTick - m_eraStartTick ;
if ( elapsedMs < 60000 | | m_passHeartbeatPrints > = 12 )
return ;
if ( m_lastHeartbeatTick ! = 0 & & nowTick - m_lastHeartbeatTick < 30000 )
diag: slow eras must explain themselves - heartbeat + era time split + pass-1 paint
The 23:42 restart left all four charts grinding ~25x slower than the 18:01
baseline (era lines in 86 seconds there; 20+ minutes of nothing here), and
NOTHING could say why from outside: pass 1 logs nothing, its status paint sat
inside the !wouldQueue branch so the IS sweep - 80% of the pass, processed
FIRST - painted nothing either, the VPS has no debugger for a thread stack,
and the hourly new-bar cache invalidation cancels and restarts an unfinished
era, so a slow era can stay invisible FOREVER. Externals gave: four chart
threads at ~95% pure user-mode compute, DLL pool idle, no file writes. That
narrows it to "MQL5-side per-item work in the era passes" and no further.
So training now explains itself:
- TrainHeartbeat: one line per 4096 processed items, only after an era has
already run 60s, at most 6 lines per era - a healthy era stays exactly as
quiet as before. Reports position and the cumulative split: feature-window
builds vs net forward/backprop vs everything else. Hooked into all three
passes.
- The era summary line gains "| ERA TOOK Ns (feature windows X, net fwd/back
Y, other Z)" whenever an era exceeded 120s.
- Pass 1 paints its progress for QUEUED bars too, not just the OOS slice, so
the panel shows "learning (era N)" instead of sitting on the idle writer's
"Getting ready..." for the entire IS sweep. The label is throttled
internally; painting per bar costs nothing.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 00:10:45 -04:00
return ;
2026-08-10 07:44:03 -04:00
m_lastHeartbeatTick = nowTick ;
diag: slow eras must explain themselves - heartbeat + era time split + pass-1 paint
The 23:42 restart left all four charts grinding ~25x slower than the 18:01
baseline (era lines in 86 seconds there; 20+ minutes of nothing here), and
NOTHING could say why from outside: pass 1 logs nothing, its status paint sat
inside the !wouldQueue branch so the IS sweep - 80% of the pass, processed
FIRST - painted nothing either, the VPS has no debugger for a thread stack,
and the hourly new-bar cache invalidation cancels and restarts an unfinished
era, so a slow era can stay invisible FOREVER. Externals gave: four chart
threads at ~95% pure user-mode compute, DLL pool idle, no file writes. That
narrows it to "MQL5-side per-item work in the era passes" and no further.
So training now explains itself:
- TrainHeartbeat: one line per 4096 processed items, only after an era has
already run 60s, at most 6 lines per era - a healthy era stays exactly as
quiet as before. Reports position and the cumulative split: feature-window
builds vs net forward/backprop vs everything else. Hooked into all three
passes.
- The era summary line gains "| ERA TOOK Ns (feature windows X, net fwd/back
Y, other Z)" whenever an era exceeded 120s.
- Pass 1 paints its progress for QUEUED bars too, not just the OOS slice, so
the panel shows "learning (era N)" instead of sitting on the idle writer's
"Getting ready..." for the entire IS sweep. The label is throttled
internally; painting per bar costs nothing.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 00:10:45 -04:00
m_passHeartbeatPrints + + ;
double featS = ( double ) m_passFeatUs / 1000000.0 ;
double netS = ( double ) m_passNetUs / 1000000.0 ;
PrintFormat ( " %s: SLOW ERA heartbeat - %s %d of %d after %.0fs | feature windows %.1fs | net fwd/back %.1fs | everything else %.1fs " ,
ID , tag , done , total , elapsedMs / 1000.0 , featS , netS ,
MathMax ( elapsedMs / 1000.0 - featS - netS , 0.0 ) ) ;
}
//+------------------------------------------------------------------+
fix: a restart no longer loses the measured geometry or the training window
Terminal restart, 22:25: all four resumed models sat on empty windows with
enum 2:6 barriers. Three interlocking causes, all visible in one log excerpt:
1) THE PRE-SCAN WINDOW WAS SIZED BY THE SAVED WATERMARK. A resumed model's
dtStudied sits at its last studied bar, so Bars(dtStudied, now) ~ 0 and the
resumed-model MI pre-scan built a zero-bar "complete" label cache - logged as
"Buy: 0 | Sell: 0 | Neutral: 0". Train()'s own era start RESETS dtStudied to
the training-window rule before computing its window; the pre-scan did not.
The rule is now factored into TrainWindowStart() and both use it. The scan
also refuses to arm before SERIES_SYNCHRONIZED (it ran in the same second as
OnInit), and deployed models keep their watermark - for them it gates
inference recency, not a training window.
2) THE HORIZON LATCHED ON AN INDICATOR WARM-UP. ComputeBarrierHorizonBars ran
against a ZigZag with 0 calculated legs, fell back, and EnsureBarrierHorizon
latched fallback(32) x slMult x tpMult = 384 for the process lifetime. A
leg-starved horizon is now PROVISIONAL: re-resolved on the next rebuild, the
label cache wiped if it moved (labels from two horizons answer different
questions), and the geometry deriver refuses to run from it - a pair derived
over a warm-up window would get PINNED.
3) THE DERIVED GEOMETRY WAS NEVER PERSISTED. The .cfg is written at model
creation and at weights-reset - both BEFORE era 0 derives - so the measured
pair lived only in memory: every restart read back zeros, adopted nothing,
fell back to the enum barriers, and the era-0-only gate meant a resumed model
could NEVER re-derive. A full day of training on 3.33/1.62 resumed as 2:6.
Now: the settled pair is pinned to the .cfg the moment derivation completes
(one-shot, atomic write), and the derive gate accepts any model with no
pinned pair, not just era 0 - mid-run stability is carried by
m_geometryDerived itself, which never allows a second derivation.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 22:40:43 -04:00
datetime CExpertSignalAIBase : : TrainWindowStart ( datetime startTrainBar )
{
datetime firstAvailableBar = ( datetime ) SeriesInfoInteger ( m_symbol . Name ( ) , PERIOD_CURRENT , SERIES_FIRSTDATE ) ;
MqlDateTime floor_time ;
TimeCurrent ( floor_time ) ;
floor_time . year = m_minTrainYear ;
floor_time . mon = 1 ;
floor_time . day = 1 ;
floor_time . hour = 0 ;
floor_time . min = 0 ;
floor_time . sec = 0 ;
datetime st_time = StructToTime ( floor_time ) ;
if ( firstAvailableBar > st_time )
st_time = firstAvailableBar ;
return MathMax ( startTrainBar , st_time ) ;
}
//+------------------------------------------------------------------+
2026-08-22 00:36:36 -04:00
//| Save the era-loop context a yielding chunk resumes against. |
//| |
//| Each pass keeps its OWN cursor (m_isTrainCursor, m_calibIndex, |
//| m_oosScoreIndex); these five are what every pass shares. One |
//| writer, because a field missed at one of the four yield points |
//| resumes the next chunk against a different era than the one that |
//| yielded, and nothing reports that until the numbers drift. |
//+------------------------------------------------------------------+
void CExpertSignalAIBase : : StashEraResume ( const int bars , const int totalIter , const int oosCutoff ,
const bool add_loop , const int barIndex )
{
m_resumeBars = bars ;
m_resumeTotalIter = totalIter ;
m_resumeOosCutoff = oosCutoff ;
m_resumeAddLoop = add_loop ;
m_resumeBarIndex = barIndex ;
m_eraResumePending = true ;
//--- This model's own learning-rate trajectory, out of the shared global before yielding.
m_modelEta = g_eta ;
}
//+------------------------------------------------------------------+
refactor(training): the era log is not part of the era loop
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>
2026-08-23 11:25:07 -04:00
//| THE ERA LINE. Everything below is string building over already- |
//| measured state - it decides nothing and changes nothing, which |
//| is exactly why it does not belong inside the era loop. |
//| |
//| Self-guarding on tel.shouldLog: the throttle is decided where |
//| the tick count is known and carried here, so the caller is one |
//| unconditional call rather than a 200-line branch. |
//+------------------------------------------------------------------+
void CExpertSignalAIBase : : ReportEraProgress ( const SEraTelemetry & tel )
{
if ( ! tel . shouldLog )
return ;
string recallInfo = ( tel . buyRecall < 0 & & tel . sellRecall < 0 & & tel . neutralRecall < 0 ) ? " " :
( " | OOS recall Buy: " + ( tel . buyRecall < 0 ? " n/a " : IntegerToString ( tel . buyRecall ) + " % " ) +
" Sell: " + ( tel . sellRecall < 0 ? " n/a " : IntegerToString ( tel . sellRecall ) + " % " ) +
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
" Neutral: " + ( tel . neutralRecall < 0 ? " n/a " : IntegerToString ( tel . neutralRecall ) + " % " ) ) ;
refactor(training): the era log is not part of the era loop
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>
2026-08-23 11:25:07 -04:00
string selectionInfo = ( tel . dirPrec < 0 ) ? " | SELECT: no directional calls survived the threshold " :
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
( " | SELECT precision " + IntegerToString ( tel . dirPrec ) + " % on " +
refactor(training): the era log is not part of the era loop
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>
2026-08-23 11:25:07 -04:00
IntegerToString ( tel . coverage ) + " % of bars (post-threshold) " +
( tel . chancePrec > = 0
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
? " (chance=base-rate " + IntegerToString ( tel . chancePrec ) + " %, edge " +
refactor(training): the era log is not part of the era loop
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>
2026-08-23 11:25:07 -04:00
( tel . dirPrec - tel . chancePrec > = 0 ? " + " : " " ) +
IntegerToString ( tel . dirPrec - tel . chancePrec ) + " pp) "
: " " ) ) ;
//--- The operating point that produced the coverage figure just above it, so the two are read
//--- together: coverage falling is only good news if it is this that caused it.
selectionInfo + = " @margin>= " + DoubleToString ( m_dirConfThreshold , 2 ) ;
//--- TRADED precision: the same calls after declustering, which since 2026-08-09 is
//--- exactly the set that becomes positions (live NMS gates the trade, not just the
//--- arrow).
if ( m_signalClusterWindow > 0 & & m_oosNmsFired > 0 )
{
int nmsPrec = ( int ) MathRound ( 100.0 * m_oosNmsHits / m_oosNmsFired ) ;
selectionInfo + = " | TRADED (declustered) " + IntegerToString ( nmsPrec ) + " % on " +
IntegerToString ( m_oosNmsFired ) + " calls " +
( tel . chancePrec > = 0
? " (edge " + ( nmsPrec - tel . chancePrec > = 0 ? " + " : " " ) +
IntegerToString ( nmsPrec - tel . chancePrec ) + " pp) "
: " " ) ;
}
// See tel.buyPred's declaration comment for why this is worth logging alongside recall -
// it's what tells apart a suppressed/dead output (predicted rate stuck at 0%) from a
// miscalibrated boundary (predicted rate healthy, precision poor), which look identical from
// recall alone.
string predictedInfo = ( tel . buyPred < 0 & & tel . sellPred < 0 ) ? " " :
( " | OOS calls Buy: " + ( tel . buyPred < 0 ? " n/a " : IntegerToString ( tel . buyPred ) + " % " ) +
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
" (precision " + ( tel . buyPrec < 0 ? " n/a " : IntegerToString ( tel . buyPrec ) + " % " ) + " ) " +
refactor(training): the era log is not part of the era loop
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>
2026-08-23 11:25:07 -04:00
" Sell: " + ( tel . sellPred < 0 ? " n/a " : IntegerToString ( tel . sellPred ) + " % " ) +
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
" (precision " + ( tel . sellPrec < 0 ? " n/a " : IntegerToString ( tel . sellPrec ) + " % " ) + " ) " ) ;
refactor(training): the era log is not part of the era loop
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>
2026-08-23 11:25:07 -04:00
//--- CALIBRATION - "does the model call each class as often as the class actually occurs".
//--- Ratios, because 1.0x is the answer and the distance from it is the error.
string calibInfo = ( tel . buyTrue < 0 | | tel . buyFired < 0 ) ? " " :
StringFormat ( " | CALIBRATION traded vs true rate Buy %d%% vs %d%% (%s) Sell %d%% vs %d%% (%s) "
" Neutral %d%% vs %d%% (%s) | pre-threshold argmax Buy %d%% Sell %d%% Neutral %d%% " ,
tel . buyFired , tel . buyTrue , CalibrationRatio ( tel . buyFired , tel . buyTrue ) ,
tel . sellFired , tel . sellTrue , CalibrationRatio ( tel . sellFired , tel . sellTrue ) ,
tel . neutralFired , tel . neutralTrue ,
CalibrationRatio ( tel . neutralFired , tel . neutralTrue ) ,
tel . buyPred , tel . sellPred , tel . neutralPred ) ;
//--- Live-fired precision: the number that actually predicts forward-trading performance - only
//--- the directional calls that cleared the confidence floor under the live/prior-corrected rule
//--- (see AdjustedSignalFromSoftmax). Count in parentheses = how many bars the model would have
//--- traded this era. "0" fires = the calibration is (this era) suppressing all directional trades.
string liveInfo = ( m_lastBuyFired < = 0 & & m_lastSellFired < = 0 ) ? " | live fires 0 this era " :
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
( " | live precision Buy: " + ( tel . buyFiredPrec < 0 ? " n/a " : IntegerToString ( tel . buyFiredPrec ) + " % " ) +
refactor(training): the era log is not part of the era loop
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>
2026-08-23 11:25:07 -04:00
" ( " + IntegerToString ( m_lastBuyFired ) + " ) " +
" Sell: " + ( tel . sellFiredPrec < 0 ? " n/a " : IntegerToString ( tel . sellFiredPrec ) + " % " ) +
" ( " + IntegerToString ( m_lastSellFired ) + " ) " ) ;
//--- Precision BY CONFIDENCE TIER, and cumulatively from each tier upward - the two
//--- numbers a decision about Signal_ThresholdOpen actually needs.
string tierInfo = " " ;
int tierFiredTotal = 0 ;
for ( int ti = 0 ; ti < 4 ; ti + + )
tierFiredTotal + = m_oosTierFired [ ti ] ;
if ( tierFiredTotal > 0 )
{
tierInfo = " | tier prec " ;
for ( int ti = 0 ; ti < 4 ; ti + + )
{
int cumFired = 0 , cumHits = 0 ;
for ( int tj = ti ; tj < 4 ; tj + + )
{
cumFired + = m_oosTierFired [ tj ] ;
cumHits + = m_oosTierHits [ tj ] ;
}
tierInfo + = " T " + IntegerToString ( ti ) + " : " +
( m_oosTierFired [ ti ] > 0
? IntegerToString ( ( int ) MathRound ( 100.0 * m_oosTierHits [ ti ] / m_oosTierFired [ ti ] ) ) + " % "
: " n/a " ) +
" ( " + IntegerToString ( m_oosTierFired [ ti ] ) + " ) " +
( cumFired > 0
? " [>= " + IntegerToString ( ( int ) MathRound ( 100.0 * cumHits / cumFired ) ) + " %/ " +
IntegerToString ( cumFired ) + " ] "
: " " ) ;
}
}
//--- Per-layer weight movement. Pairs with rawOutInfo below: a collapsed constant-
//--- classifier state has two very different causes, and only this tells them apart. See
//--- CNet::LayerLearningReport.
string layerInfo = ( CheckPointer ( Net ) = = POINTER_INVALID ) ? " " :
( " | dW/W " + Net . LayerLearningReport ( ) ) ;
// Raw-output saturation diagnostic - see m_oosOutMin's declaration comment. Spread ~0 with
// all six min/max values pinned together = the collapsed constant-classifier state.
string rawOutInfo = ( m_oosOutCount < = 0 ) ? " " :
StringFormat ( " | OOS raw out B:%.3f..%.3f S:%.3f..%.3f N:%.3f..%.3f spread avg %.4f " ,
m_oosOutMin [ 0 ] , m_oosOutMax [ 0 ] , m_oosOutMin [ 1 ] , m_oosOutMax [ 1 ] ,
m_oosOutMin [ 2 ] , m_oosOutMax [ 2 ] , m_oosOutSpreadSum / m_oosOutCount ) ;
//--- DENOMINATOR IS THE PER-ERA BAR COUNT, not m_oosSamples (fixed 2026-08-17).
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
int zsBars = m_oos . Bars ( ) ;
refactor(training): the era log is not part of the era loop
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>
2026-08-23 11:25:07 -04:00
string zeroSkillInfo = ( zsBars < = 0 ) ? " " :
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
StringFormat ( " | zero-skill on these bars: always-Buy %.1f%%, always-Sell %.1f%% "
" (the gate ranks on the LARGER of the two; the gap between them IS the "
" directional drift, and a model that only reproduces it has found the drift, "
" not an edge) " ,
100.0 * ( double ) m_oos . buyTotal / zsBars ,
100.0 * ( double ) m_oos . sellTotal / zsBars ) ;
refactor(training): the era log is not part of the era loop
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>
2026-08-23 11:25:07 -04:00
//--- THE DEPLOY BAR, stated. Reading "edge -1pp" era after era tells you the model is
//--- short; it does not tell you whether it is short by a hair or by an amount no strategy
//--- could ever cover.
2026-08-25 23:37:22 -04:00
//--- STATE THE FORMULA THAT ACTUALLY PRODUCED THE NUMBER. This line used to read
//--- "chance + 2 x SE 3.9pp" beside a printed bar of 100.0% - two quantities that cannot both
//--- be true, sitting in the same parenthesis, every era, on every chart. The bar is the EXACT
//--- binomial floor (ExactEdgeFloorPct); chance+sigmas*SE is only the normal approximation it
//--- replaced, still shown alongside because a large gap between the two is itself the signal
//--- that the sample is too small for the approximation - and because a disagreement between
//--- them is what a reader can actually check.
refactor(training): the era log is not part of the era loop
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>
2026-08-23 11:25:07 -04:00
string gateInfo = ( m_lastEdgeFloorPct < 0.0 | | m_lastEffN < = 0.0 ) ? " " :
2026-08-25 23:37:22 -04:00
StringFormat ( " | DEPLOY BAR %.1f%% (EXACT binomial at %.0f sigma; the chance+%.0fxSE "
" approximation would say %.1f%%, SE %.1fpp) on %.0f INDEPENDENT calls - "
" %d raw calls deflated by the %.1f-bar mean label lifespan%s " ,
m_lastEdgeFloorPct , EDGE_MIN_SIGMAS , EDGE_MIN_SIGMAS ,
m_oos . ChancePrecPct ( ) + EDGE_MIN_SIGMAS * m_lastPrecSE , m_lastPrecSE , m_lastEffN ,
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
m_oos . DirCalls ( ) , MeanLabelLifespan ( ) ,
refactor(training): the era log is not part of the era loop
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>
2026-08-23 11:25:07 -04:00
//--- A bar above 100% is not "hard", it is unreachable, and no amount of
//--- training addresses it - only a bigger independent sample does.
( m_lastEdgeFloorPct > = 100.0
? " <-- UNREACHABLE: no win rate can clear this. The OOS window does not hold "
" enough independent observations to certify ANY edge; widen the sample "
" (more instruments / lower timeframe) or narrow the barrier. "
: " " ) ) ;
string neutralWhy = ( m_oosOutCount < = 0 ) ? " " :
StringFormat ( " | Neutral CHOSE %.1f%% / TIED %.1f%% (of which B=S %d) | rail %.1f%% " ,
100.0 * ( double ) m_oosNeutralStrict / m_oosOutCount ,
100.0 * ( double ) m_oosNeutralTie / m_oosOutCount ,
( int ) m_oosTieBuySell ,
100.0 * ( double ) m_oosRailBars / m_oosOutCount ) ;
//--- No "(target X%)" any more - there is no absolute accuracy target. What replaces it as the
//--- progress indicator is the plateau counter: how many eras since the last new best, and how
//--- close that is to ending the run (see the PLATEAU_* ladder).
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
string plateauInfo = ( m_bestSelectionScore < 0 ) ? " " :
( " | best score " + DeployScoreText ( m_bestSelectionScore ) + " , " + IntegerToString ( m_erasSinceBest ) +
refactor(training): the era log is not part of the era loop
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>
2026-08-23 11:25:07 -04:00
" eras since (stage " + IntegerToString ( m_plateauStage ) + " / " + IntegerToString ( PLATEAU_STAGE_DEPLOY ) + " ) " ) ;
//--- Lifetime IS/OOS directional accuracy. The GAP between the two is still the over-
//--- fitting read, so it survives here, once per era, behind the compile-time
//--- DebuggingMode constant.
string lifetimeInfo = ( ! DebuggingMode | | ( m_cumIsTotal < = 0 & & m_cumOosTotal < = 0 ) ) ? " " :
( " | lifetime dir acc IS " + ( m_cumIsTotal > 0 ? IntegerToString ( ( int ) MathRound ( m_cumIsCorrect * 100.0 / m_cumIsTotal ) ) + " % " : " n/a " ) +
" OOS " + ( m_cumOosTotal > 0 ? IntegerToString ( ( int ) MathRound ( m_cumOosCorrect * 100.0 / m_cumOosTotal ) ) + " % " : " n/a " ) +
" over " + IntegerToString ( m_cumIsTotal + m_cumOosTotal ) + " calls " ) ;
//--- Wall-clock split for the era that just finished, but only when it was SLOW - a
//--- healthy era stays exactly one line. An era finished: the stall clock restarts from
//--- here (see m_lastEraCompleteTick).
m_lastEraCompleteTick = GetTickCount ( ) ;
string eraTimeInfo = " " ;
{
double eraS = ( GetTickCount ( ) - m_eraStartTick ) / 1000.0 ;
if ( eraS > 120.0 )
eraTimeInfo = StringFormat ( " | ERA TOOK %.0fs (feature windows %.0fs, net fwd/back %.0fs, "
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
" other %.0fs) " ,
refactor(training): the era log is not part of the era loop
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>
2026-08-23 11:25:07 -04:00
eraS , m_passFeatUs / 1000000.0 , m_passNetUs / 1000000.0 ,
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
MathMax ( eraS - m_passFeatUs / 1000000.0 - m_passNetUs / 1000000.0 , 0.0 ) ) ;
refactor(training): the era log is not part of the era loop
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>
2026-08-23 11:25:07 -04:00
}
//--- THROTTLED (2026-08-19): this is the ~2KB deep-dive block, and it printed every era
//--- for every member - ~3.7MB per member per day, the single largest line in a measured
//--- 22MB/9.5h journal. VerboseMode = every era again.
if ( TrainLogDue ( ) )
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
Print ( ID + " : training in progress - era " + IntegerToString ( m_eraCount ) + " , OOS accuracy " + DoubleToString ( dOosForecast , 1 ) + " %, IS error " + DoubleToString ( dError , 2 ) + recallInfo + selectionInfo + predictedInfo + calibInfo + liveInfo + tierInfo + plateauInfo + lifetimeInfo + zeroSkillInfo + gateInfo + m_lastPoolReport + rawOutInfo + neutralWhy + layerInfo + eraTimeInfo ) ;
refactor(training): the era log is not part of the era loop
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>
2026-08-23 11:25:07 -04:00
// Forced (unthrottled) panel refresh, right here alongside the console line above, using this
// era's own just-finalized m_eraCount/dOosForecast - see UpdateTrainingStatusLabel's
// declaration comment for why this can't just rely on the next throttled bar-scan call to
// catch up (it would, but a full era later than the console already reported it).
refactor(chart): ChartUI is a real collaborator, not a raw-include partial (S2)
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>
2026-08-23 20:03:52 -04:00
RefreshStatusLabel ( ) ;
refactor(training): the era log is not part of the era loop
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>
2026-08-23 11:25:07 -04:00
}
//+------------------------------------------------------------------+
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
//| PASS 1: the scan/queue sweep that walks the era backwards from |
//| era.i, building the sample queue and training on it. |
//| |
//| era.i is a MEMBER of the era state rather than a loop local |
//| because this loop yields on the wall-clock budget and resumes at |
//| the same bar on the next call. |
//+------------------------------------------------------------------+
void CExpertSignalAIBase : : RunPass1 ( STrainEra & era )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
{
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
for ( ; era . i > = 0 & & ! era . stop ; era . i - - )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
{
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
//--- Build THIS bar's own feature window and feed it forward BEFORE checking/training against
//--- its label - see r's declaration comment below for why the window must end AT bar i, and
//--- why this must run before the label-check block rather than after: the label check needs
//--- this bar's own freshly-computed prediction, not the previous iteration's (see windowOk).
TempData . Clear ( ) ;
//--- Window ends AT (includes) bar i itself, extending m_historyBars bars into the past -
//--- i.e.
int r = era . i ;
bool windowOk = false ;
double displayNeuron0 = 0 , displayNeuron1 = 0 , displayNeuron2 = 0 ;
if ( r < = era . bars )
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
{
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
ulong hbT = GetMicrosecondCount ( ) ;
windowOk = BuildFeatureWindow ( r ) ;
m_passFeatUs + = GetMicrosecondCount ( ) - hbT ;
if ( windowOk )
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
{
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
era . addLoop = true ;
m_passWindowOk + + ;
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
}
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
else
m_passWindowFail + + ;
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
}
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
TrainHeartbeat ( " pass 1 (scan/queue), bar " , era . bars - MathMax ( m_historyBars , 0 ) - era . i , era . totalIter , " scan " ) ;
//--- Determine label/queue-eligibility BEFORE running any feedForward this bar - see
//--- wouldQueue's use below for why. Mirrors the label-check condition this block used to
//--- gate on (moved earlier, unchanged).
bool haveLabel = false , buy = false , sell = false , wouldQueue = false ;
//--- "some LATER pass in this same era will feed this exact bar forward anyway", which is a
//--- strictly wider set than wouldQueue - see its use at the feedForward below. Declared out
//--- here because the three membership tests that decide it are scoped to the label block.
bool laterPassForwards = false ;
if ( windowOk & & era . i < ( int ) ( era . bars - MathMax ( m_historyBars , 0 ) - 1 ) & & era . i > 1 & & m_Time . GetData ( era . i ) > dtStudied
refactor(meta): remove meta-labeling entirely - RETRAIN-NEUTRAL
~2,300 lines. META had real, repeatedly measured ranking skill and ZERO
operating points that ever cleared break-even (0/350 H1 eras, 1/999 H4
pre-2-sigma, 0/8 pooled fitted points). The clinching arithmetic was edge x
width = 0.095 ATR/trade against spread 0.099 ATR/trade, and the
dose-response showed the high-conviction tail is temporally unstable -
the precision-vs-threshold slope flips sign between calib and test on 3 of
4 symbols, so no ex-ante threshold rule exists. It shipped default-off and
never gated a live entry. The self-measured tier weights are what actually
rank the vote, and all six H4 instruments converged on them alone.
RETRAIN-NEUTRAL, and that is the property that made this safe:
- The weights fingerprint emitted "|TGT:META2" or "|TGT:SWG1" from an
if/else. Every direction model already took the SWG1 arm, so
collapsing it to an unconditional append is byte-identical. No .nnw or
.cfg is orphaned or re-keyed.
- NetInputWidth() lost its "+ MetaDescWidth()" term. MetaDescWidth()
returned 0 for every direction model, so the input layer is unchanged.
- DbLegacyAiSlot()'s slot 5 was reachable only with all four Use_* NNs
off AND meta on - a config that never shipped. Every existing .db keeps
its filename.
Deleted outright: Signals/SignalMETA.mqh, Expert/Trading/MetaGate.mqh (the
directory is now empty), Expert/Training/{MetaCorpus,MetaCandidateStore,
MetaFamilies}.mqh, Tests/Test_MetaFamilies.mq5, Meta_Labeling_Design.md.
Unwound in place, the delicate part: Training.mqh carried four
IsMetaTarget() branches whose else-arm WRAPPED the direction body (pass 1
queueing, pass 2 backprop, pass 2.5 calibration, pass 3 OOS scoring). Each
wrapper is removed and the direction body promoted back to its original
nesting - the bodies were never re-indented when the wrappers were added,
so the promoted code is byte-identical to what ran before META existed.
Also gone: the ensemble verdict's meta-veto replay and its
approved/vetoed/unscored counters, the per-family/per-side OOS
decomposition arrays, the m_isTrainQueueCand parallel queue and its
lockstep shuffle, and the S2 era report.
Also removed: the CMetaGate abstraction and the live CheckOpenPosition
veto; m_gates plus AddFilter's non-voter routing and IsVotingSignal()
(META was the only non-voting child, so m_gates was always empty);
m_parentSignal/SetParentSignal (existed only to reach the root's gate);
SweepPrepare/SweepPrepareIndicator (only caller was the corpus sweep);
IsMetaTarget() from all four view interfaces and their adapters;
Use_MetaLabeling, EnableMETA, Meta_ExportDataset, m_trainTarget.
EvalShift is KEPT - HistoricalNetVote() uses it for the filtered overlay,
not just the corpus sweep; only its comment changed. The 2-output softmax
arm in NetForward.mqh is kept too: it costs nothing and is the reusable
binary-head path, now commented as unclaimed rather than as META's.
Compile-verified in _claude_stage: 0 errors, 0 warnings, matching the
pre-edit baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:44:52 -04:00
& & ( m_outputNeuronsCount = = 1 | | m_outputNeuronsCount = = 3 ) )
2026-08-13 10:23:11 -04:00
{
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//--- The swing label at now-relative index i only depends on price/ZigZag history, never
//--- on model state, so it's identical every era until a new bar closes and shifts the
//--- index frame (see the cache invalidation check above) - cache it rather than
//--- recomputing from scratch every single era. A cache miss here is a bar the prebuild
//--- could not resolve yet (P2 uncommitted); try again now, and a bar that is STILL
//--- unresolved is simply not trainable this era.
if ( ! m_labelCacheHasValue [ era . i ] )
AdvanceSwingLabelState ( era . i , era . bars ) ;
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
if ( m_labelCacheHasValue [ era . i ] )
{
buy = m_labelCacheBuy [ era . i ] ;
sell = m_labelCacheSell [ era . i ] ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
haveLabel = true ;
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
}
bool isOOS = ( era . i < era . oosCutoff ) ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//--- Embargo: a bar's swing label is decided by the bars that follow it, out to its
//--- pivot pair - the purge width covers the measured mean resolution lag.
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
int calibLo = CalibLoIndex ( era . oosCutoff ) ; // = oosCutoff + one purge width
int calibHi = CalibHiIndex ( era . totalIter , era . oosCutoff ) ; // == calibLo when the band is empty
bool isEmbargoed = ( ! isOOS & & era . i < calibLo ) ;
//--- The calibration slice and its far-side purge are held out of backprop for the same
//--- reason the OOS window is, and the layout is documented once at CalibLoIndex().
bool isCalib = ( era . i > = calibLo & & era . i < calibHi ) ;
bool isCalibPurge = ( calibHi > calibLo & & era . i > = calibHi & & era . i < calibHi + CalibPurgeBars ( ) ) ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//--- An unresolved bar (haveLabel false) is not trainable: queueing it would backprop a
//--- provisional Neutral against a label that does not exist yet.
wouldQueue = ( haveLabel & & ! isOOS & & ! isEmbargoed & & ! isCalib & & ! isCalibPurge ) ;
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
//--- Pass 2 re-forwards every queued bar, pass 2.5 re-forwards the whole calibration
//--- band, and pass 3 re-forwards the whole OOS window - each over EXACTLY this bar set
//--- (all three derive their bounds from the same helpers and apply the identical
//--- eligibility test this block gates on).
laterPassForwards = ( wouldQueue | | isOOS | | isCalib ) ;
2026-08-13 10:23:11 -04:00
}
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
//--- Only run this bar's feedForward (and the display/count/chart-draw work that depends
//--- on it) when NO later pass is about to redo it anyway.
ulong hbFwd = GetMicrosecondCount ( ) ;
refactor(meta): remove meta-labeling entirely - RETRAIN-NEUTRAL
~2,300 lines. META had real, repeatedly measured ranking skill and ZERO
operating points that ever cleared break-even (0/350 H1 eras, 1/999 H4
pre-2-sigma, 0/8 pooled fitted points). The clinching arithmetic was edge x
width = 0.095 ATR/trade against spread 0.099 ATR/trade, and the
dose-response showed the high-conviction tail is temporally unstable -
the precision-vs-threshold slope flips sign between calib and test on 3 of
4 symbols, so no ex-ante threshold rule exists. It shipped default-off and
never gated a live entry. The self-measured tier weights are what actually
rank the vote, and all six H4 instruments converged on them alone.
RETRAIN-NEUTRAL, and that is the property that made this safe:
- The weights fingerprint emitted "|TGT:META2" or "|TGT:SWG1" from an
if/else. Every direction model already took the SWG1 arm, so
collapsing it to an unconditional append is byte-identical. No .nnw or
.cfg is orphaned or re-keyed.
- NetInputWidth() lost its "+ MetaDescWidth()" term. MetaDescWidth()
returned 0 for every direction model, so the input layer is unchanged.
- DbLegacyAiSlot()'s slot 5 was reachable only with all four Use_* NNs
off AND meta on - a config that never shipped. Every existing .db keeps
its filename.
Deleted outright: Signals/SignalMETA.mqh, Expert/Trading/MetaGate.mqh (the
directory is now empty), Expert/Training/{MetaCorpus,MetaCandidateStore,
MetaFamilies}.mqh, Tests/Test_MetaFamilies.mq5, Meta_Labeling_Design.md.
Unwound in place, the delicate part: Training.mqh carried four
IsMetaTarget() branches whose else-arm WRAPPED the direction body (pass 1
queueing, pass 2 backprop, pass 2.5 calibration, pass 3 OOS scoring). Each
wrapper is removed and the direction body promoted back to its original
nesting - the bodies were never re-indented when the wrappers were added,
so the promoted code is byte-identical to what ran before META existed.
Also gone: the ensemble verdict's meta-veto replay and its
approved/vetoed/unscored counters, the per-family/per-side OOS
decomposition arrays, the m_isTrainQueueCand parallel queue and its
lockstep shuffle, and the S2 era report.
Also removed: the CMetaGate abstraction and the live CheckOpenPosition
veto; m_gates plus AddFilter's non-voter routing and IsVotingSignal()
(META was the only non-voting child, so m_gates was always empty);
m_parentSignal/SetParentSignal (existed only to reach the root's gate);
SweepPrepare/SweepPrepareIndicator (only caller was the corpus sweep);
IsMetaTarget() from all four view interfaces and their adapters;
Use_MetaLabeling, EnableMETA, Meta_ExportDataset, m_trainTarget.
EvalShift is KEPT - HistoricalNetVote() uses it for the filtered overlay,
not just the corpus sweep; only its comment changed. The 2-output softmax
arm in NetForward.mqh is kept too: it costs nothing and is the reusable
binary-head path, now commented as unclaimed rather than as META's.
Compile-verified in _claude_stage: 0 errors, 0 warnings, matching the
pre-edit baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:44:52 -04:00
bool scanForwardOk = ( windowOk & & ! laterPassForwards & & Net . feedForward ( TempData ) ) ;
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
m_passNetUs + = GetMicrosecondCount ( ) - hbFwd ;
if ( scanForwardOk )
feat(depth): prime -> settle -> sweep, and name which handle is short
"Max bars in chart" is set to Unlimited, so the static-terminal-limit reading in
1dda479 was wrong. Two other candidate causes are falsified too: the price series
is fully downloaded and flat (USDJPY 50,162 -> 50,163 over 80 minutes, i.e. one new
H4 bar), and the handles have been stable since 13:07 with zero windows for the 17
minutes after, so it is not download-in-progress and not handle churn. The MA period
tops out at 200 (ADIndicatorTuner MA_PERIOD_PRESETS) against ~50k bars, so it is not
indicator cost either.
What IS verified stays verified: CopyBuffer past the calculated depth fails outright
rather than short-reading, so the buffer holds nothing and every index reads
EMPTY_VALUE; m_MA is the only CiCustom whose block REJECTS on that (m_ADZigZag
neutral-fills, RSI/MACD/Ichimoku/ATR are built-ins); the wall is therefore feature 25
of every bar, exactly as the "24 of 832" stall lines said. And it is depth-correlated:
16k-bar charts train, 34k/50k get zero windows forever.
So the WHY is still open, and this fix does not depend on it. Per the user's protocol:
the request itself is the primer, so prime at full depth, then poll TunableBarsCalculated()
every 3s and hold the sweep until it stops changing (3 steady probes), then use whatever
it settled at. Bounded at 10 min, and a give-up is logged as a give-up so an abandoned
depth is never mistaken for a settled one. This supersedes 1dda479's clamp on the two
training paths, which snapshotted a value that may still have been climbing; the clamp
remains for the paths that cannot wait (inference/online/rescan/export, see 7e63a8b).
The load-bearing part is what does NOT happen while waiting: no sweep. A 50k-bar feature
scan starves the indicator threads the request just woke, which is how the failure
sustained itself for 40 minutes at a time - discard era, re-sweep, discard, which is the
0->100% oscillation on the panel.
Also adds IndicatorDepthReport(): per-handle BarsCalculated() on the priming, cap and
stall lines. The logs proved WHICH FEATURE died but never WHICH HANDLE was short, so the
cause had to be inferred - and was guessed wrong twice. The next occurrence reads it off.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 13:44:27 -04:00
{
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
Net . getResults ( TempData ) ;
if ( m_outputNeuronsCount = = 1 )
dPrevSignal = TempData [ 0 ] ;
else
if ( m_outputNeuronsCount = = 3 )
dPrevSignal = ApplyClassificationSoftmax ( ) ;
//--- Snapshot the just-computed neuron output(s) for the status label display below, before
//--- the label-check block clears/refills TempData with the target label (Step A always
//--- runs after this point now) - reading TempData directly for display after that would
//--- show the TRUE LABEL of the bar just trained on, not the network's own prediction.
if ( TempData . Total ( ) > 0 )
displayNeuron0 = TempData [ 0 ] ;
if ( TempData . Total ( ) > 1 )
displayNeuron1 = TempData [ 1 ] ;
if ( TempData . Total ( ) > 2 )
displayNeuron2 = TempData [ 2 ] ;
switch ( DoubleToSignal ( dPrevSignal ) )
{
case Buy :
m_countBuySignals + + ;
break ;
case Sell :
m_countSellSignals + + ;
break ;
default :
m_countNeutralSignals + + ;
break ;
}
m_lastBarTime = m_Time . GetData ( era . i ) ;
if ( era . i > 0 )
{
// NMS on: record only - the era-end sweep is the SOLE renderer, so no raw (un-
// declustered) arrow is ever drawn mid-era. NMS off: draw inline as before.
if ( m_signalClusterWindow > 0 )
{
if ( era . i < ArraySize ( m_arrowSignalCache ) )
m_arrowSignalCache [ era . i ] = dPrevSignal ;
}
else
if ( DoubleToSignal ( dPrevSignal ) = = Neutral )
DeleteObject ( m_lastBarTime ) ;
else
DrawObject ( m_lastBarTime , dPrevSignal , m_Close . GetData ( era . i ) ) ;
}
UpdateTrainingStatusLabel (
StringFormat ( " Bar %d of %d -> %.2f%% (scan) " , era . bars - era . i + 1 , era . bars , ( double ) ( era . bars - era . i + 1.0 ) / era . bars * 100 ) ,
displayNeuron0 , displayNeuron1 , displayNeuron2 , dPrevSignal ) ;
feat(depth): prime -> settle -> sweep, and name which handle is short
"Max bars in chart" is set to Unlimited, so the static-terminal-limit reading in
1dda479 was wrong. Two other candidate causes are falsified too: the price series
is fully downloaded and flat (USDJPY 50,162 -> 50,163 over 80 minutes, i.e. one new
H4 bar), and the handles have been stable since 13:07 with zero windows for the 17
minutes after, so it is not download-in-progress and not handle churn. The MA period
tops out at 200 (ADIndicatorTuner MA_PERIOD_PRESETS) against ~50k bars, so it is not
indicator cost either.
What IS verified stays verified: CopyBuffer past the calculated depth fails outright
rather than short-reading, so the buffer holds nothing and every index reads
EMPTY_VALUE; m_MA is the only CiCustom whose block REJECTS on that (m_ADZigZag
neutral-fills, RSI/MACD/Ichimoku/ATR are built-ins); the wall is therefore feature 25
of every bar, exactly as the "24 of 832" stall lines said. And it is depth-correlated:
16k-bar charts train, 34k/50k get zero windows forever.
So the WHY is still open, and this fix does not depend on it. Per the user's protocol:
the request itself is the primer, so prime at full depth, then poll TunableBarsCalculated()
every 3s and hold the sweep until it stops changing (3 steady probes), then use whatever
it settled at. Bounded at 10 min, and a give-up is logged as a give-up so an abandoned
depth is never mistaken for a settled one. This supersedes 1dda479's clamp on the two
training paths, which snapshotted a value that may still have been climbing; the clamp
remains for the paths that cannot wait (inference/online/rescan/export, see 7e63a8b).
The load-bearing part is what does NOT happen while waiting: no sweep. A 50k-bar feature
scan starves the indicator threads the request just woke, which is how the failure
sustained itself for 40 minutes at a time - discard era, re-sweep, discard, which is the
0->100% oscillation on the panel.
Also adds IndicatorDepthReport(): per-handle BarsCalculated() on the priming, cap and
stall lines. The logs proved WHICH FEATURE died but never WHICH HANDLE was short, so the
cause had to be inferred - and was guessed wrong twice. The next occurrence reads it off.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 13:44:27 -04:00
}
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
else
//--- Bars a later pass will re-forward skip the feedForward above, and they are now
//--- very nearly ALL of pass 1 - the queued IS bars (~58%, processed FIRST because the
//--- loop walks oldest-to-newest), plus the calibration band and the OOS slice.
UpdateTrainingStatusLabel (
StringFormat ( " Bar %d of %d -> %.2f%% (scan) " , era . bars - era . i + 1 , era . bars , ( double ) ( era . bars - era . i + 1.0 ) / era . bars * 100 ) ,
displayNeuron0 , displayNeuron1 , displayNeuron2 , dPrevSignal ) ;
refactor(meta): remove meta-labeling entirely - RETRAIN-NEUTRAL
~2,300 lines. META had real, repeatedly measured ranking skill and ZERO
operating points that ever cleared break-even (0/350 H1 eras, 1/999 H4
pre-2-sigma, 0/8 pooled fitted points). The clinching arithmetic was edge x
width = 0.095 ATR/trade against spread 0.099 ATR/trade, and the
dose-response showed the high-conviction tail is temporally unstable -
the precision-vs-threshold slope flips sign between calib and test on 3 of
4 symbols, so no ex-ante threshold rule exists. It shipped default-off and
never gated a live entry. The self-measured tier weights are what actually
rank the vote, and all six H4 instruments converged on them alone.
RETRAIN-NEUTRAL, and that is the property that made this safe:
- The weights fingerprint emitted "|TGT:META2" or "|TGT:SWG1" from an
if/else. Every direction model already took the SWG1 arm, so
collapsing it to an unconditional append is byte-identical. No .nnw or
.cfg is orphaned or re-keyed.
- NetInputWidth() lost its "+ MetaDescWidth()" term. MetaDescWidth()
returned 0 for every direction model, so the input layer is unchanged.
- DbLegacyAiSlot()'s slot 5 was reachable only with all four Use_* NNs
off AND meta on - a config that never shipped. Every existing .db keeps
its filename.
Deleted outright: Signals/SignalMETA.mqh, Expert/Trading/MetaGate.mqh (the
directory is now empty), Expert/Training/{MetaCorpus,MetaCandidateStore,
MetaFamilies}.mqh, Tests/Test_MetaFamilies.mq5, Meta_Labeling_Design.md.
Unwound in place, the delicate part: Training.mqh carried four
IsMetaTarget() branches whose else-arm WRAPPED the direction body (pass 1
queueing, pass 2 backprop, pass 2.5 calibration, pass 3 OOS scoring). Each
wrapper is removed and the direction body promoted back to its original
nesting - the bodies were never re-indented when the wrappers were added,
so the promoted code is byte-identical to what ran before META existed.
Also gone: the ensemble verdict's meta-veto replay and its
approved/vetoed/unscored counters, the per-family/per-side OOS
decomposition arrays, the m_isTrainQueueCand parallel queue and its
lockstep shuffle, and the S2 era report.
Also removed: the CMetaGate abstraction and the live CheckOpenPosition
veto; m_gates plus AddFilter's non-voter routing and IsVotingSignal()
(META was the only non-voting child, so m_gates was always empty);
m_parentSignal/SetParentSignal (existed only to reach the root's gate);
SweepPrepare/SweepPrepareIndicator (only caller was the corpus sweep);
IsMetaTarget() from all four view interfaces and their adapters;
Use_MetaLabeling, EnableMETA, Meta_ExportDataset, m_trainTarget.
EvalShift is KEPT - HistoricalNetVote() uses it for the filtered overlay,
not just the corpus sweep; only its comment changed. The 2-output softmax
arm in NetForward.mqh is kept too: it costs nothing and is the reusable
binary-head path, now commented as unclaimed rather than as META's.
Compile-verified in _claude_stage: 0 errors, 0 warnings, matching the
pre-edit baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:44:52 -04:00
if ( haveLabel )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
{
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
// True label as an ENUM_SIGNAL, derived directly from the buy/sell bools - not read
// back from TempData, which no longer holds a target at this point at all (see above).
ENUM_SIGNAL trueSignal = buy ? Buy : ( sell ? Sell : Neutral ) ;
// Track the true class distribution this era (used below to weight IS oversampling,
// and surfaced in the status label text alongside the predicted-class counts)
switch ( trueSignal )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
{
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
case Buy :
m_trueBuyCount + + ;
break ;
case Sell :
m_trueSellCount + + ;
break ;
default :
m_trueNeutralCount + + ;
break ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
}
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
// OOS scoring used to happen right here, against whatever weights this bar's earlier
// feedForward (this pass) happened to be using - which for era 0 is the network's
// still-untrained cold-start state (100% Neutral - see the output-layer bias seed's
// declaration comment), and for every later era is last era's END-of-training state,
// never THIS era's. That silently gave every era's OOS score a full one-era lag behind
// its own training, and made era 0's OOS score meaningless by construction. OOS scoring
// now happens in its own pass (see m_isPass3Active's declaration comment), AFTER pass 2
// has actually trained on this era's IS data, against a fresh feedForward on each OOS
// bar rather than this scan's now-stale one.
if ( wouldQueue )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
{
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
//--- Queue this bar for pass 2's shuffled backProp instead of training on it here,
//--- immediately, in strict chronological order - see m_isTrainQueue's declaration
//--- comment for the full rationale.
//--- MINORITY REPLAY IS GONE (2026-07-31): every bar is queued exactly ONCE and the
//--- class imbalance is corrected analytically in the gradient by the logit-adjusted
//--- loss (Menon et al. 2021).
if ( m_isTrainQueueCount + 1 > ArraySize ( m_isTrainQueue ) )
fix: the deploy gate was benchmarking a win rate against a label frequency
The gate rests on an invariant stated at ExpertSignalAIBase.mqh:199 - under a
driftless walk P(touch +k before -m) is m/(m+k), and break-even for a k:m trade
is ALSO m/(m+k), so "beats chance" and "is profitable" are the same test.
That invariant needs reward >= risk, and the measured geometry no longer
satisfies it. With target 1.62*ATR and stop 3.33*ATR, break-even is 67.3%, but
both-won bars were stripped out of Buy and Sell so the label base rate read
37.5%. chancePrecPct is max(BuyTotal,SellTotal)/bars, so the gate was clearing
models nearly 30pp short of break-even: 42% "directional precision" is +4 sigma
against 37.5% and loses money on every single trade. Live since 217b9bc.
Root cause is that label agreement stopped being the same question as trade
profitability. Buy implies winLong, but the converse fails on every both-won
bar, and the label can only name one of two directions that both pay.
So stop asking the model whether it matched a label and start asking whether
its trade paid:
- cache winLong/winShort per bar beside the label, under the same validity
flag; published from the barrier walk before the collapse to 3 classes
- dirPrecPct now counts wins on the side actually called
- chancePrecPct is max(P(winLong), P(winShort)), MEASURED - the textbook
m/(m+k) would credit SP500's drift to the model
- the NMS "what would I have made" pair, the live-fired precision, and the
IS/OOS cumulative win rates all move to the same test. IS and OOS are read
side by side as the overfitting signal, so measuring one in wins and the
other in agreement would put a fixed gap between them that has nothing to do
with generalization
- the confidence threshold is FITTED on wins too, so the operating point
maximises what the gate grades
- per-class label-agreement precision is still computed and logged; it is the
right diagnostic for class separation, just not for a deploy decision
- era line renamed dir-precision -> win-rate, chance -> chance=break-even
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 16:00:51 -04:00
{
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
int newQueueSize = m_isTrainQueueCount + 1 ;
ArrayResize ( m_isTrainQueue , newQueueSize , 16384 ) ;
fix: the deploy gate was benchmarking a win rate against a label frequency
The gate rests on an invariant stated at ExpertSignalAIBase.mqh:199 - under a
driftless walk P(touch +k before -m) is m/(m+k), and break-even for a k:m trade
is ALSO m/(m+k), so "beats chance" and "is profitable" are the same test.
That invariant needs reward >= risk, and the measured geometry no longer
satisfies it. With target 1.62*ATR and stop 3.33*ATR, break-even is 67.3%, but
both-won bars were stripped out of Buy and Sell so the label base rate read
37.5%. chancePrecPct is max(BuyTotal,SellTotal)/bars, so the gate was clearing
models nearly 30pp short of break-even: 42% "directional precision" is +4 sigma
against 37.5% and loses money on every single trade. Live since 217b9bc.
Root cause is that label agreement stopped being the same question as trade
profitability. Buy implies winLong, but the converse fails on every both-won
bar, and the label can only name one of two directions that both pay.
So stop asking the model whether it matched a label and start asking whether
its trade paid:
- cache winLong/winShort per bar beside the label, under the same validity
flag; published from the barrier walk before the collapse to 3 classes
- dirPrecPct now counts wins on the side actually called
- chancePrecPct is max(P(winLong), P(winShort)), MEASURED - the textbook
m/(m+k) would credit SP500's drift to the model
- the NMS "what would I have made" pair, the live-fired precision, and the
IS/OOS cumulative win rates all move to the same test. IS and OOS are read
side by side as the overfitting signal, so measuring one in wins and the
other in agreement would put a fixed gap between them that has nothing to do
with generalization
- the confidence threshold is FITTED on wins too, so the operating point
maximises what the gate grades
- per-class label-agreement precision is still computed and logged; it is the
right diagnostic for class separation, just not for a deploy decision
- era line renamed dir-precision -> win-rate, chance -> chance=break-even
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 16:00:51 -04:00
}
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
m_isTrainQueue [ m_isTrainQueueCount ] = era . i ;
m_isTrainQueueCount + + ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
}
}
refactor(training): Train()'s working state is one object, not eight locals
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>
2026-08-23 11:28:11 -04:00
era . stop = IsStopped ( ) | | m_trainingStopRequested ;
2026-08-23 11:40:15 -04:00
if ( ! era . stop & & era . i > 0 & & era . BudgetSpent ( ) )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
{
//--- yield: save exactly enough to resume this same era, mid-bar-loop, on the next call -
//--- see m_trainRunActive's declaration comment for why this must happen instead of
//--- letting one era (or the whole run) process synchronously to completion
refactor(training): Train()'s working state is one object, not eight locals
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>
2026-08-23 11:28:11 -04:00
StashEraResume ( era . bars , era . totalIter , era . oosCutoff , era . addLoop , era . i - 1 ) ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
return ;
}
}
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
}
//+------------------------------------------------------------------+
feat(training): wire TrainingPool into pass 2 - peer rows contribute gradient only
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>
2026-08-24 14:00:21 -04:00
//| One adopted peer row: forward, target, backward. Nothing else. |
//| |
//| The local pass-2 body cannot be reused for this. Every line of it |
//| after the forward pass reaches for something indexed by a LOCAL |
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//| bar - m_labelCache, the arrow cache, m_Time - and a peer row has |
//| no local bar. Sharing |
feat(training): wire TrainingPool into pass 2 - peer rows contribute gradient only
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>
2026-08-24 14:00:21 -04:00
//| the path would mean inventing values for all of those, which is |
//| how another instrument's outcomes end up inside this chart's IS |
//| accuracy and the operating point gets fitted to them. |
//+------------------------------------------------------------------+
void CExpertSignalAIBase : : TrainPoolStep ( const int poolIdx )
{
//--- Only the 3-class direction head is poolable. Guarded rather than assumed: a head change
//--- would otherwise silently train peer rows against a target of the wrong width.
if ( m_outputNeuronsCount ! = 3 )
return ;
int w = NetInputWidth ( ) ;
TempData . Clear ( ) ;
for ( int k = 0 ; k < w ; k + + )
TempData . Add ( m_trainPoolReader . At ( poolIdx , k ) ) ;
if ( TempData . Total ( ) < w | | ! Net . feedForward ( TempData ) )
return ;
//--- Slot order is buy / sell / neutral, identical to the local branch below, and the same label
//--- smoothing - a literal 1.0 target the sigmoid only approaches asymptotically grows weights
//--- toward the MAX_WEIGHT clamp.
int label = m_trainPoolReader . LabelAt ( poolIdx ) ;
TempData . Clear ( ) ;
TempData . Add ( label = = 0 ? LABEL_SMOOTH_HIGH : LABEL_SMOOTH_LOW ) ;
TempData . Add ( label = = 1 ? LABEL_SMOOTH_HIGH : LABEL_SMOOTH_LOW ) ;
TempData . Add ( label = = 2 ? LABEL_SMOOTH_HIGH : LABEL_SMOOTH_LOW ) ;
Net . backProp ( TempData ) ;
perf(deinit): I/O-free chart cleanup, dead-panel purge, skip clean weight saves
The 18:23 terminal close (20260825.log) killed two of six charts inside
OnDeinit: they printed "shutting down" then nothing for 5.9 s until
"Abnormal termination", stranding ~700 objects each - including the one
family no prefix sweep can reach, the control panel (CAppDialog names
its 15 objects <numeric instance id><control>, and a re-attach mints a
new id, so a killed panel is a permanent ghost; XTIUSD carried one
across sessions). The stall sat in the two file writes that preceded
all visible cleanup while the four sibling charts flooded the same
2013-era disk - the ~4x18MB-per-chart shutdown weight saves.
Three changes:
1. OnDeinit touches no file until the chart is clean. CVoteArrowStore
splits Save() into Snapshot() (the chart scan, in memory) and
WriteSnapshot() (the disk half, consuming). New order: status label,
vote-arrow snapshot, prefix sweep, panel destroy - all object ops -
then member sidecars, final sweep, timings, and only then the
visibility file, the vote-arrow write and the weight saves.
2. PurgeOrphanedPanelObjects() at OnInit: deletes numeric-prefix
CAppDialog ghosts by name (6 chrome + 9 buttons), qualifying a
prefix only when >=4 of OUR button names carry it, so a foreign
dialog sharing stock chrome names is never touched.
3. m_netDirty: set by every net mutation (both backProp sites, both
RestoreWeights sites, online learning conservatively, panel reset),
cleared only on a successful Net.Save. Shutdown AND the per-bar
autosave now skip the ~18MB write when the net is provably unchanged
- for converged ensembles that is every save - which removes the
very flood that starved the sibling charts. .stats still writes
every time (small; carries the vote record and calibration). A
skipped save leaves the .nnw header dtStudied stale, which is the
already-handled attach-after-offline-gap case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:46:51 -04:00
m_netDirty = true ;
feat(training): wire TrainingPool into pass 2 - peer rows contribute gradient only
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>
2026-08-24 14:00:21 -04:00
}
//+------------------------------------------------------------------+
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
//| PASS 2: the second sweep over the in-sample span. |
//| |
//| Ordering matters here in a way it does not in pass 1 - see |
//| dOosForecast's declaration comment for why the recursion makes |
//| this pass's direction load-bearing. |
//+------------------------------------------------------------------+
void CExpertSignalAIBase : : RunPass2 ( STrainEra & era )
{
refactor(training): Train()'s working state is one object, not eight locals
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>
2026-08-23 11:28:11 -04:00
if ( ! era . stop & & era . addLoop & & ! m_isPass2Done )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
{
if ( ! m_isPass2Active )
{
m_isPass2Active = true ;
m_isTrainCursor = 0 ;
2026-08-22 00:25:52 -04:00
//--- MINI-BATCH ON, for pass 2 only (2026-08-09 audit, F4). Switched back off where pass 2
//--- completes.
feat: mini-batch gradient accumulation (F4), front-end-aware capacity budget (F6), split Wyckoff categoricals (N1)
Completes the 2026-08-09 training audit. FORCES A RETRAIN of every
Wyckoff-enabled config (N1 re-keys the fingerprint), and BOTH DLLs must be
redeployed alongside the .ex5 - they carry new exports.
F4 - mini-batch accumulation, TRAIN_BATCH_SIZE=32. Training was pure online
SGD (one weight update per bar), which is the mechanical source of the
era-to-era whipsaw every downstream guard was built to cope with. The O(n^2)
outer product is native - AccumulateWeightGrad / AccumulateWeightGradConv /
AccumulateBufferInto in Network.cl, WarriorCPU and WarriorDML - while the
optimizer step is host-side MQL5 shared by all tiers (ApplyAccumToBlock), so
there is one Adam/SGD implementation instead of four that can drift.
- the LSTM needs no outer-product kernel (WeightsGradient already holds the
sample's full dW) but could NOT simply be left un-zeroed between samples:
CPU_LSTMSeqBackward/DML_LSTMSeqBackward memset it on entry. Hence a
separate accumulator plus an elementwise add.
- batch-norm gamma/beta accumulate in host arrays, not new BatchOptions
slots - BN_OPT_STRIDE is baked into every persisted .nnw.
- scoped to pass 2; online learning keeps immediate updates. Every save /
checkpoint / scoring boundary flushes, scaling by the real sample count.
- degrades to per-sample updates (one log line) on a tier that cannot
accumulate, so old devices and DLL-free builds are unaffected.
- verified offline: DirectML/batch_accum_check.cpp drives the real exports
against an independent reference; at B=1 the accumulator matches the
shipped unbatched kernel's own gradient to 1.1e-16. Math only - the
in-situ check remains the per-layer dW/W report on a real era.
F6 - ComputeFirstLayerWidth budgeted against the RAW input width even where a
conv/LSTM front end had already reduced it, so an LSTM's dense stack was
charged for 1,280 inputs when it receives 64. Confirmed from the deployed
.cfg files: CONV, LSTM and HYBRID were all pinned at the 16-unit floor. Now
budgeted against the front-end output and capped at it (never fan out), with
the derivation reordered so both stages settle first.
N1 - EventCode/EventPhase/StructuralPhase are signed categoricals packing
direction and Wyckoff stage into one scalar across a sign discontinuity. Split
into direction + [0,1] magnitude, the same convention the base OHLC block uses.
Information-preserving; 13 readings now occupy 16 inputs.
Compiled clean (0 errors, 0 warnings); both DLLs rebuilt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 11:48:03 -04:00
Net . SetBatchSize ( TRAIN_BATCH_SIZE ) ;
2026-08-22 00:25:52 -04:00
//--- 2026-07-28: a "replay-only optimizer override" was removed from here. It arrived with
//--- the DFA change set and was never part of any validated run. The optimizer the user
//--- selects is now the optimizer that runs.
feat(training): wire TrainingPool into pass 2 - peer rows contribute gradient only
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>
2026-08-24 14:00:21 -04:00
//--- CROSS-INSTRUMENT ROWS join the queue HERE, before the shuffle, so peer samples are
//--- interleaved with this chart's rather than trained in a block at one end - a block would
//--- be a curriculum, and the last thing the optimizer saw would decide where it landed.
//--- They ride as NEGATIVE sentinels (-(poolIndex+1)); the loop below dispatches on the sign.
//--- The purge cutoff is the OLDEST OOS BAR'S TIME: a peer row whose label resolved at or
//--- after that instant carries information from a window this model is about to be judged
//--- on. Bar indices cannot be compared across instruments - each has its own calendar - so
//--- the key is wall-clock on both sides.
if ( TrainPoolEnabled ( ) )
{
m_trainPoolWriter . Begin ( BuildModelFingerprint ( ) , m_symbol . Name ( ) , ( int ) m_period ) ;
long cutoffMs = ( long ) m_Time . GetData ( era . oosCutoff ) * 1000 ;
int adopted = m_trainPoolReader . Adopt ( BuildModelFingerprint ( ) , m_symbol . Name ( ) ,
( int ) m_period , cutoffMs , NetInputWidth ( ) ) ;
if ( adopted > 0 )
{
int base = m_isTrainQueueCount ;
ArrayResize ( m_isTrainQueue , base + adopted , 16384 ) ;
for ( int p = 0 ; p < adopted ; p + + )
m_isTrainQueue [ base + p ] = - ( p + 1 ) ;
m_isTrainQueueCount + = adopted ;
}
fix(training-pool): say why a peer was rejected instead of adopting nothing in silence
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>
2026-08-24 14:43:12 -04:00
//--- No print here. CTrainPoolReader::Adopt reports its own verdict - adopted, alone, or
//--- every peer rejected and why - and reports it once per CHANGE rather than once per
//--- era, because an era on a warm feature cache is a fraction of a second long.
feat(training): wire TrainingPool into pass 2 - peer rows contribute gradient only
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>
2026-08-24 14:00:21 -04:00
}
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
for ( int sIdx = m_isTrainQueueCount - 1 ; sIdx > 0 ; sIdx - - )
{
fix: training-stability audit fixes F1/F2/F3/F5 - unbiased shuffle, real plateau escapes, fresh optimizer state on restore, pure OOS metric
Four of the six findings from research/training_pipeline_audit_2026-08-09.md
(F4 mini-batching and F6 feature re-encode deliberately deferred - see the
report's implementation-status section for why):
- F1: pass-2 Fisher-Yates (and AutoTune's MI block shuffle) used MathRand()%,
which is 15-bit - provably non-uniform on every full-history era over 32,768
queued samples. New 30-bit ShuffleRandomIndex().
- F2: plateau warm restarts were a no-op whenever eta already sat at its
ceiling (the normal state of a non-regressing plateau) - the ladder was just
a 24-era countdown. Restarts now overshoot to 5x the ceiling
(PLATEAU_RESTART_BOOST) and anneal geometrically back over the patience
window, SGDR-style; ETA_MIN widened 1e-4 -> 1e-5 so the decay schedule has
real range.
- F3: checkpoint restores put weights back but kept the rejected trajectory's
Adam moments, so the optimizer immediately pushed back toward the rolled-back
state (the restore->regress->restore oscillation). CNet::ResetOptimizerState()
zeroes moments/momentum/step counters (weights, BN statistics, gamma/beta
untouched) on every mid-run restore, every boosted restart, and the
deploy-time restore that online learning continues from.
- F5: batch-norm running statistics now freeze for the pass-3 OOS scoring walk,
so the selection metric the checkpoint ranking and deploy gate read is a pure
function of the checkpoint instead of partly measuring BN drift. Defensive
unfreeze in FinalizeTrainRun covers stop-mid-pass; live/online adaptation and
the OOS continual-learning simulation stay adaptive by design.
Compiled clean (0 errors, 0 warnings) via the staged-tree recipe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 10:54:09 -04:00
//--- ShuffleRandomIndex, NOT MathRand()%: the queue routinely exceeds MathRand()'s 15-bit
//--- range on a full-history window, which silently biased this shuffle - see the helper.
int sJ = ShuffleRandomIndex ( sIdx + 1 ) ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
int sTmp = m_isTrainQueue [ sIdx ] ;
m_isTrainQueue [ sIdx ] = m_isTrainQueue [ sJ ] ;
m_isTrainQueue [ sJ ] = sTmp ;
}
}
for ( ; m_isTrainCursor < m_isTrainQueueCount ; m_isTrainCursor + + )
{
int qi = m_isTrainQueue [ m_isTrainCursor ] ;
2026-08-10 07:22:29 -04:00
TrainHeartbeat ( " pass 2 (shuffled backprop), sample " , m_isTrainCursor + 1 , m_isTrainQueueCount , " training " ) ;
feat(training): wire TrainingPool into pass 2 - peer rows contribute gradient only
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>
2026-08-24 14:00:21 -04:00
//--- A PEER ROW contributes GRADIENT ONLY and then leaves. Everything below this point is
//--- keyed on a LOCAL bar index - the excursion head, the chart arrows, m_labelCache, and
//--- the IS accuracy counters - and a peer bar has none of those. Letting one through would
//--- not crash; it would quietly pollute m_cumIsCorrect and the operating-point fit with
//--- another instrument's outcomes, and the IS-vs-OOS gap is read as THE overfitting signal.
if ( qi < 0 )
{
TrainPoolStep ( - qi - 1 ) ;
continue ;
}
diag: slow eras must explain themselves - heartbeat + era time split + pass-1 paint
The 23:42 restart left all four charts grinding ~25x slower than the 18:01
baseline (era lines in 86 seconds there; 20+ minutes of nothing here), and
NOTHING could say why from outside: pass 1 logs nothing, its status paint sat
inside the !wouldQueue branch so the IS sweep - 80% of the pass, processed
FIRST - painted nothing either, the VPS has no debugger for a thread stack,
and the hourly new-bar cache invalidation cancels and restarts an unfinished
era, so a slow era can stay invisible FOREVER. Externals gave: four chart
threads at ~95% pure user-mode compute, DLL pool idle, no file writes. That
narrows it to "MQL5-side per-item work in the era passes" and no further.
So training now explains itself:
- TrainHeartbeat: one line per 4096 processed items, only after an era has
already run 60s, at most 6 lines per era - a healthy era stays exactly as
quiet as before. Reports position and the cumulative split: feature-window
builds vs net forward/backprop vs everything else. Hooked into all three
passes.
- The era summary line gains "| ERA TOOK Ns (feature windows X, net fwd/back
Y, other Z)" whenever an era exceeded 120s.
- Pass 1 paints its progress for QUEUED bars too, not just the OOS slice, so
the panel shows "learning (era N)" instead of sitting on the idle writer's
"Getting ready..." for the entire IS sweep. The label is throttled
internally; painting per bar costs nothing.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 00:10:45 -04:00
ulong hbT = GetMicrosecondCount ( ) ;
fix: the sequence models were reading the window backwards
BuildFeatureWindow() replaces eight hand-rolled copies of the same loop
and feeds the window OLDEST BAR FIRST. Every copy fed it newest-first,
because MQL5 timeseries indices run backwards and `r + b` with b ascending
walks into the past.
Harmless for PAI and CONV - a dense layer learns a weight per position
either way, a conv learns time-mirrored kernels. Not harmless for the
recurrent stacks:
- LSTM_SeqStepForward reads `inputs + t*Iw`, so step t is block t.
- It writes output[] only when t == steps-1: the visible output IS the
last hidden state.
- c_t = f*c_{t-1} + i*g decays toward the start of the sequence.
lstm_seq_flowcheck.cpp measured block 0's influence on the output at
1.2e-2 of block T-1's, at the shipped forget bias of 1.0.
So the bar being PREDICTED sat at the far end of the decay and the output
was handed to the OLDEST bar in the window - the exact inverse of what the
window is for. ~80x backwards on LSTM and HYBRID, on all three tiers
(OpenCL kernel, CPU DLL, pure-MQL5 inference), which is why it never
surfaced as a backend discrepancy.
This does not create edge - the MI diagnostics read at the noise floor
(p=0.4975) with a working positive control. It makes the one hypothesis
those diagnostics explicitly do NOT cover testable: they are marginal and
per-bar, and state they "cannot rule out one that only exists in
combination or across time". The sequence model is the instrument for
across-time structure and it has been crippled, so that hypothesis has
never been honestly tested.
Fingerprint gets an unconditional |WIN:2 - the vector keeps its shape and
its features, so a stale .nnw would load cleanly and run a model fitted to
one ordering against the other, silently. Re-keying every config is the
point, not collateral damage. FORCES A FULL RETRAIN.
Also: the now-relative bar caches are re-keyed on the two live paths.
EnsureBarCachesCapacity() was only ever called from training paths, but
once m_trainingComplete is set ScheduleTrainingIfNeeded() routes every bar
to RefreshConvergedSignal() and Train() is never re-entered - so nothing
cleared the feature cache again for the life of the process. A chart that
trained to convergence kept replaying the rows computed for the last
training era's bar grid: the live signal froze at its convergence-time
value, and OnlineLearnStep() backpropped those stale features against
freshly resolved labels. Backtests were never affected (an inference-only
process never allocates the arrays, so every read recomputes).
Compiles clean: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 18:28:44 -04:00
bool qWindowOk = BuildFeatureWindow ( qi ) ;
feat(training): wire TrainingPool into pass 2 - peer rows contribute gradient only
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>
2026-08-24 14:00:21 -04:00
//--- Contribute this row to the pool while the window is still in TempData and before the
refactor(meta): remove meta-labeling entirely - RETRAIN-NEUTRAL
~2,300 lines. META had real, repeatedly measured ranking skill and ZERO
operating points that ever cleared break-even (0/350 H1 eras, 1/999 H4
pre-2-sigma, 0/8 pooled fitted points). The clinching arithmetic was edge x
width = 0.095 ATR/trade against spread 0.099 ATR/trade, and the
dose-response showed the high-conviction tail is temporally unstable -
the precision-vs-threshold slope flips sign between calib and test on 3 of
4 symbols, so no ex-ante threshold rule exists. It shipped default-off and
never gated a live entry. The self-measured tier weights are what actually
rank the vote, and all six H4 instruments converged on them alone.
RETRAIN-NEUTRAL, and that is the property that made this safe:
- The weights fingerprint emitted "|TGT:META2" or "|TGT:SWG1" from an
if/else. Every direction model already took the SWG1 arm, so
collapsing it to an unconditional append is byte-identical. No .nnw or
.cfg is orphaned or re-keyed.
- NetInputWidth() lost its "+ MetaDescWidth()" term. MetaDescWidth()
returned 0 for every direction model, so the input layer is unchanged.
- DbLegacyAiSlot()'s slot 5 was reachable only with all four Use_* NNs
off AND meta on - a config that never shipped. Every existing .db keeps
its filename.
Deleted outright: Signals/SignalMETA.mqh, Expert/Trading/MetaGate.mqh (the
directory is now empty), Expert/Training/{MetaCorpus,MetaCandidateStore,
MetaFamilies}.mqh, Tests/Test_MetaFamilies.mq5, Meta_Labeling_Design.md.
Unwound in place, the delicate part: Training.mqh carried four
IsMetaTarget() branches whose else-arm WRAPPED the direction body (pass 1
queueing, pass 2 backprop, pass 2.5 calibration, pass 3 OOS scoring). Each
wrapper is removed and the direction body promoted back to its original
nesting - the bodies were never re-indented when the wrappers were added,
so the promoted code is byte-identical to what ran before META existed.
Also gone: the ensemble verdict's meta-veto replay and its
approved/vetoed/unscored counters, the per-family/per-side OOS
decomposition arrays, the m_isTrainQueueCand parallel queue and its
lockstep shuffle, and the S2 era report.
Also removed: the CMetaGate abstraction and the live CheckOpenPosition
veto; m_gates plus AddFilter's non-voter routing and IsVotingSignal()
(META was the only non-voting child, so m_gates was always empty);
m_parentSignal/SetParentSignal (existed only to reach the root's gate);
SweepPrepare/SweepPrepareIndicator (only caller was the corpus sweep);
IsMetaTarget() from all four view interfaces and their adapters;
Use_MetaLabeling, EnableMETA, Meta_ExportDataset, m_trainTarget.
EvalShift is KEPT - HistoricalNetVote() uses it for the filtered overlay,
not just the corpus sweep; only its comment changed. The 2-output softmax
arm in NetForward.mqh is kept too: it costs nothing and is the reusable
binary-head path, now commented as unclaimed rather than as META's.
Compile-verified in _claude_stage: 0 errors, 0 warnings, matching the
pre-edit baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:44:52 -04:00
//--- forward pass overwrites it.
if ( qWindowOk & & TrainPoolEnabled ( ) & & m_labelCacheHasValue [ qi ] )
feat(training): wire TrainingPool into pass 2 - peer rows contribute gradient only
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>
2026-08-24 14:00:21 -04:00
m_trainPoolWriter . Add ( TempData ,
m_labelCacheBuy [ qi ] ? 0 : ( m_labelCacheSell [ qi ] ? 1 : 2 ) ,
TrainPoolResolvedMs ( qi ) ) ;
diag: slow eras must explain themselves - heartbeat + era time split + pass-1 paint
The 23:42 restart left all four charts grinding ~25x slower than the 18:01
baseline (era lines in 86 seconds there; 20+ minutes of nothing here), and
NOTHING could say why from outside: pass 1 logs nothing, its status paint sat
inside the !wouldQueue branch so the IS sweep - 80% of the pass, processed
FIRST - painted nothing either, the VPS has no debugger for a thread stack,
and the hourly new-bar cache invalidation cancels and restarts an unfinished
era, so a slow era can stay invisible FOREVER. Externals gave: four chart
threads at ~95% pure user-mode compute, DLL pool idle, no file writes. That
narrows it to "MQL5-side per-item work in the era passes" and no further.
So training now explains itself:
- TrainHeartbeat: one line per 4096 processed items, only after an era has
already run 60s, at most 6 lines per era - a healthy era stays exactly as
quiet as before. Reports position and the cumulative split: feature-window
builds vs net forward/backprop vs everything else. Hooked into all three
passes.
- The era summary line gains "| ERA TOOK Ns (feature windows X, net fwd/back
Y, other Z)" whenever an era exceeded 120s.
- Pass 1 paints its progress for QUEUED bars too, not just the OOS slice, so
the panel shows "learning (era N)" instead of sitting on the idle writer's
"Getting ready..." for the entire IS sweep. The label is throttled
internally; painting per bar costs nothing.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 00:10:45 -04:00
m_passFeatUs + = GetMicrosecondCount ( ) - hbT ;
2026-08-02 01:09:18 -04:00
//--- A failed forward pass must NOT be followed by backProp() further down this block: the
//--- output layer would still hold the PREVIOUS sample's activations, so the update would be
//--- this bar's label against another bar's prediction - training on pure noise while every
//--- accuracy counter kept reporting normally.
diag: slow eras must explain themselves - heartbeat + era time split + pass-1 paint
The 23:42 restart left all four charts grinding ~25x slower than the 18:01
baseline (era lines in 86 seconds there; 20+ minutes of nothing here), and
NOTHING could say why from outside: pass 1 logs nothing, its status paint sat
inside the !wouldQueue branch so the IS sweep - 80% of the pass, processed
FIRST - painted nothing either, the VPS has no debugger for a thread stack,
and the hourly new-bar cache invalidation cancels and restarts an unfinished
era, so a slow era can stay invisible FOREVER. Externals gave: four chart
threads at ~95% pure user-mode compute, DLL pool idle, no file writes. That
narrows it to "MQL5-side per-item work in the era passes" and no further.
So training now explains itself:
- TrainHeartbeat: one line per 4096 processed items, only after an era has
already run 60s, at most 6 lines per era - a healthy era stays exactly as
quiet as before. Reports position and the cumulative split: feature-window
builds vs net forward/backprop vs everything else. Hooked into all three
passes.
- The era summary line gains "| ERA TOOK Ns (feature windows X, net fwd/back
Y, other Z)" whenever an era exceeded 120s.
- Pass 1 paints its progress for QUEUED bars too, not just the OOS slice, so
the panel shows "learning (era N)" instead of sitting on the idle writer's
"Getting ready..." for the entire IS sweep. The label is throttled
internally; painting per bar costs nothing.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 00:10:45 -04:00
hbT = GetMicrosecondCount ( ) ;
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus
The NN now has a target that is not per-bar direction (closed, best-of-999
p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of
cost). One net for all 52 pattern-sides, AIType=AI_META.
- NetForward.mqh: the host-side softmax+CE gradient generalized total==3 ->
2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no
compute backend changes.
- SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on
disk (decoupled from the config fingerprint that burned four S1 runs); the
GMT->server offset is measured PER ROW against entryPrice vs bar open
(DST-immune, histogram logged); a window-span regime filter drops the
pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the
input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR).
- Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate
calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell
lets checkpoint selection, the edge floor, the plateau ladder and the
family-wise deploy gate run UNCHANGED: precision reads as win rate among
traded candidates, chance as the base win rate, recalls as sensitivity/
specificity. Era-end META line: coverage x (p - break-even) vs the null.
- Labels are the side-conditional triple-barrier win caches - never the DB's
stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base
rate). Live inference + online learning guarded off until S3.
- Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename
slot keep meta models fully separate from direction models.
Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with
AIType=AI_META; S3 wires the votes via the per-side hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
bool qForwardOk = ( qWindowOk & & TempData . Total ( ) > = NetInputWidth ( ) & &
2026-08-02 01:09:18 -04:00
Net . feedForward ( TempData ) ) ;
diag: slow eras must explain themselves - heartbeat + era time split + pass-1 paint
The 23:42 restart left all four charts grinding ~25x slower than the 18:01
baseline (era lines in 86 seconds there; 20+ minutes of nothing here), and
NOTHING could say why from outside: pass 1 logs nothing, its status paint sat
inside the !wouldQueue branch so the IS sweep - 80% of the pass, processed
FIRST - painted nothing either, the VPS has no debugger for a thread stack,
and the hourly new-bar cache invalidation cancels and restarts an unfinished
era, so a slow era can stay invisible FOREVER. Externals gave: four chart
threads at ~95% pure user-mode compute, DLL pool idle, no file writes. That
narrows it to "MQL5-side per-item work in the era passes" and no further.
So training now explains itself:
- TrainHeartbeat: one line per 4096 processed items, only after an era has
already run 60s, at most 6 lines per era - a healthy era stays exactly as
quiet as before. Reports position and the cumulative split: feature-window
builds vs net forward/backprop vs everything else. Hooked into all three
passes.
- The era summary line gains "| ERA TOOK Ns (feature windows X, net fwd/back
Y, other Z)" whenever an era exceeded 120s.
- Pass 1 paints its progress for QUEUED bars too, not just the OOS slice, so
the panel shows "learning (era N)" instead of sitting on the idle writer's
"Getting ready..." for the entire IS sweep. The label is throttled
internally; painting per bar costs nothing.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 00:10:45 -04:00
m_passNetUs + = GetMicrosecondCount ( ) - hbT ;
refactor(training): Train()'s working state is one object, not eight locals
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>
2026-08-23 11:28:11 -04:00
if ( qWindowOk & & ! qForwardOk & & ! era . forwardFailureReported )
2026-08-02 01:09:18 -04:00
{
refactor(training): Train()'s working state is one object, not eight locals
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>
2026-08-23 11:28:11 -04:00
era . forwardFailureReported = true ;
2026-08-02 01:09:18 -04:00
Print ( __FUNCTION__ + " : CNet::feedForward FAILED at era " + IntegerToString ( ( int ) m_eraCount ) +
" - this era's remaining samples are being skipped, not trained. A layer is refusing to "
" accept its own output (check the preceding BufferWrite/BufferRead lines for which "
" buffer, and see NormalizeHost in AI \\ NeuronBatchNorm.mqh for the batch-norm case). " ) ;
}
refactor(meta): remove meta-labeling entirely - RETRAIN-NEUTRAL
~2,300 lines. META had real, repeatedly measured ranking skill and ZERO
operating points that ever cleared break-even (0/350 H1 eras, 1/999 H4
pre-2-sigma, 0/8 pooled fitted points). The clinching arithmetic was edge x
width = 0.095 ATR/trade against spread 0.099 ATR/trade, and the
dose-response showed the high-conviction tail is temporally unstable -
the precision-vs-threshold slope flips sign between calib and test on 3 of
4 symbols, so no ex-ante threshold rule exists. It shipped default-off and
never gated a live entry. The self-measured tier weights are what actually
rank the vote, and all six H4 instruments converged on them alone.
RETRAIN-NEUTRAL, and that is the property that made this safe:
- The weights fingerprint emitted "|TGT:META2" or "|TGT:SWG1" from an
if/else. Every direction model already took the SWG1 arm, so
collapsing it to an unconditional append is byte-identical. No .nnw or
.cfg is orphaned or re-keyed.
- NetInputWidth() lost its "+ MetaDescWidth()" term. MetaDescWidth()
returned 0 for every direction model, so the input layer is unchanged.
- DbLegacyAiSlot()'s slot 5 was reachable only with all four Use_* NNs
off AND meta on - a config that never shipped. Every existing .db keeps
its filename.
Deleted outright: Signals/SignalMETA.mqh, Expert/Trading/MetaGate.mqh (the
directory is now empty), Expert/Training/{MetaCorpus,MetaCandidateStore,
MetaFamilies}.mqh, Tests/Test_MetaFamilies.mq5, Meta_Labeling_Design.md.
Unwound in place, the delicate part: Training.mqh carried four
IsMetaTarget() branches whose else-arm WRAPPED the direction body (pass 1
queueing, pass 2 backprop, pass 2.5 calibration, pass 3 OOS scoring). Each
wrapper is removed and the direction body promoted back to its original
nesting - the bodies were never re-indented when the wrappers were added,
so the promoted code is byte-identical to what ran before META existed.
Also gone: the ensemble verdict's meta-veto replay and its
approved/vetoed/unscored counters, the per-family/per-side OOS
decomposition arrays, the m_isTrainQueueCand parallel queue and its
lockstep shuffle, and the S2 era report.
Also removed: the CMetaGate abstraction and the live CheckOpenPosition
veto; m_gates plus AddFilter's non-voter routing and IsVotingSignal()
(META was the only non-voting child, so m_gates was always empty);
m_parentSignal/SetParentSignal (existed only to reach the root's gate);
SweepPrepare/SweepPrepareIndicator (only caller was the corpus sweep);
IsMetaTarget() from all four view interfaces and their adapters;
Use_MetaLabeling, EnableMETA, Meta_ExportDataset, m_trainTarget.
EvalShift is KEPT - HistoricalNetVote() uses it for the filtered overlay,
not just the corpus sweep; only its comment changed. The 2-output softmax
arm in NetForward.mqh is kept too: it costs nothing and is the reusable
binary-head path, now commented as unclaimed rather than as META's.
Compile-verified in _claude_stage: 0 errors, 0 warnings, matching the
pre-edit baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:44:52 -04:00
if ( qForwardOk )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
{
Net . getResults ( TempData ) ;
// Must go through ApplyClassificationSoftmax() (3-output case) before reading the
// per-class values below - Net.getResults() returns each output neuron's own independent
// SIGMOID activation (each already in [0,1] but NOT summing to 1 across the three), not a
// true class-conditional probability distribution; ApplyClassificationSoftmax() is what
// turns that into one (and is also what pass 1/3's displayNeuron0/1/2 already go through).
double qPrevSignal = ( m_outputNeuronsCount = = 3 ) ? ApplyClassificationSoftmax ( ) : TempData [ 0 ] ;
double pt0 = ( TempData . Total ( ) > 0 ) ? TempData [ 0 ] : 0.0 ;
double pt1 = ( TempData . Total ( ) > 1 ) ? TempData [ 1 ] : 0.0 ;
double pt2 = ( TempData . Total ( ) > 2 ) ? TempData [ 2 ] : 0.0 ;
bool qBuy = m_labelCacheHasValue [ qi ] ? m_labelCacheBuy [ qi ] : false ;
bool qSell = m_labelCacheHasValue [ qi ] ? m_labelCacheSell [ qi ] : false ;
ENUM_SIGNAL qTrueSignal = qBuy ? Buy : ( qSell ? Sell : Neutral ) ;
UpdateTrainingStatusLabel (
StringFormat ( " Training bar %d of %d -> %.2f%% (shuffled) " , m_isTrainCursor + 1 , m_isTrainQueueCount , ( double ) ( m_isTrainCursor + 1.0 ) / MathMax ( m_isTrainQueueCount , 1 ) * 100 ) ,
pt0 , pt1 , pt2 , qPrevSignal ) ;
2026-08-22 00:25:52 -04:00
//--- Predicted-signal tally, chart marker, and IS-accuracy stat that pass 1 used to
//--- compute from its own (now-removed) redundant feedForward on this same bar - see
//--- pass 1's wouldQueue comment.
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
switch ( DoubleToSignal ( qPrevSignal ) )
{
case Buy :
m_countBuySignals + + ;
break ;
case Sell :
m_countSellSignals + + ;
break ;
default :
m_countNeutralSignals + + ;
break ;
}
datetime qBarTime = m_Time . GetData ( qi ) ;
// NMS on: record only (the era-end sweep renders); off: draw inline. See pass 1's note.
if ( m_signalClusterWindow > 0 )
{
if ( qi < ArraySize ( m_arrowSignalCache ) )
m_arrowSignalCache [ qi ] = qPrevSignal ;
}
else
if ( DoubleToSignal ( qPrevSignal ) = = Neutral )
DeleteObject ( qBarTime ) ;
else
feat(chart): signal marks become price LEVELS at the trigger, not arrows beside the candle
User request: 'move from arrows on lows and highs to small horizontal lines at the actual
prices the entry/exit would trigger, just a bit larger than the candles. dark green for
buy, dark red for sell.'
Every mark is now an OBJ_TREND segment with both anchors at one price and both rays off,
spanning 1.3 bar widths, drawn at the bar's CLOSE - the price a market order actually
fires at, and the exact entry TripleBarrierLabel assumes. It used to sit on the candle's
LOW for a Buy and its HIGH for a Sell: prices the trade never touches, picked so an arrow
glyph would clear the candle. The tooltip now carries that price too.
COLOUR NOW MEANS DIRECTION AND ONLY DIRECTION on every layer (dark green / dark red).
Layer moves to width+style - the traded vote is solid and thick and drawn in front, a
single model's raw opinion is thin, dotted and behind the candles - which keeps the
distinction the old palette existed to draw (a model's opinion must never read as a trade)
while freeing colour to say one thing consistently.
Consequences handled, all of them the same 'a typed scan went blind' failure:
- SaveChartSignals filtered OBJPROP_TYPE == OBJ_ARROW and read OBJPROP_ARROWCODE. It now
filters OBJ_TREND and recovers direction from the colour. The sidecar keeps the old
217/218 numbers as its buy/sell token deliberately, so existing .arrows files still load.
- AdvanceChartSignalRestore now rebuilds through the SAME creation point the live path
uses, so a restored mark and a fresh one are identical objects.
- The rescan-scoped delete enumerated ObjectsTotal(OBJ_ARROW) - retyped, or it silently
deletes nothing.
- ApplySignalsVisibility enumerated OBJ_ARROW with NO prefix filter. Under the new type
that would have hidden and shown THE USER'S OWN trend lines on every Hide/Show click;
it is now prefix-scoped. The old type was uncommon enough on a real chart to mask the
missing check - trend lines are the most hand-drawn object there is.
- DrawObject's high/low parameters are gone (6 call sites pass m_Close instead), so no
caller can hand it a price it no longer draws at.
- Fixed a pre-existing stale comment that still described the purge sweep as OBJ_ARROW-only
three lines above the note explaining it had been widened to every type.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 14:11:49 -04:00
DrawObject ( qBarTime , qPrevSignal , m_Close . GetData ( qi ) ) ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
bool qClassified = ( DoubleToSignal ( qPrevSignal ) = = Buy | | DoubleToSignal ( qPrevSignal ) = = Sell | | DoubleToSignal ( qPrevSignal ) = = Neutral ) ;
if ( qClassified )
{
bool isHit = ( DoubleToSignal ( qPrevSignal ) = = qTrueSignal ) ;
if ( isHit )
dForecast + = ( 100 - dForecast ) / Net . recentAverageSmoothingFactor ;
else
dForecast - = dForecast / Net . recentAverageSmoothingFactor ;
dUndefine - = dUndefine / Net . recentAverageSmoothingFactor ;
2026-08-22 00:25:52 -04:00
//--- Compounded, persistent DIRECTIONAL win-rate: count only bars the model actually
//--- called Buy or Sell (a Neutral "no trade" call is neither a win nor a loss), so
//--- this tracks the accuracy of its directional signals rather than the Neutral-
//--- inflated all-class rate.
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
ENUM_SIGNAL qPred = DoubleToSignal ( qPrevSignal ) ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//--- Directional calls scored on label agreement, identically to the OOS side,
//--- because the IS and OOS rates are read side by side as the overfitting signal.
refactor(training): delete the minority-replay machinery that stopped running in July
Two arrays parallel to m_isTrainQueue existed only to serve replay: a
per-occurrence sample weight, and a "count this bar once" flag that kept
the reported IS accuracy on the natural class distribution while backProp
trained on the oversampled one. Replay was removed on 2026-07-31 - every
bar has been queued exactly once since - and the scaffolding was left
standing, provably constant:
m_isTrainQueueWeightScale[] - written 1.0 at both queue sites, swapped
through the Fisher-Yates shuffle to stay in lockstep with nothing,
read into a variable passed to backProp, whose own default is 1.0.
m_isTrainQueuePrimary[] - written true at both sites, swapped the
same way, and read as two guards that could not be false.
Also a `for(int rep = 0; rep < repCount; rep++)` around a hardcoded
repCount = 1, and both arrays preallocated at totalIter * 4 - about
1.5MB per model of always-constant data, on a six-core box that trains
four of them at once. Behaviour is unchanged by construction: every
removed read had one possible value.
m_maxClassSampleWeight goes with them - declared, initialised to 1.5 in
the constructor, read nowhere, and documented as "currently unread by
that path... kept in case a smaller, additive loss-level nudge is ever
reintroduced". That is the definition of YAGNI, and its 17-line comment
described the class-balance correction as data-level oversampling, which
has not been true since July.
Comment pass on ExpertSignalAIBase.mqh, -164 lines this session with
every constant and measured number kept. One correction worth naming:
the header carried 15 lines arguing for HARD 0/1 one-hot targets and
explaining why smoothing was no longer needed - directly above
LABEL_SMOOTH_HIGH 0.9 / LABEL_SMOOTH_LOW 0.05, which every training path
actually uses. It described the opposite of what ships.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:30:51 -04:00
if ( qPred = = Buy | | qPred = = Sell )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
{
m_cumIsTotal + + ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
if ( isHit )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
m_cumIsCorrect + + ;
}
2026-08-22 00:25:52 -04:00
//--- THE OPERATING-POINT FIT NO LONGER HARVESTS HERE. It moved to the held-out
//--- calibration walk below; DIR_CONF_CALIB_PCT_OF_IS carries the measured IS-vs-OOS
//--- divergence that forced the move.
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
}
else
if ( qBuy & & qSell )
dUndefine + = ( 100 - dUndefine ) / Net . recentAverageSmoothingFactor ;
TempData . Clear ( ) ;
if ( m_outputNeuronsCount = = 1 )
TempData . Add ( qBuy & & ! qSell ? 1 : ! qBuy & & qSell ? -1 : 0 ) ;
else
if ( m_outputNeuronsCount = = 3 )
{
TempData . Add ( qBuy ? LABEL_SMOOTH_HIGH : LABEL_SMOOTH_LOW ) ;
TempData . Add ( qSell ? LABEL_SMOOTH_HIGH : LABEL_SMOOTH_LOW ) ;
TempData . Add ( ( ! qBuy & & ! qSell ) ? LABEL_SMOOTH_HIGH : LABEL_SMOOTH_LOW ) ;
}
2026-08-22 00:25:52 -04:00
//--- FOCAL-LOSS MODULATION REMOVED 2026-07-31. It multiplied this weight by
//--- (1-pt)^gamma, a second correction on the same axis as the logit adjustment - the
//--- stacking failure Buda et al.
diag: slow eras must explain themselves - heartbeat + era time split + pass-1 paint
The 23:42 restart left all four charts grinding ~25x slower than the 18:01
baseline (era lines in 86 seconds there; 20+ minutes of nothing here), and
NOTHING could say why from outside: pass 1 logs nothing, its status paint sat
inside the !wouldQueue branch so the IS sweep - 80% of the pass, processed
FIRST - painted nothing either, the VPS has no debugger for a thread stack,
and the hourly new-bar cache invalidation cancels and restarts an unfinished
era, so a slow era can stay invisible FOREVER. Externals gave: four chart
threads at ~95% pure user-mode compute, DLL pool idle, no file writes. That
narrows it to "MQL5-side per-item work in the era passes" and no further.
So training now explains itself:
- TrainHeartbeat: one line per 4096 processed items, only after an era has
already run 60s, at most 6 lines per era - a healthy era stays exactly as
quiet as before. Reports position and the cumulative split: feature-window
builds vs net forward/backprop vs everything else. Hooked into all three
passes.
- The era summary line gains "| ERA TOOK Ns (feature windows X, net fwd/back
Y, other Z)" whenever an era exceeded 120s.
- Pass 1 paints its progress for QUEUED bars too, not just the OOS slice, so
the panel shows "learning (era N)" instead of sitting on the idle writer's
"Getting ready..." for the entire IS sweep. The label is throttled
internally; painting per bar costs nothing.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 00:10:45 -04:00
ulong hbBp = GetMicrosecondCount ( ) ;
refactor(training): delete the minority-replay machinery that stopped running in July
Two arrays parallel to m_isTrainQueue existed only to serve replay: a
per-occurrence sample weight, and a "count this bar once" flag that kept
the reported IS accuracy on the natural class distribution while backProp
trained on the oversampled one. Replay was removed on 2026-07-31 - every
bar has been queued exactly once since - and the scaffolding was left
standing, provably constant:
m_isTrainQueueWeightScale[] - written 1.0 at both queue sites, swapped
through the Fisher-Yates shuffle to stay in lockstep with nothing,
read into a variable passed to backProp, whose own default is 1.0.
m_isTrainQueuePrimary[] - written true at both sites, swapped the
same way, and read as two guards that could not be false.
Also a `for(int rep = 0; rep < repCount; rep++)` around a hardcoded
repCount = 1, and both arrays preallocated at totalIter * 4 - about
1.5MB per model of always-constant data, on a six-core box that trains
four of them at once. Behaviour is unchanged by construction: every
removed read had one possible value.
m_maxClassSampleWeight goes with them - declared, initialised to 1.5 in
the constructor, read nowhere, and documented as "currently unread by
that path... kept in case a smaller, additive loss-level nudge is ever
reintroduced". That is the definition of YAGNI, and its 17-line comment
described the class-balance correction as data-level oversampling, which
has not been true since July.
Comment pass on ExpertSignalAIBase.mqh, -164 lines this session with
every constant and measured number kept. One correction worth naming:
the header carried 15 lines arguing for HARD 0/1 one-hot targets and
explaining why smoothing was no longer needed - directly above
LABEL_SMOOTH_HIGH 0.9 / LABEL_SMOOTH_LOW 0.05, which every training path
actually uses. It described the opposite of what ships.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:30:51 -04:00
//--- No per-sample weight: the imbalance correction is analytic (logit-adjusted loss), so
//--- backProp's own sampleWeight default of 1.0 is the shipped behaviour.
Net . backProp ( TempData ) ;
perf(deinit): I/O-free chart cleanup, dead-panel purge, skip clean weight saves
The 18:23 terminal close (20260825.log) killed two of six charts inside
OnDeinit: they printed "shutting down" then nothing for 5.9 s until
"Abnormal termination", stranding ~700 objects each - including the one
family no prefix sweep can reach, the control panel (CAppDialog names
its 15 objects <numeric instance id><control>, and a re-attach mints a
new id, so a killed panel is a permanent ghost; XTIUSD carried one
across sessions). The stall sat in the two file writes that preceded
all visible cleanup while the four sibling charts flooded the same
2013-era disk - the ~4x18MB-per-chart shutdown weight saves.
Three changes:
1. OnDeinit touches no file until the chart is clean. CVoteArrowStore
splits Save() into Snapshot() (the chart scan, in memory) and
WriteSnapshot() (the disk half, consuming). New order: status label,
vote-arrow snapshot, prefix sweep, panel destroy - all object ops -
then member sidecars, final sweep, timings, and only then the
visibility file, the vote-arrow write and the weight saves.
2. PurgeOrphanedPanelObjects() at OnInit: deletes numeric-prefix
CAppDialog ghosts by name (6 chrome + 9 buttons), qualifying a
prefix only when >=4 of OUR button names carry it, so a foreign
dialog sharing stock chrome names is never touched.
3. m_netDirty: set by every net mutation (both backProp sites, both
RestoreWeights sites, online learning conservatively, panel reset),
cleared only on a successful Net.Save. Shutdown AND the per-bar
autosave now skip the ~18MB write when the net is provably unchanged
- for converged ensembles that is every save - which removes the
very flood that starved the sibling charts. .stats still writes
every time (small; carries the vote record and calibration). A
skipped save leaves the .nnw header dtStudied stale, which is the
already-handled attach-after-offline-gap case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:46:51 -04:00
m_netDirty = true ;
diag: slow eras must explain themselves - heartbeat + era time split + pass-1 paint
The 23:42 restart left all four charts grinding ~25x slower than the 18:01
baseline (era lines in 86 seconds there; 20+ minutes of nothing here), and
NOTHING could say why from outside: pass 1 logs nothing, its status paint sat
inside the !wouldQueue branch so the IS sweep - 80% of the pass, processed
FIRST - painted nothing either, the VPS has no debugger for a thread stack,
and the hourly new-bar cache invalidation cancels and restarts an unfinished
era, so a slow era can stay invisible FOREVER. Externals gave: four chart
threads at ~95% pure user-mode compute, DLL pool idle, no file writes. That
narrows it to "MQL5-side per-item work in the era passes" and no further.
So training now explains itself:
- TrainHeartbeat: one line per 4096 processed items, only after an era has
already run 60s, at most 6 lines per era - a healthy era stays exactly as
quiet as before. Reports position and the cumulative split: feature-window
builds vs net forward/backprop vs everything else. Hooked into all three
passes.
- The era summary line gains "| ERA TOOK Ns (feature windows X, net fwd/back
Y, other Z)" whenever an era exceeded 120s.
- Pass 1 paints its progress for QUEUED bars too, not just the OOS slice, so
the panel shows "learning (era N)" instead of sitting on the idle writer's
"Getting ready..." for the entire IS sweep. The label is throttled
internally; painting per bar costs nothing.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 00:10:45 -04:00
m_passNetUs + = GetMicrosecondCount ( ) - hbBp ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
}
2026-08-22 00:25:52 -04:00
//--- YIELD ON TIME **OR** ON A STOP REQUEST. The time budget bounds THROUGHPUT; it does
//--- not bound LATENCY to an unload. Pass 1 has checked IsStopped() all along; passes 2,
//--- 2.5 and 3 never did, and they are the ones that grow with history.
2026-08-23 11:40:15 -04:00
if ( m_isTrainCursor + 1 < m_isTrainQueueCount & & ( IsStopped ( ) | | era . BudgetSpent ( ) ) )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
{
//--- yield: save enough to resume PASS 2 mid-queue on the next call - m_isPass2Active
//--- and m_isTrainCursor (both members) carry the actual resume position; bars/oosCutoff/
//--- add_loop are stashed the same way pass 1 already does, since era-end logic just
//--- below still needs them once pass 2 finishes.
refactor(training): Train()'s working state is one object, not eight locals
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>
2026-08-23 11:28:11 -04:00
StashEraResume ( era . bars , era . totalIter , era . oosCutoff , era . addLoop , era . i ) ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
return ;
}
}
2026-08-22 00:25:52 -04:00
//--- Apply whatever the final (usually short) batch of this era accumulated, and return the
//--- net to per-sample updates. FlushBatch scales by the REAL sample count, so a short
//--- trailing batch still takes a correctly-sized step.
feat: mini-batch gradient accumulation (F4), front-end-aware capacity budget (F6), split Wyckoff categoricals (N1)
Completes the 2026-08-09 training audit. FORCES A RETRAIN of every
Wyckoff-enabled config (N1 re-keys the fingerprint), and BOTH DLLs must be
redeployed alongside the .ex5 - they carry new exports.
F4 - mini-batch accumulation, TRAIN_BATCH_SIZE=32. Training was pure online
SGD (one weight update per bar), which is the mechanical source of the
era-to-era whipsaw every downstream guard was built to cope with. The O(n^2)
outer product is native - AccumulateWeightGrad / AccumulateWeightGradConv /
AccumulateBufferInto in Network.cl, WarriorCPU and WarriorDML - while the
optimizer step is host-side MQL5 shared by all tiers (ApplyAccumToBlock), so
there is one Adam/SGD implementation instead of four that can drift.
- the LSTM needs no outer-product kernel (WeightsGradient already holds the
sample's full dW) but could NOT simply be left un-zeroed between samples:
CPU_LSTMSeqBackward/DML_LSTMSeqBackward memset it on entry. Hence a
separate accumulator plus an elementwise add.
- batch-norm gamma/beta accumulate in host arrays, not new BatchOptions
slots - BN_OPT_STRIDE is baked into every persisted .nnw.
- scoped to pass 2; online learning keeps immediate updates. Every save /
checkpoint / scoring boundary flushes, scaling by the real sample count.
- degrades to per-sample updates (one log line) on a tier that cannot
accumulate, so old devices and DLL-free builds are unaffected.
- verified offline: DirectML/batch_accum_check.cpp drives the real exports
against an independent reference; at B=1 the accumulator matches the
shipped unbatched kernel's own gradient to 1.1e-16. Math only - the
in-situ check remains the per-layer dW/W report on a real era.
F6 - ComputeFirstLayerWidth budgeted against the RAW input width even where a
conv/LSTM front end had already reduced it, so an LSTM's dense stack was
charged for 1,280 inputs when it receives 64. Confirmed from the deployed
.cfg files: CONV, LSTM and HYBRID were all pinned at the 16-unit floor. Now
budgeted against the front-end output and capped at it (never fan out), with
the derivation reordered so both stages settle first.
N1 - EventCode/EventPhase/StructuralPhase are signed categoricals packing
direction and Wyckoff stage into one scalar across a sign discontinuity. Split
into direction + [0,1] magnitude, the same convention the base OHLC block uses.
Information-preserving; 13 readings now occupy 16 inputs.
Compiled clean (0 errors, 0 warnings); both DLLs rebuilt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 11:48:03 -04:00
Net . FlushBatch ( ) ;
Net . SetBatchSize ( 1 ) ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
m_isPass2Active = false ;
m_isPass2Done = true ;
feat(training): wire TrainingPool into pass 2 - peer rows contribute gradient only
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>
2026-08-24 14:00:21 -04:00
//--- Publish this chart's rows once the era's queue is fully walked, so a peer never reads a
//--- partial sweep. The buffer is refilled from scratch each era rather than accumulated: one
//--- era already covers the whole in-sample span, so accumulating would republish the same
//--- bars N times and inflate this instrument's weight in every peer's pool.
if ( TrainPoolEnabled ( ) & & m_trainPoolWriter . Count ( ) > 0 )
m_trainPoolWriter . Publish ( ) ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
}
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
}
//+------------------------------------------------------------------+
//| CALIBRATION PASS: fit the directional confidence threshold on a |
//| PURGED held-out slice. |
//| |
//| Separate from pass 2 because it must not see bars the weights |
//| were fitted on - a threshold fitted on memorised bars is the |
//| single easiest way to manufacture an edge that does not exist. |
//+------------------------------------------------------------------+
void CExpertSignalAIBase : : RunCalibrationPass ( STrainEra & era )
{
refactor(training): Train()'s working state is one object, not eight locals
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>
2026-08-23 11:28:11 -04:00
if ( ! era . stop & & era . addLoop & & ! m_isCalibDone )
fix: the operating point was fitted on bars the net had memorized
FitDirConfThreshold harvested its margin histogram from pass 2's own
backprop samples. Pairing every fit against the same era's OOS result
shows what that measured:
PAI era 1 IS 25% cov @ 66.1% (-0.8pp) -> OOS 64% (-3pp) gap +2.1pp
PAI era 76 IS 90% cov @ 79.6% (+12.7pp) -> OOS 65% (-2pp) gap +14.6pp
LSTM era 9 IS 77% cov @ 81.6% (+14.6pp) -> OOS 63% (-4pp) gap +18.6pp
The gap grows monotonically while OOS stays flat, so within a handful of
eras the curve stops describing behaviour on unseen bars. That is fatal
here specifically, because the objective branches on the SIGN of
(p - break-even): the memorized curve reads +12pp at 95% coverage, so
coverage x (p - p0) correctly maximises coverage and returns ~0.02 - fire
on every bar. The "p < p0 -> get more selective" branch, which is the
actual regime and the entire point of 983a6a3, could never fire because IS
never showed p < p0.
Carve a calibration slice out of the IS span - DIR_CONF_CALIB_PCT_OF_IS,
purged from backprop by one label horizon on BOTH sides (the far-side
purge is not optional: without it the newest training bars carry labels
partly decided by price action inside the slice, putting the memorization
straight back into the curve). Score it in a new chunked pass 2.5, after
pass 2 has trained and before pass 3 grades - the only position where the
histogram is simultaneously not-trained-on, not-graded, and current with
the weights it will be applied to.
Costs 15% of the training data. Worth it beyond honesty: the deploy gate
needs dirPrecPct > chance + EDGE_MIN_SIGMAS*SE, and a threshold pinned
near zero dilutes any edge concentrated in the confident bars across every
bar the model calls, driving dirPrecPct toward chance by construction. A
threshold that can be selective is the only mechanism by which a small,
concentrated edge could ever clear that gate.
Also: a sparse histogram now KEEPS the previous threshold instead of
resetting to 0.0. A failed measurement must not decay to the most exposed
setting in the range.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 15:58:18 -04:00
{
refactor(training): Train()'s working state is one object, not eight locals
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>
2026-08-23 11:28:11 -04:00
int calibLo = CalibLoIndex ( era . oosCutoff ) ;
int calibHi = CalibHiIndex ( era . totalIter , era . oosCutoff ) ;
fix: the operating point was fitted on bars the net had memorized
FitDirConfThreshold harvested its margin histogram from pass 2's own
backprop samples. Pairing every fit against the same era's OOS result
shows what that measured:
PAI era 1 IS 25% cov @ 66.1% (-0.8pp) -> OOS 64% (-3pp) gap +2.1pp
PAI era 76 IS 90% cov @ 79.6% (+12.7pp) -> OOS 65% (-2pp) gap +14.6pp
LSTM era 9 IS 77% cov @ 81.6% (+14.6pp) -> OOS 63% (-4pp) gap +18.6pp
The gap grows monotonically while OOS stays flat, so within a handful of
eras the curve stops describing behaviour on unseen bars. That is fatal
here specifically, because the objective branches on the SIGN of
(p - break-even): the memorized curve reads +12pp at 95% coverage, so
coverage x (p - p0) correctly maximises coverage and returns ~0.02 - fire
on every bar. The "p < p0 -> get more selective" branch, which is the
actual regime and the entire point of 983a6a3, could never fire because IS
never showed p < p0.
Carve a calibration slice out of the IS span - DIR_CONF_CALIB_PCT_OF_IS,
purged from backprop by one label horizon on BOTH sides (the far-side
purge is not optional: without it the newest training bars carry labels
partly decided by price action inside the slice, putting the memorization
straight back into the curve). Score it in a new chunked pass 2.5, after
pass 2 has trained and before pass 3 grades - the only position where the
histogram is simultaneously not-trained-on, not-graded, and current with
the weights it will be applied to.
Costs 15% of the training data. Worth it beyond honesty: the deploy gate
needs dirPrecPct > chance + EDGE_MIN_SIGMAS*SE, and a threshold pinned
near zero dilutes any edge concentrated in the confident bars across every
bar the model calls, driving dirPrecPct toward chance by construction. A
threshold that can be selective is the only mechanism by which a small,
concentrated edge could ever clear that gate.
Also: a sparse histogram now KEEPS the previous threshold instead of
resetting to 0.0. A failed measurement must not decay to the most exposed
setting in the range.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 15:58:18 -04:00
if ( ! m_isCalibActive )
{
m_isCalibActive = true ;
Net . SetBatchNormFrozen ( true ) ;
ResetDirConfHistogram ( ) ;
//--- Same upper clamp pass 3 applies: a bar needs m_historyBars of older bars behind it to
//--- build a window at all, so the band is trimmed to what is actually scoreable.
refactor(training): Train()'s working state is one object, not eight locals
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>
2026-08-23 11:28:11 -04:00
m_calibStartIndex = ( int ) MathMin ( calibHi - 1 , era . bars - MathMax ( m_historyBars , 0 ) - 2 ) ;
fix: the operating point was fitted on bars the net had memorized
FitDirConfThreshold harvested its margin histogram from pass 2's own
backprop samples. Pairing every fit against the same era's OOS result
shows what that measured:
PAI era 1 IS 25% cov @ 66.1% (-0.8pp) -> OOS 64% (-3pp) gap +2.1pp
PAI era 76 IS 90% cov @ 79.6% (+12.7pp) -> OOS 65% (-2pp) gap +14.6pp
LSTM era 9 IS 77% cov @ 81.6% (+14.6pp) -> OOS 63% (-4pp) gap +18.6pp
The gap grows monotonically while OOS stays flat, so within a handful of
eras the curve stops describing behaviour on unseen bars. That is fatal
here specifically, because the objective branches on the SIGN of
(p - break-even): the memorized curve reads +12pp at 95% coverage, so
coverage x (p - p0) correctly maximises coverage and returns ~0.02 - fire
on every bar. The "p < p0 -> get more selective" branch, which is the
actual regime and the entire point of 983a6a3, could never fire because IS
never showed p < p0.
Carve a calibration slice out of the IS span - DIR_CONF_CALIB_PCT_OF_IS,
purged from backprop by one label horizon on BOTH sides (the far-side
purge is not optional: without it the newest training bars carry labels
partly decided by price action inside the slice, putting the memorization
straight back into the curve). Score it in a new chunked pass 2.5, after
pass 2 has trained and before pass 3 grades - the only position where the
histogram is simultaneously not-trained-on, not-graded, and current with
the weights it will be applied to.
Costs 15% of the training data. Worth it beyond honesty: the deploy gate
needs dirPrecPct > chance + EDGE_MIN_SIGMAS*SE, and a threshold pinned
near zero dilutes any edge concentrated in the confident bars across every
bar the model calls, driving dirPrecPct toward chance by construction. A
threshold that can be selective is the only mechanism by which a small,
concentrated edge could ever clear that gate.
Also: a sparse histogram now KEEPS the previous threshold instead of
resetting to 0.0. A failed measurement must not decay to the most exposed
setting in the range.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 15:58:18 -04:00
m_calibIndex = m_calibStartIndex ;
}
for ( ; m_calibIndex > = calibLo ; m_calibIndex - - )
{
int ci = m_calibIndex ;
//--- Same eligibility test pass 1 gates labelling on (its line reads
//--- `i < bars-historyBars-1 && i > 1 && Time[i] > dtStudied`), so this walk can only score bars
//--- pass 1 actually produced a label for. Pass 3 applies the identical test on its own window.
refactor(training): Train()'s working state is one object, not eight locals
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>
2026-08-23 11:28:11 -04:00
if ( ! ( ci < ( int ) ( era . bars - MathMax ( m_historyBars , 0 ) - 1 ) & & ci > 1 & & m_Time . GetData ( ci ) > dtStudied ) )
fix: the operating point was fitted on bars the net had memorized
FitDirConfThreshold harvested its margin histogram from pass 2's own
backprop samples. Pairing every fit against the same era's OOS result
shows what that measured:
PAI era 1 IS 25% cov @ 66.1% (-0.8pp) -> OOS 64% (-3pp) gap +2.1pp
PAI era 76 IS 90% cov @ 79.6% (+12.7pp) -> OOS 65% (-2pp) gap +14.6pp
LSTM era 9 IS 77% cov @ 81.6% (+14.6pp) -> OOS 63% (-4pp) gap +18.6pp
The gap grows monotonically while OOS stays flat, so within a handful of
eras the curve stops describing behaviour on unseen bars. That is fatal
here specifically, because the objective branches on the SIGN of
(p - break-even): the memorized curve reads +12pp at 95% coverage, so
coverage x (p - p0) correctly maximises coverage and returns ~0.02 - fire
on every bar. The "p < p0 -> get more selective" branch, which is the
actual regime and the entire point of 983a6a3, could never fire because IS
never showed p < p0.
Carve a calibration slice out of the IS span - DIR_CONF_CALIB_PCT_OF_IS,
purged from backprop by one label horizon on BOTH sides (the far-side
purge is not optional: without it the newest training bars carry labels
partly decided by price action inside the slice, putting the memorization
straight back into the curve). Score it in a new chunked pass 2.5, after
pass 2 has trained and before pass 3 grades - the only position where the
histogram is simultaneously not-trained-on, not-graded, and current with
the weights it will be applied to.
Costs 15% of the training data. Worth it beyond honesty: the deploy gate
needs dirPrecPct > chance + EDGE_MIN_SIGMAS*SE, and a threshold pinned
near zero dilutes any edge concentrated in the confident bars across every
bar the model calls, driving dirPrecPct toward chance by construction. A
threshold that can be selective is the only mechanism by which a small,
concentrated edge could ever clear that gate.
Also: a sparse histogram now KEEPS the previous threshold instead of
resetting to 0.0. A failed measurement must not decay to the most exposed
setting in the range.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 15:58:18 -04:00
continue ;
TrainHeartbeat ( " pass 2.5 (calibration), bar " , m_calibStartIndex - m_calibIndex + 1 ,
m_calibStartIndex - calibLo + 1 , " calibrating " ) ;
ulong hbC = GetMicrosecondCount ( ) ;
bool cWindowOk = BuildFeatureWindow ( ci ) ;
m_passFeatUs + = GetMicrosecondCount ( ) - hbC ;
hbC = GetMicrosecondCount ( ) ;
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus
The NN now has a target that is not per-bar direction (closed, best-of-999
p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of
cost). One net for all 52 pattern-sides, AIType=AI_META.
- NetForward.mqh: the host-side softmax+CE gradient generalized total==3 ->
2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no
compute backend changes.
- SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on
disk (decoupled from the config fingerprint that burned four S1 runs); the
GMT->server offset is measured PER ROW against entryPrice vs bar open
(DST-immune, histogram logged); a window-span regime filter drops the
pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the
input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR).
- Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate
calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell
lets checkpoint selection, the edge floor, the plateau ladder and the
family-wise deploy gate run UNCHANGED: precision reads as win rate among
traded candidates, chance as the base win rate, recalls as sensitivity/
specificity. Era-end META line: coverage x (p - break-even) vs the null.
- Labels are the side-conditional triple-barrier win caches - never the DB's
stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base
rate). Live inference + online learning guarded off until S3.
- Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename
slot keep meta models fully separate from direction models.
Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with
AIType=AI_META; S3 wires the votes via the per-side hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
bool cForwardOk = ( cWindowOk & & TempData . Total ( ) > = NetInputWidth ( ) & &
fix: the operating point was fitted on bars the net had memorized
FitDirConfThreshold harvested its margin histogram from pass 2's own
backprop samples. Pairing every fit against the same era's OOS result
shows what that measured:
PAI era 1 IS 25% cov @ 66.1% (-0.8pp) -> OOS 64% (-3pp) gap +2.1pp
PAI era 76 IS 90% cov @ 79.6% (+12.7pp) -> OOS 65% (-2pp) gap +14.6pp
LSTM era 9 IS 77% cov @ 81.6% (+14.6pp) -> OOS 63% (-4pp) gap +18.6pp
The gap grows monotonically while OOS stays flat, so within a handful of
eras the curve stops describing behaviour on unseen bars. That is fatal
here specifically, because the objective branches on the SIGN of
(p - break-even): the memorized curve reads +12pp at 95% coverage, so
coverage x (p - p0) correctly maximises coverage and returns ~0.02 - fire
on every bar. The "p < p0 -> get more selective" branch, which is the
actual regime and the entire point of 983a6a3, could never fire because IS
never showed p < p0.
Carve a calibration slice out of the IS span - DIR_CONF_CALIB_PCT_OF_IS,
purged from backprop by one label horizon on BOTH sides (the far-side
purge is not optional: without it the newest training bars carry labels
partly decided by price action inside the slice, putting the memorization
straight back into the curve). Score it in a new chunked pass 2.5, after
pass 2 has trained and before pass 3 grades - the only position where the
histogram is simultaneously not-trained-on, not-graded, and current with
the weights it will be applied to.
Costs 15% of the training data. Worth it beyond honesty: the deploy gate
needs dirPrecPct > chance + EDGE_MIN_SIGMAS*SE, and a threshold pinned
near zero dilutes any edge concentrated in the confident bars across every
bar the model calls, driving dirPrecPct toward chance by construction. A
threshold that can be selective is the only mechanism by which a small,
concentrated edge could ever clear that gate.
Also: a sparse histogram now KEEPS the previous threshold instead of
resetting to 0.0. A failed measurement must not decay to the most exposed
setting in the range.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 15:58:18 -04:00
Net . feedForward ( TempData ) ) ;
m_passNetUs + = GetMicrosecondCount ( ) - hbC ;
if ( cForwardOk )
{
Net . getResults ( TempData ) ;
2026-08-22 00:25:52 -04:00
//--- RAW argmax softmax, NOT AdjustedSignalFromSoftmax(): feeding the fit its own
//--- already- thresholded decisions would make the threshold a fixed point of itself,
//--- able only to ratchet upward.
fix: the operating point was fitted on bars the net had memorized
FitDirConfThreshold harvested its margin histogram from pass 2's own
backprop samples. Pairing every fit against the same era's OOS result
shows what that measured:
PAI era 1 IS 25% cov @ 66.1% (-0.8pp) -> OOS 64% (-3pp) gap +2.1pp
PAI era 76 IS 90% cov @ 79.6% (+12.7pp) -> OOS 65% (-2pp) gap +14.6pp
LSTM era 9 IS 77% cov @ 81.6% (+14.6pp) -> OOS 63% (-4pp) gap +18.6pp
The gap grows monotonically while OOS stays flat, so within a handful of
eras the curve stops describing behaviour on unseen bars. That is fatal
here specifically, because the objective branches on the SIGN of
(p - break-even): the memorized curve reads +12pp at 95% coverage, so
coverage x (p - p0) correctly maximises coverage and returns ~0.02 - fire
on every bar. The "p < p0 -> get more selective" branch, which is the
actual regime and the entire point of 983a6a3, could never fire because IS
never showed p < p0.
Carve a calibration slice out of the IS span - DIR_CONF_CALIB_PCT_OF_IS,
purged from backprop by one label horizon on BOTH sides (the far-side
purge is not optional: without it the newest training bars carry labels
partly decided by price action inside the slice, putting the memorization
straight back into the curve). Score it in a new chunked pass 2.5, after
pass 2 has trained and before pass 3 grades - the only position where the
histogram is simultaneously not-trained-on, not-graded, and current with
the weights it will be applied to.
Costs 15% of the training data. Worth it beyond honesty: the deploy gate
needs dirPrecPct > chance + EDGE_MIN_SIGMAS*SE, and a threshold pinned
near zero dilutes any edge concentrated in the confident bars across every
bar the model calls, driving dirPrecPct toward chance by construction. A
threshold that can be selective is the only mechanism by which a small,
concentrated edge could ever clear that gate.
Also: a sparse histogram now KEEPS the previous threshold instead of
resetting to 0.0. A failed measurement must not decay to the most exposed
setting in the range.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 15:58:18 -04:00
double cSignal = ( m_outputNeuronsCount = = 3 ) ? ApplyClassificationSoftmax ( ) : TempData [ 0 ] ;
ENUM_SIGNAL cPred = DoubleToSignal ( cSignal ) ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//--- Scored on LABEL AGREEMENT - the same currency every other verdict in the run uses.
//--- An unresolved bar has no label to agree with, so it joins neither the numerator nor
//--- the denominator of the fit.
bool cBuy = ( m_labelCacheHasValue [ ci ] & & m_labelCacheBuy [ ci ] ) ;
bool cSell = ( m_labelCacheHasValue [ ci ] & & m_labelCacheSell [ ci ] ) ;
bool cHit = ( cPred = = Buy ) ? cBuy : ( ( cPred = = Sell ) ? cSell : false ) ;
fix: the operating point was fitted on bars the net had memorized
FitDirConfThreshold harvested its margin histogram from pass 2's own
backprop samples. Pairing every fit against the same era's OOS result
shows what that measured:
PAI era 1 IS 25% cov @ 66.1% (-0.8pp) -> OOS 64% (-3pp) gap +2.1pp
PAI era 76 IS 90% cov @ 79.6% (+12.7pp) -> OOS 65% (-2pp) gap +14.6pp
LSTM era 9 IS 77% cov @ 81.6% (+14.6pp) -> OOS 63% (-4pp) gap +18.6pp
The gap grows monotonically while OOS stays flat, so within a handful of
eras the curve stops describing behaviour on unseen bars. That is fatal
here specifically, because the objective branches on the SIGN of
(p - break-even): the memorized curve reads +12pp at 95% coverage, so
coverage x (p - p0) correctly maximises coverage and returns ~0.02 - fire
on every bar. The "p < p0 -> get more selective" branch, which is the
actual regime and the entire point of 983a6a3, could never fire because IS
never showed p < p0.
Carve a calibration slice out of the IS span - DIR_CONF_CALIB_PCT_OF_IS,
purged from backprop by one label horizon on BOTH sides (the far-side
purge is not optional: without it the newest training bars carry labels
partly decided by price action inside the slice, putting the memorization
straight back into the curve). Score it in a new chunked pass 2.5, after
pass 2 has trained and before pass 3 grades - the only position where the
histogram is simultaneously not-trained-on, not-graded, and current with
the weights it will be applied to.
Costs 15% of the training data. Worth it beyond honesty: the deploy gate
needs dirPrecPct > chance + EDGE_MIN_SIGMAS*SE, and a threshold pinned
near zero dilutes any edge concentrated in the confident bars across every
bar the model calls, driving dirPrecPct toward chance by construction. A
threshold that can be selective is the only mechanism by which a small,
concentrated edge could ever clear that gate.
Also: a sparse histogram now KEEPS the previous threshold instead of
resetting to 0.0. A failed measurement must not decay to the most exposed
setting in the range.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 15:58:18 -04:00
//--- isPrimaryBar is unconditionally true: this walk visits each bar once in chronological
//--- order, so there is no oversampled replay to correct for here.
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
if ( m_labelCacheHasValue [ ci ] )
AccumulateDirConfSample ( DirectionalMargin ( ) , cHit , true ) ;
2026-08-22 00:25:52 -04:00
//--- Predicted-class tally and chart marker for the calibration band, which pass 1 used
//--- to compute from its own (now-removed) redundant feedForward on this same bar.
perf: pass 1 forward-passed ~40% of bars that a later pass redid anyway
Pass 1 already skipped its feedForward on QUEUED bars, because pass 2 redoes
them. The same argument covers two more bands it was still forwarding:
OOS window (30% of bars) - pass 3 re-forwards every one of them
calibration band (~10% of bars) - pass 2.5 re-forwards every one of them
All three passes derive their bounds from the same helpers and apply the
identical eligibility test, so the bar sets are equal by construction, not by
coincidence. Only the two purge bands and the ineligible edge bars are visited
in pass 1 and nowhere else - those keep their forward pass.
The scan's copy was never the one that survived. Its arrow-cache write was
overwritten by pass 3's (with the thresholded, post-training decision), its
status-label paint was transient, and its predicted-class tally measured
last era's weights. Those tallies move to pass 2.5 and pass 3, on the raw
argmax exactly as pass 1 and pass 2 count it, so the population behind the
panel's "Predicted -> Buy/Sell/Neutral" line is unchanged and stays comparable
with the "Actual" line beside it, which pass 1 still accumulates over every
labelled bar.
Verified unaffected by the cut: dPrevSignal and m_lastBarTime are both written
last by bars 0/1, which are label-ineligible and therefore still forwarded, so
FinalizeTrainRun's `dtStudied = m_lastBarTime` and Lifecycle's newBarPending
sentinel read the same values as before.
Correctness, not just speed: batch norm is UNFROZEN during pass 1 (passes 2.5
and 3 freeze it deliberately), so every scan-time forward on a held-out bar was
advancing the BN running mean/variance from data the model is graded on. Those
running statistics are inference-time model state. It is the mild,
unsupervised kind of leakage - feature statistics, not labels - but it fed the
weights pass 3 then scored, and it is now gone.
Cost: ~40% of all bars lose one forward pass per era, ~16% of net time once
pass 2's backward pass is weighted in. Per-dispatch, so it lands on every
backend.
Both variants compile 0 errors / 0 warnings. Build tag scan-nofwd-v5.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:50:14 -04:00
switch ( cPred )
{
case Buy :
m_countBuySignals + + ;
break ;
case Sell :
m_countSellSignals + + ;
break ;
default :
m_countNeutralSignals + + ;
break ;
}
datetime cBarTime = m_Time . GetData ( ci ) ;
if ( ci > 0 )
{
// NMS on: record only (the era-end sweep renders); off: draw inline. See pass 1's note.
if ( m_signalClusterWindow > 0 )
{
if ( ci < ArraySize ( m_arrowSignalCache ) )
m_arrowSignalCache [ ci ] = cSignal ;
}
else
if ( cPred = = Neutral )
DeleteObject ( cBarTime ) ;
else
feat(chart): signal marks become price LEVELS at the trigger, not arrows beside the candle
User request: 'move from arrows on lows and highs to small horizontal lines at the actual
prices the entry/exit would trigger, just a bit larger than the candles. dark green for
buy, dark red for sell.'
Every mark is now an OBJ_TREND segment with both anchors at one price and both rays off,
spanning 1.3 bar widths, drawn at the bar's CLOSE - the price a market order actually
fires at, and the exact entry TripleBarrierLabel assumes. It used to sit on the candle's
LOW for a Buy and its HIGH for a Sell: prices the trade never touches, picked so an arrow
glyph would clear the candle. The tooltip now carries that price too.
COLOUR NOW MEANS DIRECTION AND ONLY DIRECTION on every layer (dark green / dark red).
Layer moves to width+style - the traded vote is solid and thick and drawn in front, a
single model's raw opinion is thin, dotted and behind the candles - which keeps the
distinction the old palette existed to draw (a model's opinion must never read as a trade)
while freeing colour to say one thing consistently.
Consequences handled, all of them the same 'a typed scan went blind' failure:
- SaveChartSignals filtered OBJPROP_TYPE == OBJ_ARROW and read OBJPROP_ARROWCODE. It now
filters OBJ_TREND and recovers direction from the colour. The sidecar keeps the old
217/218 numbers as its buy/sell token deliberately, so existing .arrows files still load.
- AdvanceChartSignalRestore now rebuilds through the SAME creation point the live path
uses, so a restored mark and a fresh one are identical objects.
- The rescan-scoped delete enumerated ObjectsTotal(OBJ_ARROW) - retyped, or it silently
deletes nothing.
- ApplySignalsVisibility enumerated OBJ_ARROW with NO prefix filter. Under the new type
that would have hidden and shown THE USER'S OWN trend lines on every Hide/Show click;
it is now prefix-scoped. The old type was uncommon enough on a real chart to mask the
missing check - trend lines are the most hand-drawn object there is.
- DrawObject's high/low parameters are gone (6 call sites pass m_Close instead), so no
caller can hand it a price it no longer draws at.
- Fixed a pre-existing stale comment that still described the purge sweep as OBJ_ARROW-only
three lines above the note explaining it had been widened to every type.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 14:11:49 -04:00
DrawObject ( cBarTime , cSignal , m_Close . GetData ( ci ) ) ;
perf: pass 1 forward-passed ~40% of bars that a later pass redid anyway
Pass 1 already skipped its feedForward on QUEUED bars, because pass 2 redoes
them. The same argument covers two more bands it was still forwarding:
OOS window (30% of bars) - pass 3 re-forwards every one of them
calibration band (~10% of bars) - pass 2.5 re-forwards every one of them
All three passes derive their bounds from the same helpers and apply the
identical eligibility test, so the bar sets are equal by construction, not by
coincidence. Only the two purge bands and the ineligible edge bars are visited
in pass 1 and nowhere else - those keep their forward pass.
The scan's copy was never the one that survived. Its arrow-cache write was
overwritten by pass 3's (with the thresholded, post-training decision), its
status-label paint was transient, and its predicted-class tally measured
last era's weights. Those tallies move to pass 2.5 and pass 3, on the raw
argmax exactly as pass 1 and pass 2 count it, so the population behind the
panel's "Predicted -> Buy/Sell/Neutral" line is unchanged and stays comparable
with the "Actual" line beside it, which pass 1 still accumulates over every
labelled bar.
Verified unaffected by the cut: dPrevSignal and m_lastBarTime are both written
last by bars 0/1, which are label-ineligible and therefore still forwarded, so
FinalizeTrainRun's `dtStudied = m_lastBarTime` and Lifecycle's newBarPending
sentinel read the same values as before.
Correctness, not just speed: batch norm is UNFROZEN during pass 1 (passes 2.5
and 3 freeze it deliberately), so every scan-time forward on a held-out bar was
advancing the BN running mean/variance from data the model is graded on. Those
running statistics are inference-time model state. It is the mild,
unsupervised kind of leakage - feature statistics, not labels - but it fed the
weights pass 3 then scored, and it is now gone.
Cost: ~40% of all bars lose one forward pass per era, ~16% of net time once
pass 2's backward pass is weighted in. Per-dispatch, so it lands on every
backend.
Both variants compile 0 errors / 0 warnings. Build tag scan-nofwd-v5.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:50:14 -04:00
}
fix: the operating point was fitted on bars the net had memorized
FitDirConfThreshold harvested its margin histogram from pass 2's own
backprop samples. Pairing every fit against the same era's OOS result
shows what that measured:
PAI era 1 IS 25% cov @ 66.1% (-0.8pp) -> OOS 64% (-3pp) gap +2.1pp
PAI era 76 IS 90% cov @ 79.6% (+12.7pp) -> OOS 65% (-2pp) gap +14.6pp
LSTM era 9 IS 77% cov @ 81.6% (+14.6pp) -> OOS 63% (-4pp) gap +18.6pp
The gap grows monotonically while OOS stays flat, so within a handful of
eras the curve stops describing behaviour on unseen bars. That is fatal
here specifically, because the objective branches on the SIGN of
(p - break-even): the memorized curve reads +12pp at 95% coverage, so
coverage x (p - p0) correctly maximises coverage and returns ~0.02 - fire
on every bar. The "p < p0 -> get more selective" branch, which is the
actual regime and the entire point of 983a6a3, could never fire because IS
never showed p < p0.
Carve a calibration slice out of the IS span - DIR_CONF_CALIB_PCT_OF_IS,
purged from backprop by one label horizon on BOTH sides (the far-side
purge is not optional: without it the newest training bars carry labels
partly decided by price action inside the slice, putting the memorization
straight back into the curve). Score it in a new chunked pass 2.5, after
pass 2 has trained and before pass 3 grades - the only position where the
histogram is simultaneously not-trained-on, not-graded, and current with
the weights it will be applied to.
Costs 15% of the training data. Worth it beyond honesty: the deploy gate
needs dirPrecPct > chance + EDGE_MIN_SIGMAS*SE, and a threshold pinned
near zero dilutes any edge concentrated in the confident bars across every
bar the model calls, driving dirPrecPct toward chance by construction. A
threshold that can be selective is the only mechanism by which a small,
concentrated edge could ever clear that gate.
Also: a sparse histogram now KEEPS the previous threshold instead of
resetting to 0.0. A failed measurement must not decay to the most exposed
setting in the range.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 15:58:18 -04:00
}
fix(deinit): vote arrows survived the cheap sweep, and 5 long loops ignored the stop
Leftover chart objects on long-history charts. Two causes, one of them
introduced by 07aa017.
THE ONE I ADDED. The filtered view's overlay draws up to
SIGNAL_RESCAN_LOOKBACK_BARS vote arrows. OnDeinit's EARLY VISIBLE-UI SWEEP
runs with skipArrows=true, which skips any prefix equal to SIG_ARROW_PREFIX -
and "WarSig_VOTE_..." starts with "WarSig_", so every one of them was skipped
by the one sweep that is cheap enough to always complete. They then sat in the
object list while the two expensive scans that follow walked it: a per-member
SaveChartSignals O(total) scan, then the by-name rescan. On a chart with years
of history that is thousands of extra objects walked twice, inside a teardown
budget measured from the stop REQUEST rather than from OnDeinit's first line.
skipArrows exists because the per-model arrows' sidecar is rebuilt by SCANNING
them off the chart, so they cannot be deleted before that write. Vote arrows
have no sidecar - they are a reconstruction, rebuilt on the next attach - so
nothing is preserving them and they now get their own prefix slot, deleted by
one native call in the first few milliseconds.
THE FIVE LOOPS. A time budget bounds THROUGHPUT, not latency to an unload, and
OnDeinit cannot begin until whatever is in flight returns. These all scaled
with history and none of them checked:
* Training passes 2, 2.5 and 3 yielded only on TRAIN_TIME_BUDGET_MS. Pass 1
has checked IsStopped() all along; the other three never have, and they
are the ones that grow with the bar count. Free to fix - the resume state
is written either way, so a stopped chunk simply is not re-entered.
* PruneDirectionalClusters: the one UNCHUNKED sweep left, once per era over
every bar, with its own header noting that raising the training budget
cannot help its cost. Now bails outright.
* AdvanceChartSignalRestore / AdvanceChartSignalRescan: chunked, but the
rescan runs a full feedForward per bar over up to 5000 bars and the
restore can hold MAX_RESTORED_ARROWS entries. Checked on the same
64-object stride as the clock read, since the check is not free either.
* AdvanceFilteredOverlay (mine, 07aa017) replays Direction() on every
classic filter per bar and had no check at all. Now per bar.
ChartUI.mqh had ZERO shutdown checks across six loops before this.
Nothing was added to the purge path itself: that is the work that must
complete, and an IsStopped() check inside it would abort unconditionally -
IsStopped() is already true by the time OnDeinit runs.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 17:13:50 -04:00
//--- Time OR stop - see pass 2's matching comment.
2026-08-23 11:40:15 -04:00
if ( m_calibIndex - 1 > = calibLo & & ( IsStopped ( ) | | era . BudgetSpent ( ) ) )
fix: the operating point was fitted on bars the net had memorized
FitDirConfThreshold harvested its margin histogram from pass 2's own
backprop samples. Pairing every fit against the same era's OOS result
shows what that measured:
PAI era 1 IS 25% cov @ 66.1% (-0.8pp) -> OOS 64% (-3pp) gap +2.1pp
PAI era 76 IS 90% cov @ 79.6% (+12.7pp) -> OOS 65% (-2pp) gap +14.6pp
LSTM era 9 IS 77% cov @ 81.6% (+14.6pp) -> OOS 63% (-4pp) gap +18.6pp
The gap grows monotonically while OOS stays flat, so within a handful of
eras the curve stops describing behaviour on unseen bars. That is fatal
here specifically, because the objective branches on the SIGN of
(p - break-even): the memorized curve reads +12pp at 95% coverage, so
coverage x (p - p0) correctly maximises coverage and returns ~0.02 - fire
on every bar. The "p < p0 -> get more selective" branch, which is the
actual regime and the entire point of 983a6a3, could never fire because IS
never showed p < p0.
Carve a calibration slice out of the IS span - DIR_CONF_CALIB_PCT_OF_IS,
purged from backprop by one label horizon on BOTH sides (the far-side
purge is not optional: without it the newest training bars carry labels
partly decided by price action inside the slice, putting the memorization
straight back into the curve). Score it in a new chunked pass 2.5, after
pass 2 has trained and before pass 3 grades - the only position where the
histogram is simultaneously not-trained-on, not-graded, and current with
the weights it will be applied to.
Costs 15% of the training data. Worth it beyond honesty: the deploy gate
needs dirPrecPct > chance + EDGE_MIN_SIGMAS*SE, and a threshold pinned
near zero dilutes any edge concentrated in the confident bars across every
bar the model calls, driving dirPrecPct toward chance by construction. A
threshold that can be selective is the only mechanism by which a small,
concentrated edge could ever clear that gate.
Also: a sparse histogram now KEEPS the previous threshold instead of
resetting to 0.0. A failed measurement must not decay to the most exposed
setting in the range.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 15:58:18 -04:00
{
//--- yield: m_isCalibActive + m_calibIndex carry the resume position, same as passes 1-3.
refactor(training): Train()'s working state is one object, not eight locals
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>
2026-08-23 11:28:11 -04:00
StashEraResume ( era . bars , era . totalIter , era . oosCutoff , era . addLoop , era . i ) ;
fix: the operating point was fitted on bars the net had memorized
FitDirConfThreshold harvested its margin histogram from pass 2's own
backprop samples. Pairing every fit against the same era's OOS result
shows what that measured:
PAI era 1 IS 25% cov @ 66.1% (-0.8pp) -> OOS 64% (-3pp) gap +2.1pp
PAI era 76 IS 90% cov @ 79.6% (+12.7pp) -> OOS 65% (-2pp) gap +14.6pp
LSTM era 9 IS 77% cov @ 81.6% (+14.6pp) -> OOS 63% (-4pp) gap +18.6pp
The gap grows monotonically while OOS stays flat, so within a handful of
eras the curve stops describing behaviour on unseen bars. That is fatal
here specifically, because the objective branches on the SIGN of
(p - break-even): the memorized curve reads +12pp at 95% coverage, so
coverage x (p - p0) correctly maximises coverage and returns ~0.02 - fire
on every bar. The "p < p0 -> get more selective" branch, which is the
actual regime and the entire point of 983a6a3, could never fire because IS
never showed p < p0.
Carve a calibration slice out of the IS span - DIR_CONF_CALIB_PCT_OF_IS,
purged from backprop by one label horizon on BOTH sides (the far-side
purge is not optional: without it the newest training bars carry labels
partly decided by price action inside the slice, putting the memorization
straight back into the curve). Score it in a new chunked pass 2.5, after
pass 2 has trained and before pass 3 grades - the only position where the
histogram is simultaneously not-trained-on, not-graded, and current with
the weights it will be applied to.
Costs 15% of the training data. Worth it beyond honesty: the deploy gate
needs dirPrecPct > chance + EDGE_MIN_SIGMAS*SE, and a threshold pinned
near zero dilutes any edge concentrated in the confident bars across every
bar the model calls, driving dirPrecPct toward chance by construction. A
threshold that can be selective is the only mechanism by which a small,
concentrated edge could ever clear that gate.
Also: a sparse histogram now KEEPS the previous threshold instead of
resetting to 0.0. A failed measurement must not decay to the most exposed
setting in the range.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 15:58:18 -04:00
return ;
}
}
Net . SetBatchNormFrozen ( false ) ;
//--- An empty band (era too short to carve one - see CalibBandBars) means there is no measurement
//--- this era, which is not the same as a measurement that says "trade everything". Leave the
//--- operating point exactly where the last successful fit put it rather than refitting on nothing.
if ( calibHi > calibLo )
FitDirConfThreshold ( ) ;
m_isCalibActive = false ;
m_isCalibDone = true ;
}
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
}
//+------------------------------------------------------------------+
//| PASS 3: SCORE THE OUT-OF-SAMPLE SPAN. |
//| |
//| Walks the OOS bars with batch-norm frozen (scoring must not move |
//| the running statistics - live adaptation is untouched), then runs |
//| the reports that read the walk it just finished: exit-policy |
//| simulation, candidate geometry, excursions, cluster pruning, tier |
//| re-ranking, drift and the Alglib baselines. |
//| |
//| It MEASURES. Nothing here decides anything - the recall gate, the |
//| plateau ladder, the deploy gate and the era checkpoint all read |
//| these numbers afterwards, back in Train(). |
//| |
//| Guarded on era.addLoop: a chunk that ran out of wall-clock budget |
//| mid-era has no complete era to score. |
//+------------------------------------------------------------------+
void CExpertSignalAIBase : : RunOosPass ( STrainEra & era )
{
refactor(training): Train()'s working state is one object, not eight locals
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>
2026-08-23 11:28:11 -04:00
if ( ! era . stop & & era . addLoop )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
{
if ( ! m_isPass3Active )
{
m_isPass3Active = true ;
2026-08-22 00:25:52 -04:00
//--- Freeze batch-norm running statistics for the whole scoring walk (2026-08-09 audit,
//--- F5). Same reasoning (and same mechanism) as ValidateCpuInference. Live/online
//--- adaptation is untouched - only scoring is frozen.
fix: training-stability audit fixes F1/F2/F3/F5 - unbiased shuffle, real plateau escapes, fresh optimizer state on restore, pure OOS metric
Four of the six findings from research/training_pipeline_audit_2026-08-09.md
(F4 mini-batching and F6 feature re-encode deliberately deferred - see the
report's implementation-status section for why):
- F1: pass-2 Fisher-Yates (and AutoTune's MI block shuffle) used MathRand()%,
which is 15-bit - provably non-uniform on every full-history era over 32,768
queued samples. New 30-bit ShuffleRandomIndex().
- F2: plateau warm restarts were a no-op whenever eta already sat at its
ceiling (the normal state of a non-regressing plateau) - the ladder was just
a 24-era countdown. Restarts now overshoot to 5x the ceiling
(PLATEAU_RESTART_BOOST) and anneal geometrically back over the patience
window, SGDR-style; ETA_MIN widened 1e-4 -> 1e-5 so the decay schedule has
real range.
- F3: checkpoint restores put weights back but kept the rejected trajectory's
Adam moments, so the optimizer immediately pushed back toward the rolled-back
state (the restore->regress->restore oscillation). CNet::ResetOptimizerState()
zeroes moments/momentum/step counters (weights, BN statistics, gamma/beta
untouched) on every mid-run restore, every boosted restart, and the
deploy-time restore that online learning continues from.
- F5: batch-norm running statistics now freeze for the pass-3 OOS scoring walk,
so the selection metric the checkpoint ranking and deploy gate read is a pure
function of the checkpoint instead of partly measuring BN drift. Defensive
unfreeze in FinalizeTrainRun covers stop-mid-pass; live/online adaptation and
the OOS continual-learning simulation stay adaptive by design.
Compiled clean (0 errors, 0 warnings) via the staged-tree recipe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 10:54:09 -04:00
Net . SetBatchNormFrozen ( true ) ;
refactor(training): Train()'s working state is one object, not eight locals
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>
2026-08-23 11:28:11 -04:00
m_oosScoreStartIndex = ( int ) MathMin ( era . oosCutoff - 1 , era . bars - MathMax ( m_historyBars , 0 ) - 2 ) ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
m_oosScoreIndex = m_oosScoreStartIndex ;
for ( int rn = 0 ; rn < 3 ; rn + + )
{
m_oosOutMin [ rn ] = DBL_MAX ;
m_oosOutMax [ rn ] = - DBL_MAX ;
}
m_oosOutSpreadSum = 0.0 ;
m_oosOutCount = 0 ;
feat(diagnostics): split a reported "Neutral" into CHOSE vs TIED - they need opposite fixes
ApplyClassificationSoftmax() requires a STRICT majority over both rivals and
sends every tie, 2-way or 3-way, to Neutral. So "OOS recall Neutral:100%" is
two completely different events sharing one label:
CHOSE - the net genuinely ranks Neutral highest. A class-prior/label problem.
TIED - the top two are EXACTLY equal, so the net expressed no preference and
the tie-break reported Neutral. A SATURATION problem: the head is
SIGMOID, and a saturated sigmoid returns exactly 0.0f or 1.0f in the
DLL's float32, so two classes pinned to the same rail compare equal
and the bar is silently discarded.
Nothing in the logs could tell them apart, and the fixes point opposite ways.
Eras 1-25 of the 2026-08-17 solo PAI run read "Neutral 100%" at spread avg 0.99
- fully saturated - and broke out at era 27 as the spread fell to 0.75. That is
consistent with EITHER story. The user reports the Neutral phase on most runs,
so it is worth four longs to stop guessing.
Four per-era counters on the pass 3 OOS walk, reported as:
| Neutral CHOSE 12.4% / TIED 38.1% (of which B=S 1204) | rail 61.2%
m_oosNeutralStrict - Neutral strictly highest
m_oosNeutralTie - no strict winner; the tie-break produced Neutral
m_oosTieBuySell - the costly subset: Buy and Sell tied AT the top, i.e. a
DIRECTIONAL reading thrown away by float equality
m_oosRailBars - any raw output sitting on a sigmoid asymptote, the
saturation that makes exact ties possible at all
Read on the RAW logits, before ApplyClassificationSoftmax() overwrites TempData
in place. Legitimate because softmax is strictly monotone: it cannot change the
ordering and cannot break a tie either, so the raw reading and the decision
always agree. Placed alongside the existing min/max/spread capture so all the
output diagnostics describe the same values.
Measurement only - no decision path reads these.
NOT COMPILED - user compiles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 00:06:54 -04:00
m_oosNeutralStrict = 0 ;
m_oosNeutralTie = 0 ;
m_oosTieBuySell = 0 ;
m_oosRailBars = 0 ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
}
fix(gate): move the ranking slice to the OLD end - it walled off the recent chart
NOT COMPILED - user compiles.
User: "there is quite some trading going on, but absolutely nothing on the recent
area of the chart, like there is a hard wall starting around november 2025."
That wall is 7caf2f6's ranking slice, and it was placed at the wrong end. Chart
arrows are only ever drawn on bars pass 3 GRADES, and the slice reserved the
NEWEST 20% of the OOS window plus a label-horizon purge. At the live sizing -
~4,860 OOS bars, 128-bar horizon - that is ~1,100 H4 bars withheld from grading,
about ten months back from today, exactly where the wall appears.
The invisible cost was worse than the visible one: it handed the deploy gate the
OLDEST 80% of the OOS window and withheld the most recent regime from the single
decision that has to generalise forward.
Both fixed by putting the reserve at the oldest end instead:
[0, oosScoreHi) OOS - graded by pass 3 (NEWEST, arrows restored)
[oosScoreHi, rankLo) purge - one label horizon
[rankLo, oosCutoff) RANKING - backfill only, graded by nobody
[oosCutoff, calibLo) purge
[calibLo, calibHi) CALIBRATION
... IS
Of the three consumers competing for those bars, recency is worth least to the
ranking: it is an ORDERING of confidence tiers, far less regime-sensitive than an
absolute win rate, while the gate's power and the operator's read of the chart
both want the newest data. The slice keeps every property that made it worth
carving - never graded, never selected on, never seen by the gate, purged on both
sides - so the backfilled rows are still honestly out-of-sample.
RankSliceHiIndex is replaced by RankSliceLoIndex + OosScoreHiIndex; pass 3 now
excludes the slice at the TOP of its walk and descends to 2 as it always did.
The backfill walks [RankSliceLoIndex, oosCutoff) via a new m_dbBackfillStopIndex,
clamped at both ends so a degenerate slice yields an empty walk rather than one
that wanders into graded bars. Verified no reference to the old helper survives.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 22:58:55 -04:00
for ( ; m_oosScoreIndex > = 2 ; m_oosScoreIndex - - )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
{
int oi = m_oosScoreIndex ;
refactor(training): Train()'s working state is one object, not eight locals
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>
2026-08-23 11:28:11 -04:00
if ( ! ( oi < ( int ) ( era . bars - MathMax ( m_historyBars , 0 ) - 1 ) & & m_Time . GetData ( oi ) > dtStudied ) )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
continue ;
diag: slow eras must explain themselves - heartbeat + era time split + pass-1 paint
The 23:42 restart left all four charts grinding ~25x slower than the 18:01
baseline (era lines in 86 seconds there; 20+ minutes of nothing here), and
NOTHING could say why from outside: pass 1 logs nothing, its status paint sat
inside the !wouldQueue branch so the IS sweep - 80% of the pass, processed
FIRST - painted nothing either, the VPS has no debugger for a thread stack,
and the hourly new-bar cache invalidation cancels and restarts an unfinished
era, so a slow era can stay invisible FOREVER. Externals gave: four chart
threads at ~95% pure user-mode compute, DLL pool idle, no file writes. That
narrows it to "MQL5-side per-item work in the era passes" and no further.
So training now explains itself:
- TrainHeartbeat: one line per 4096 processed items, only after an era has
already run 60s, at most 6 lines per era - a healthy era stays exactly as
quiet as before. Reports position and the cumulative split: feature-window
builds vs net forward/backprop vs everything else. Hooked into all three
passes.
- The era summary line gains "| ERA TOOK Ns (feature windows X, net fwd/back
Y, other Z)" whenever an era exceeded 120s.
- Pass 1 paints its progress for QUEUED bars too, not just the OOS slice, so
the panel shows "learning (era N)" instead of sitting on the idle writer's
"Getting ready..." for the entire IS sweep. The label is throttled
internally; painting per bar costs nothing.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 00:10:45 -04:00
TrainHeartbeat ( " pass 3 (OOS scoring), bar " , m_oosScoreStartIndex - m_oosScoreIndex + 1 ,
2026-08-10 07:22:29 -04:00
m_oosScoreStartIndex + 1 , " scoring " ) ;
diag: slow eras must explain themselves - heartbeat + era time split + pass-1 paint
The 23:42 restart left all four charts grinding ~25x slower than the 18:01
baseline (era lines in 86 seconds there; 20+ minutes of nothing here), and
NOTHING could say why from outside: pass 1 logs nothing, its status paint sat
inside the !wouldQueue branch so the IS sweep - 80% of the pass, processed
FIRST - painted nothing either, the VPS has no debugger for a thread stack,
and the hourly new-bar cache invalidation cancels and restarts an unfinished
era, so a slow era can stay invisible FOREVER. Externals gave: four chart
threads at ~95% pure user-mode compute, DLL pool idle, no file writes. That
narrows it to "MQL5-side per-item work in the era passes" and no further.
So training now explains itself:
- TrainHeartbeat: one line per 4096 processed items, only after an era has
already run 60s, at most 6 lines per era - a healthy era stays exactly as
quiet as before. Reports position and the cumulative split: feature-window
builds vs net forward/backprop vs everything else. Hooked into all three
passes.
- The era summary line gains "| ERA TOOK Ns (feature windows X, net fwd/back
Y, other Z)" whenever an era exceeded 120s.
- Pass 1 paints its progress for QUEUED bars too, not just the OOS slice, so
the panel shows "learning (era N)" instead of sitting on the idle writer's
"Getting ready..." for the entire IS sweep. The label is throttled
internally; painting per bar costs nothing.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 00:10:45 -04:00
ulong hbT = GetMicrosecondCount ( ) ;
fix: the sequence models were reading the window backwards
BuildFeatureWindow() replaces eight hand-rolled copies of the same loop
and feeds the window OLDEST BAR FIRST. Every copy fed it newest-first,
because MQL5 timeseries indices run backwards and `r + b` with b ascending
walks into the past.
Harmless for PAI and CONV - a dense layer learns a weight per position
either way, a conv learns time-mirrored kernels. Not harmless for the
recurrent stacks:
- LSTM_SeqStepForward reads `inputs + t*Iw`, so step t is block t.
- It writes output[] only when t == steps-1: the visible output IS the
last hidden state.
- c_t = f*c_{t-1} + i*g decays toward the start of the sequence.
lstm_seq_flowcheck.cpp measured block 0's influence on the output at
1.2e-2 of block T-1's, at the shipped forget bias of 1.0.
So the bar being PREDICTED sat at the far end of the decay and the output
was handed to the OLDEST bar in the window - the exact inverse of what the
window is for. ~80x backwards on LSTM and HYBRID, on all three tiers
(OpenCL kernel, CPU DLL, pure-MQL5 inference), which is why it never
surfaced as a backend discrepancy.
This does not create edge - the MI diagnostics read at the noise floor
(p=0.4975) with a working positive control. It makes the one hypothesis
those diagnostics explicitly do NOT cover testable: they are marginal and
per-bar, and state they "cannot rule out one that only exists in
combination or across time". The sequence model is the instrument for
across-time structure and it has been crippled, so that hypothesis has
never been honestly tested.
Fingerprint gets an unconditional |WIN:2 - the vector keeps its shape and
its features, so a stale .nnw would load cleanly and run a model fitted to
one ordering against the other, silently. Re-keying every config is the
point, not collateral damage. FORCES A FULL RETRAIN.
Also: the now-relative bar caches are re-keyed on the two live paths.
EnsureBarCachesCapacity() was only ever called from training paths, but
once m_trainingComplete is set ScheduleTrainingIfNeeded() routes every bar
to RefreshConvergedSignal() and Train() is never re-entered - so nothing
cleared the feature cache again for the life of the process. A chart that
trained to convergence kept replaying the rows computed for the last
training era's bar grid: the live signal froze at its convergence-time
value, and OnlineLearnStep() backpropped those stale features against
freshly resolved labels. Backtests were never affected (an inference-only
process never allocates the arrays, so every read recomputes).
Compiles clean: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 18:28:44 -04:00
bool oWindowOk = BuildFeatureWindow ( oi ) ;
diag: slow eras must explain themselves - heartbeat + era time split + pass-1 paint
The 23:42 restart left all four charts grinding ~25x slower than the 18:01
baseline (era lines in 86 seconds there; 20+ minutes of nothing here), and
NOTHING could say why from outside: pass 1 logs nothing, its status paint sat
inside the !wouldQueue branch so the IS sweep - 80% of the pass, processed
FIRST - painted nothing either, the VPS has no debugger for a thread stack,
and the hourly new-bar cache invalidation cancels and restarts an unfinished
era, so a slow era can stay invisible FOREVER. Externals gave: four chart
threads at ~95% pure user-mode compute, DLL pool idle, no file writes. That
narrows it to "MQL5-side per-item work in the era passes" and no further.
So training now explains itself:
- TrainHeartbeat: one line per 4096 processed items, only after an era has
already run 60s, at most 6 lines per era - a healthy era stays exactly as
quiet as before. Reports position and the cumulative split: feature-window
builds vs net forward/backprop vs everything else. Hooked into all three
passes.
- The era summary line gains "| ERA TOOK Ns (feature windows X, net fwd/back
Y, other Z)" whenever an era exceeded 120s.
- Pass 1 paints its progress for QUEUED bars too, not just the OOS slice, so
the panel shows "learning (era N)" instead of sitting on the idle writer's
"Getting ready..." for the entire IS sweep. The label is throttled
internally; painting per bar costs nothing.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 00:10:45 -04:00
m_passFeatUs + = GetMicrosecondCount ( ) - hbT ;
2026-08-02 01:09:18 -04:00
//--- Same guard as pass 2, and it matters more here: OOS accuracy is what checkpoint selection
//--- and the plateau ladder's auto-deploy both rank on, so scoring a stale forward pass would
//--- not just be wrong, it would be wrong in the one number that decides which model ships.
//--- A skipped bar simply isn't counted; it never becomes a hit or a miss.
diag: slow eras must explain themselves - heartbeat + era time split + pass-1 paint
The 23:42 restart left all four charts grinding ~25x slower than the 18:01
baseline (era lines in 86 seconds there; 20+ minutes of nothing here), and
NOTHING could say why from outside: pass 1 logs nothing, its status paint sat
inside the !wouldQueue branch so the IS sweep - 80% of the pass, processed
FIRST - painted nothing either, the VPS has no debugger for a thread stack,
and the hourly new-bar cache invalidation cancels and restarts an unfinished
era, so a slow era can stay invisible FOREVER. Externals gave: four chart
threads at ~95% pure user-mode compute, DLL pool idle, no file writes. That
narrows it to "MQL5-side per-item work in the era passes" and no further.
So training now explains itself:
- TrainHeartbeat: one line per 4096 processed items, only after an era has
already run 60s, at most 6 lines per era - a healthy era stays exactly as
quiet as before. Reports position and the cumulative split: feature-window
builds vs net forward/backprop vs everything else. Hooked into all three
passes.
- The era summary line gains "| ERA TOOK Ns (feature windows X, net fwd/back
Y, other Z)" whenever an era exceeded 120s.
- Pass 1 paints its progress for QUEUED bars too, not just the OOS slice, so
the panel shows "learning (era N)" instead of sitting on the idle writer's
"Getting ready..." for the entire IS sweep. The label is throttled
internally; painting per bar costs nothing.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 00:10:45 -04:00
hbT = GetMicrosecondCount ( ) ;
2026-08-02 01:09:18 -04:00
bool oForwardOk = ( oWindowOk & & TempData . Total ( ) > = ( int ) m_historyBars * m_neuronsCount & &
Net . feedForward ( TempData ) ) ;
diag: slow eras must explain themselves - heartbeat + era time split + pass-1 paint
The 23:42 restart left all four charts grinding ~25x slower than the 18:01
baseline (era lines in 86 seconds there; 20+ minutes of nothing here), and
NOTHING could say why from outside: pass 1 logs nothing, its status paint sat
inside the !wouldQueue branch so the IS sweep - 80% of the pass, processed
FIRST - painted nothing either, the VPS has no debugger for a thread stack,
and the hourly new-bar cache invalidation cancels and restarts an unfinished
era, so a slow era can stay invisible FOREVER. Externals gave: four chart
threads at ~95% pure user-mode compute, DLL pool idle, no file writes. That
narrows it to "MQL5-side per-item work in the era passes" and no further.
So training now explains itself:
- TrainHeartbeat: one line per 4096 processed items, only after an era has
already run 60s, at most 6 lines per era - a healthy era stays exactly as
quiet as before. Reports position and the cumulative split: feature-window
builds vs net forward/backprop vs everything else. Hooked into all three
passes.
- The era summary line gains "| ERA TOOK Ns (feature windows X, net fwd/back
Y, other Z)" whenever an era exceeded 120s.
- Pass 1 paints its progress for QUEUED bars too, not just the OOS slice, so
the panel shows "learning (era N)" instead of sitting on the idle writer's
"Getting ready..." for the entire IS sweep. The label is throttled
internally; painting per bar costs nothing.
Both build variants compile 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 00:10:45 -04:00
m_passNetUs + = GetMicrosecondCount ( ) - hbT ;
refactor(training): Train()'s working state is one object, not eight locals
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>
2026-08-23 11:28:11 -04:00
if ( oWindowOk & & ! oForwardOk & & ! era . forwardFailureReported )
2026-08-02 01:09:18 -04:00
{
refactor(training): Train()'s working state is one object, not eight locals
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>
2026-08-23 11:28:11 -04:00
era . forwardFailureReported = true ;
2026-08-02 01:09:18 -04:00
Print ( __FUNCTION__ + " : CNet::feedForward FAILED during OOS scoring at era " +
IntegerToString ( ( int ) m_eraCount ) + " - affected bars are excluded from the OOS "
" accuracy rather than scored against a stale prediction. " ) ;
}
if ( oForwardOk )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
{
Net . getResults ( TempData ) ;
// Raw output stats MUST be captured here, before ApplyClassificationSoftmax() overwrites
// TempData[0..2] in place with the softmax probabilities - see m_oosOutMin's declaration
// comment for what these feed.
if ( m_outputNeuronsCount = = 3 & & TempData . Total ( ) > = 3 )
{
double rawHi = - DBL_MAX , rawLo = DBL_MAX ;
for ( int rn = 0 ; rn < 3 ; rn + + )
{
double rv = TempData . At ( rn ) ;
if ( rv < m_oosOutMin [ rn ] )
m_oosOutMin [ rn ] = rv ;
if ( rv > m_oosOutMax [ rn ] )
m_oosOutMax [ rn ] = rv ;
rawHi = MathMax ( rawHi , rv ) ;
rawLo = MathMin ( rawLo , rv ) ;
}
m_oosOutSpreadSum + = rawHi - rawLo ;
m_oosOutCount + + ;
2026-08-22 00:25:52 -04:00
//--- WHY Neutral won, split into its two causes - see m_oosNeutralStrict's
//--- declaration.
feat(diagnostics): split a reported "Neutral" into CHOSE vs TIED - they need opposite fixes
ApplyClassificationSoftmax() requires a STRICT majority over both rivals and
sends every tie, 2-way or 3-way, to Neutral. So "OOS recall Neutral:100%" is
two completely different events sharing one label:
CHOSE - the net genuinely ranks Neutral highest. A class-prior/label problem.
TIED - the top two are EXACTLY equal, so the net expressed no preference and
the tie-break reported Neutral. A SATURATION problem: the head is
SIGMOID, and a saturated sigmoid returns exactly 0.0f or 1.0f in the
DLL's float32, so two classes pinned to the same rail compare equal
and the bar is silently discarded.
Nothing in the logs could tell them apart, and the fixes point opposite ways.
Eras 1-25 of the 2026-08-17 solo PAI run read "Neutral 100%" at spread avg 0.99
- fully saturated - and broke out at era 27 as the spread fell to 0.75. That is
consistent with EITHER story. The user reports the Neutral phase on most runs,
so it is worth four longs to stop guessing.
Four per-era counters on the pass 3 OOS walk, reported as:
| Neutral CHOSE 12.4% / TIED 38.1% (of which B=S 1204) | rail 61.2%
m_oosNeutralStrict - Neutral strictly highest
m_oosNeutralTie - no strict winner; the tie-break produced Neutral
m_oosTieBuySell - the costly subset: Buy and Sell tied AT the top, i.e. a
DIRECTIONAL reading thrown away by float equality
m_oosRailBars - any raw output sitting on a sigmoid asymptote, the
saturation that makes exact ties possible at all
Read on the RAW logits, before ApplyClassificationSoftmax() overwrites TempData
in place. Legitimate because softmax is strictly monotone: it cannot change the
ordering and cannot break a tie either, so the raw reading and the decision
always agree. Placed alongside the existing min/max/spread capture so all the
output diagnostics describe the same values.
Measurement only - no decision path reads these.
NOT COMPILED - user compiles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 00:06:54 -04:00
double rB = TempData . At ( 0 ) , rS = TempData . At ( 1 ) , rN = TempData . At ( 2 ) ;
bool strictB = ( rB > rS & & rB > rN ) ;
bool strictS = ( rS > rB & & rS > rN ) ;
bool strictN = ( rN > rB & & rN > rS ) ;
if ( strictN )
m_oosNeutralStrict + + ;
else
if ( ! strictB & & ! strictS )
{
//--- No class holds a strict majority, so the top two are EXACTLY equal and
//--- ApplyClassificationSoftmax() returns Neutral by the tie rule, not by choice.
m_oosNeutralTie + + ;
//--- The expensive subset: Buy and Sell tied AT the top (either a 2-way tie above
//--- Neutral, or a 3-way). The net had a directional reading and float equality
//--- threw it away.
if ( rB = = rS & & rB > = rN )
m_oosTieBuySell + + ;
}
2026-08-22 00:25:52 -04:00
//--- Sigmoid rails. The head is SIGMOID (Topology.mqh), so 0 and 1 are its
//--- asymptotes; a raw value sitting ON one in float32 is the saturation that MAKES
//--- exact ties possible.
feat(diagnostics): split a reported "Neutral" into CHOSE vs TIED - they need opposite fixes
ApplyClassificationSoftmax() requires a STRICT majority over both rivals and
sends every tie, 2-way or 3-way, to Neutral. So "OOS recall Neutral:100%" is
two completely different events sharing one label:
CHOSE - the net genuinely ranks Neutral highest. A class-prior/label problem.
TIED - the top two are EXACTLY equal, so the net expressed no preference and
the tie-break reported Neutral. A SATURATION problem: the head is
SIGMOID, and a saturated sigmoid returns exactly 0.0f or 1.0f in the
DLL's float32, so two classes pinned to the same rail compare equal
and the bar is silently discarded.
Nothing in the logs could tell them apart, and the fixes point opposite ways.
Eras 1-25 of the 2026-08-17 solo PAI run read "Neutral 100%" at spread avg 0.99
- fully saturated - and broke out at era 27 as the spread fell to 0.75. That is
consistent with EITHER story. The user reports the Neutral phase on most runs,
so it is worth four longs to stop guessing.
Four per-era counters on the pass 3 OOS walk, reported as:
| Neutral CHOSE 12.4% / TIED 38.1% (of which B=S 1204) | rail 61.2%
m_oosNeutralStrict - Neutral strictly highest
m_oosNeutralTie - no strict winner; the tie-break produced Neutral
m_oosTieBuySell - the costly subset: Buy and Sell tied AT the top, i.e. a
DIRECTIONAL reading thrown away by float equality
m_oosRailBars - any raw output sitting on a sigmoid asymptote, the
saturation that makes exact ties possible at all
Read on the RAW logits, before ApplyClassificationSoftmax() overwrites TempData
in place. Legitimate because softmax is strictly monotone: it cannot change the
ordering and cannot break a tie either, so the raw reading and the decision
always agree. Placed alongside the existing min/max/spread capture so all the
output diagnostics describe the same values.
Measurement only - no decision path reads these.
NOT COMPILED - user compiles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 00:06:54 -04:00
if ( rawLo < = 1e-6 | | rawHi > = 1.0 - 1e-6 )
m_oosRailBars + + ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
}
double oPrevSignal = ( m_outputNeuronsCount = = 3 ) ? ApplyClassificationSoftmax ( ) : TempData [ 0 ] ;
double oDeploySignal = oPrevSignal ;
if ( m_outputNeuronsCount = = 3 )
oDeploySignal = AdjustedSignalFromSoftmax ( ) ;
double oNeuron0 = ( TempData . Total ( ) > 0 ) ? TempData [ 0 ] : 0.0 ;
double oNeuron1 = ( TempData . Total ( ) > 1 ) ? TempData [ 1 ] : 0.0 ;
double oNeuron2 = ( TempData . Total ( ) > 2 ) ? TempData [ 2 ] : 0.0 ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
bool oLabeled = ( oi < ArraySize ( m_labelCacheHasValue ) & & m_labelCacheHasValue [ oi ] ) ;
bool oBuy = oLabeled ? m_labelCacheBuy [ oi ] : false ;
bool oSell = oLabeled ? m_labelCacheSell [ oi ] : false ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
ENUM_SIGNAL oTrueSignal = oBuy ? Buy : ( oSell ? Sell : Neutral ) ;
feat(vote): thresholds become confidence percentages, on ONE scale everywhere
User request: "the entry/exit thresholds are manual numbers, I would like
them to be confidence percentages, so the current 20 would be only 20%
confidence in a profitable trade."
WHY 20 WAS EVER SENSIBLE. Under UseDatabaseRanking both factors of a filter's
contribution are win rates: the pattern weight is that pattern's measured win
rate (UpdateSignalsWeights -> ApplyPatternWeight) and m_weight is the filter's
average win rate over its patterns, /100. Dividing the sum by the VOTER COUNT
therefore produced a mean of PRODUCTS of two win rates - a genuinely
60%-accurate filter firing a 60% pattern scored 0.60 x 60 = 36. The number was
never on a probability scale, so its magnitude meant nothing on its own.
Dividing by Sum(m_weight) instead makes it a weighted MEAN of win rates, which
is a win rate: result = Sum(w_i*p_i)/Sum(w_i). Every voter at 60% now reads 60;
MACD's double-divergence pattern (weight 100) voting alone reads 100. m_weight
stops being a discount on the probability and becomes how much a filter's
opinion COUNTS - which is what a module weight should always have been.
Default Min_Vote_Open 20 -> 50: not a tightening, the same bar re-expressed.
ONE SCALE, EVERYWHERE - the part that made this bigger than a rescale. Three
other places compared against a 0..1 softmax confidence and would each have
become a fresh currency mismatch the moment the input changed meaning:
* the AI early-exit route (LiveSignedConfidence vs m_ai_exit_threshold) now
reads m_lastAiVote - the AI filters' own weighted mean, undiluted by the
classic side, which is the only reason that route exists - against the
same m_threshold_close the averaged vote uses. m_ai_exit_threshold is
retired rather than left dangling.
* m_oosDecisionSeries now carries the vote, not the confidence, so the exit
SIMULATION stops modelling a close rule the EA does not run.
* ExitPolicy() clamped anything > 1.0 to zero. Passing the unscaled input
through that would have silently switched vote exits off in the
simulation while live went on running them - found before it shipped;
the bound now tracks the scale.
LiveSignedConfidence() is deliberately untouched and still 0..1: MM sizing,
SL/TP scaling and the intelligent trailing want a model confidence, not a win
rate.
CALIBRATION CAVEAT, stated in the code where the claim is made: this is only a
real probability to the extent the pattern weights are. A pattern with fewer
than MIN_TRADES_FOR_WIN_RATE journaled trades keeps its DEFAULT weight - a
designed prior (25/50/75/100 for the AI tiers), not a measurement. Until the
signal DB fills, "60" means "the designed conviction of the patterns that
fired". Closing that gap is the next commit.
Also corrects VOTE_CLOSE_PRESETS' comment, which documented the two scales
this removes.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 15:52:08 -04:00
//--- ONE CURRENCY, and it is the live one. oEnsembleVote is the signed vote this member
2026-08-22 00:25:52 -04:00
//--- would have cast on this bar - m_weight x its tier's pattern weight, the same
//--- number CExpertSignalCustom::Direction() sums and the same 0-100 win-rate scale
//--- Signal_ThresholdOpen/Signal_ThresholdClose are expressed in.
fix(gate): the ensemble gate certified a vote the EA never casts
g_ensembleVoteThreshold's comment claims the combined-vote scorer "fires on
the same criterion the live trade does". It did not. Two independent
mismatches, both silent:
CURRENCY. Each member contributed its raw signed confidence x100 - a 33..100
number straight off the softmax head. Live contributes m_weight x the tier's
pattern weight, and BOTH of those are rewritten from the signal DB by
UpdateSignalsWeights(). A head output and a DB-ranked win-rate weight share
an axis and nothing relates them, so the same bar was one number to the gate
and a different one to the order path. Same shape as the 2026-08-09 geometry
incident: certified on one game, paid on another.
DENOMINATOR. The gate divided by the member count, so an abstaining member
pulled the average toward zero. CExpertSignalCustom::Direction() skips a zero
contribution in BOTH the sum and the count (`if(direction == 0) continue;`
before `number++`) - live is a mean over VOTERS. The gate was therefore
scoring a strictly more agreement-heavy set of bars than the EA trades. The
contribution hook's own comment asserted the opposite ("abstentions dilute
the average exactly as they do in the live vote"), while the AI_CHOICE enum
20 lines away correctly documented union semantics.
LiveVoteContribution() is now the single definition of "what this member
votes", called from the gate; the live path reaches the same arithmetic
through LongCondition/ShortCondition. g_ensVoteVoterMask records who actually
voted, separately from who evaluated the bar, because those are the divisor
and the shared-population test respectively.
ConfidenceTier() is split into ConfidenceTierFor(signal) plus a thin live-bar
wrapper - the OOS scan holds the scanned bar's decision in a local, and
dPrevSignal is a different bar.
NOT changed, deliberately: the gate still does not model live NMS
declustering, and the per-member solo gate still scores every directional
call rather than threshold-clearing ones. Both are selection-metric changes
and this codebase has twice been bitten by switching one blind.
Also corrects a stale paragraph in m_pattern_0's declaration block quoting
80/87/93/100 as the tier defaults. The constructor is 25/50/75/100 and has
been since the confidence floor and alternation gate were removed; the block
carried both tables at once, and the dead one was quoted back as fact.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 15:23:33 -04:00
double oEnsembleVote = LiveVoteContribution ( oDeploySignal ) ;
feat(vote): CONSENSUS arithmetic - agreement is now what the threshold dials
Era-680 report, all three observations one equation: "peak 29, no arrows at
threshold 30" / "at 20, arrows on EVERY bar" / "label at 12 while arrows
everywhere". Under the voters-only divisor, any bar with at least one
directional voter read the weighted mean of the firing tiers' weights - and
once the tiers self-ranked to each model's pooled win rate (~28-31), that
mean was NEAR-CONSTANT regardless of headcount. One member alone: ~29. Four
unanimous: ~29. Min_Vote_Open was a step function around that constant -
above it nothing ever fired, below it everything did - and the label's 12
was a 3v1 split netting through the same divisor. Not three display bugs:
one arithmetic that could not express agreement.
The divisor is now the CAPABLE weight - every filter that could vote,
whether it did or not:
* live (Direction): VoteCapableWeight() - classic pattern ladders always,
veto filters never, AI members once past the same readiness test
LongCondition gates on. A model still training must not dilute an
ensemble it cannot join: four trainees + one deployed model is a solo
chart wearing an ensemble label, and the solo vote reads full strength.
* gate (EnsembleEraVerdict): g_ensVoteWeightSum accumulates for every
member that EVALUATED the bar, Neutral included.
* overlay sweep + prospective readout: weight counts whenever the member
has data; a snapshotted Neutral dilutes.
One arithmetic, four sites, same numbers everywhere.
What the numbers become (four members, w~0.29, tiers~29): unanimous ~29 -
the CEILING, which is the pooled win rate and is what the peak displays;
3-of-4 ~22; 2-of-4 ~14.5; 3v1 ~14.5. Min_Vote_Open 20 now means "roughly
three-quarters of the ensemble's trust agrees, net". It MUST sit below the
ceiling to ever fire - the census/peak states the ceiling.
This is the ensemble the user specified in the original design discussion
("if the perceptron also votes, both together reach the threshold; if
another NN votes the other side, the threshold is not reached") - union
semantics was the pre-ensemble behaviour, kept until measurement showed its
vote magnitude was a constant.
Plus overlay DECLUSTERING, the other half of "arrows on every bar": the
same three NMS rules as the per-member arrows (same-direction runs collapse
to their first bar, cross-direction flicker keeps the stronger side), online
over the sweep's strictly oldest->newest walk. Suppression is a verdict and
deletes a standing arrow; the den==0 no-data skip still never does.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 06:58:03 -04:00
//--- The divisor term that goes with it - the member's weight WHENEVER it evaluated the
//--- bar, Neutral included, because consensus arithmetic (2026-08-19) has abstention
//--- dilute. Was zeroed on abstention under union semantics.
feat(vote): edge-over-chance currency, no-skill exclusion, checkpoint burn-in
RETRAIN-FORCING and deliberately so. Two independent fixes for the same symptom -
charts that go quiet while others overtrade.
1. THE VOTE CURRENCY IS NOW EDGE OVER CHANCE, not an absolute win rate.
A tier weight is a raw win rate and a raw win rate means nothing without the
chance rate behind it: 30% is strong under a 14% base rate and catastrophic under
50%, yet both entered the mean as "30". That is why the threshold needed
re-tuning every time the label changed - 25 was permissive at ~70% win rates
under the old direction label and a near-unanimity rule at ~30% under the
pivot-event one - and why one chart's 25% was never the same statement as
another's. Subtracting the member's own chance rate makes the units percentage
points of demonstrated edge, comparable across charts, labels and regimes.
Clamped at zero: a below-chance tier is anti-informative, and contributing
negatively would act on a broken model as an inverted oracle rather than
discarding it.
2. A NO-SKILL MEMBER IS NOW ABSENT, NOT ABSTAINING.
Measured on XTIUSD: a Perceptron collapsed to B97/S6/N3, pooled win rate 11.5%
against a 14% chance rate - worse than guessing - and still voting. Three healthy
members voting Sell scored -21.06/0.77 = -27.4 and cleared; with the dead one
voting Buy it became (-21.06+1.44)/0.89 = -22.0 and was BLOCKED. It vetoed its
own ensemble on ~95% of bars, and that WAS the chart's 3.3% coverage. Neither
existing guard caught it: it IS self-ranked and its tier weights were 11-14.
The fix has to remove it from the DIVISOR, not just the sum - an abstainer
contributes weight by design, so zeroing only the contribution makes the dilution
worse. VoteCapableWeight() already means exactly "may this member's weight sit in
the denominator", so the skill test belongs there. ReconstructionWeight() and the
OOS scorer's divisor move with it or the scorer certifies a vote live does not
cast. The skill test reads the PREVIOUS era's measurement - gating this era's
vote on this era's own outcome would be circular.
3. CHECKPOINT BURN-IN (ENSEMBLE_CHECKPOINT_MIN_ERA 20).
XAUUSD deployed the checkpoint from ERA 2, XTIUSD from ERA 4, each after 69 and
65 further eras failed to beat it. Ensemble coverage measures AGREEMENT, and four
models that have barely moved off their initialisation agree almost by
construction - so coverage is inflated exactly when the models know least and
decays as they differentiate (XAUUSD 6.6% at era 8 -> 0.4% at era 75). Since
selectionScore is precision discounted by coverage, an early era outscores every
mature one and the ladder freezes on it.
INTENDED CONSEQUENCE: a chart whose MATURE coverage cannot clear the floor now
refuses to deploy rather than shipping era-2 weights. Fewer deploys, honest ones.
Burn-in eras are also kept out of g_ensCandidateEras (they could not have won, so
counting them inflates the family-wise N and raises the bar for nothing) and out
of g_ensErasSinceBest (or the run reaches "no better vote for N eras" with no
best to beat, exhausting the escalation ladder before the first era may compete).
Every pinned threshold and .stats record is in the OLD currency and is now
meaningless - this forces a fresh start on its own. Nothing needs re-tuning
because the threshold is DERIVED: the sweep re-picks the rung by itself.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 15:38:26 -04:00
//---
//--- ReconstructionWeight(), not ModuleWeight(): it carries the SKILL test that now keeps
//--- a no-skill member out of the live divisor, without the converged-run test that would
//--- zero every weight here (this runs DURING training, where m_trainingComplete is
//--- false). Using ModuleWeight() would leave the scorer certifying a vote with a dead
//--- member still in the denominator while live excluded it - certified != traded.
//---
//--- The skill test reads the PREVIOUS era's measurement, which is the honest choice:
//--- gating this era's vote on this era's own outcome would be circular.
double oEnsembleWeight = ReconstructionWeight ( ) ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
if ( m_ensembleMember & & oLabeled )
EnsembleOosContribute ( oi , oEnsembleVote , oEnsembleWeight , oBuy , oSell , ( oBuy | | oSell ) ) ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
UpdateTrainingStatusLabel (
StringFormat ( " Scoring OOS bar %d of %d -> %.2f%% (post-training) " , m_oosScoreStartIndex - m_oosScoreIndex + 1 , m_oosScoreStartIndex + 1 ,
( double ) ( m_oosScoreStartIndex - m_oosScoreIndex + 1.0 ) / MathMax ( m_oosScoreStartIndex + 1 , 1 ) * 100 ) ,
oNeuron0 , oNeuron1 , oNeuron2 , oDeploySignal ) ;
// Held-out bar: score the model's freshly-trained-this-era forecast against the actual
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
// label without learning from it - keeps the OOS accuracy an honest overfitting signal.
// An UNRESOLVED bar (no committed pivot pair yet) has no label to score against, so it
// is excluded from every tally rather than counted as a true Neutral it may not be.
bool oClassified = oLabeled & &
( DoubleToSignal ( oPrevSignal ) = = Buy | | DoubleToSignal ( oPrevSignal ) = = Sell | | DoubleToSignal ( oPrevSignal ) = = Neutral ) ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
if ( oClassified )
{
m_oosSamples + + ;
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
m_oos . confidenceSum + = MathAbs ( oPrevSignal ) ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
if ( dOosError < 0 )
dOosError = 0 ;
bool hit = ( DoubleToSignal ( oPrevSignal ) = = oTrueSignal ) ;
ENUM_SIGNAL oPred = DoubleToSignal ( oPrevSignal ) ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//--- Compounded, persistent DIRECTIONAL precision: count only bars the model actually
//--- called Buy or Sell (Neutral "no trade" calls are neither right nor wrong here).
perf(autotune): replace the genetic search with a filter score - hours to seconds
MEASURED COST OF THE GA, which is what retired it. Per generation:
rung 0: 8 cand x 3 seeds x 3 eras = 72 eras
rung 1: 4 cand x 3 seeds x 8 eras = 96
rung 2: 2 cand x 3 seeds x 20 eras = 120
= 288 eras/generation x 4 generations = 1152 eras BEFORE the winner's
real training began. Against the observed era times on SP500 H1:
PAI 29.1 s/era -> 9.3 h (matches the observed 00:37 -> 09:22)
CONV 41.3 s/era -> 13.2 h
LSTM 150.4 s/era -> 48.1 h
HYBRID 154.6 s/era -> 49.5 h
Two days to tune is not a first-run experience, and it is the phase in
which the panel goes quiet, which is what made it look like a hang.
It also bought nothing. The space is 90 points (10 MA periods x 9 MA
types), so 1152 evaluations revisited each point ~13 times; and rungs of
3 and 8 eras cannot separate two MA periods at all. The 2026-08-01 run
proves it: every finalist scored 25.0-25.9% balanced accuracy - below the
33.3% one-class floor, i.e. indistinguishable noise - and the search then
"deployed the winner" of that.
THE ERROR WAS THE SCORING FUNCTION, not its constants. Using a full
training run to choose a feature's period is a wrapper method paying
wrapper prices for a decision that does not need one. The reference book
does not do this: ch. 3.3 selects inputs by measuring each candidate
indicator's CORRELATION with the target and dropping the ones with none,
with no network involved.
So: rank candidates by the MUTUAL INFORMATION between the resulting
feature vector and the triple-barrier label. MI rather than correlation
because the label is 3-class categorical and the features are not
monotonically related to it. Equal-FREQUENCY binning (rank-based),
because these features are ATR-normalised and heavy-tailed - fixed-width
bins put nearly everything in one bucket and report ~0 information for a
genuinely useful feature.
Scoring is arithmetic over the feature cache, so it costs seconds and its
cost is independent of topology: LSTM now tunes as fast as the MLP.
Coordinate sweep, not product sweep - cost is the SUM of per-parameter
candidate counts, so enabling every indicator stays affordable - with a
second pass that breaks early once nothing moves.
Sampling is IS-ONLY. Letting the OOS window influence which indicator
settings ship would mean the holdout had been used for selection and had
stopped being a holdout.
HONEST LIMIT, recorded because it is the price: MI is marginal, so a
parameter that only pays off in combination with another can be missed
(Guyon & Elisseeff 2003, filter vs wrapper). Given the wrapper it
replaces was ranking pure noise at 48 h a run, this is strictly better.
Deleted with it: GaRungEras/GaExtract/GaStore/GaMutate/GaRandomCandidate/
GaBlockCrossover/GaSortAliveByScoreDesc/GaBreedNextGeneration, 14 m_ga*
members, the GA_*/TUNE_POP_* constants, and ComputeTuneTrialBudget.
AND m_evalMode/m_evalEraBudget, because nothing set them any more - 28
read sites all permanently inert. That is not a tidy-up: the `if
(!m_evalMode)` guard on UpdateClassPriors is exactly what silently
disabled the imbalance correction for entire runs two commits ago. Dead
machinery that still reads like live machinery is this codebase's most
expensive recurring bug, and leaving 28 more instances of it would have
been indefensible.
The panel's tuning-progress state goes too - tuning no longer takes long
enough to need one.
Both builds compile 0 errors / 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:29:31 -04:00
if ( oPred = = Buy | | oPred = = Sell )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
{
m_cumOosTotal + + ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
if ( hit )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
m_cumOosCorrect + + ;
}
// Per-class confusion counts, used for the Buy/Sell recall convergence gate below
switch ( oTrueSignal )
{
case Buy :
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
m_oos . buyTotal + + ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
if ( hit )
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
m_oos . buyHits + + ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
break ;
case Sell :
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
m_oos . sellTotal + + ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
if ( hit )
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
m_oos . sellHits + + ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
break ;
default :
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
m_oos . neutralTotal + + ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
if ( hit )
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
m_oos . neutralHits + + ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
break ;
}
2026-08-22 00:25:52 -04:00
//--- DECLUSTERED count: of the calls that would actually become POSITIONS, how many
//--- were right. This pair is the one that answers "what would I have made".
fix: NMS gates the TRADE, not just the arrow - one arrow is now one trade
NmsLiveAccept() appeared in exactly one place: wrapped around DrawObject().
It never touched dPrevSignal, and dPrevSignal is what LongCondition() /
ShortCondition() / SignedAIConfidence() read. So a declustered bar lost its
arrow and still opened a position.
Measured on SP500 H1 2026-08-09: CONV called a direction on 64% of bars,
so the ~500 bars visible on screen held ~320 decisions - and ~40 arrows
were drawn. Roughly one arrow per eight positions the EA would take.
And the survivors are not a random eighth. Rule 2 of the declustering
keeps the HIGHER-CONFIDENCE side of a cluster, so the visible set is
systematically the best member of each run. A chart showing the best of
every eight decisions and hiding the rest reads far better than the model
is - the same best-of-N selection error already corrected in the geometry
scan, the indicator tuner, the lag profile and the deploy gate, this time
on the display layer, where it is most likely to mislead the person
deciding whether to trade.
Fixed by neutralising dPrevSignal when NMS rejects, rather than adding a
"may trade" flag consulted at each read site: that leaves exactly ONE
definition of what the model decided this bar, so the arrow, the panel's
"Current signal", the confidence feeding sizing/SL/TP/trailing, the
refresh tally and the order itself cannot drift apart again.
Also reports the consequence instead of hiding it. Every OOS counter on
the era line still scores every directional call - a population ~8x larger
than what now trades - so the line carries a second figure:
| TRADED (declustered) NN% on N calls (edge +Npp)
replaying the identical rule over pass 3 (which walks OOS bars oldest to
newest, the same order the live sweep sees). Its cursors are separate
members from the live ones so a training pass can never disturb the live
chart's declustering.
Deliberately NOT switched into selectionScore yet. Declustering cuts
coverage from ~64% of bars to ~8%, well under
MIN_COVERAGE_FRACTION_OF_BASE_RATE, which would make every checkpoint
undeployable overnight - the minRR collision and the recall-floor catch-22
twice over. The floor gets re-derived from these measurements first.
Compiles clean: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:22:31 -04:00
if ( m_signalClusterWindow > 0 )
{
fix: the deploy gate graded the un-thresholded model
coveragePct, dirPrecPct and the declustered TRADED tally were all computed
from oPrevSignal - the RAW argmax - while the live order, the arrow and the
panel all run on oDeploySignal, which is argmax AFTER the confidence
threshold. The gate was certifying a strategy the EA does not trade.
Invisible until now: the threshold sat at ~0.02, so the two populations
were the same set. The held-out calibration slice (2189316) moved it to
0.14-0.40 and the gap opened immediately - PAI era 256 graded 100% coverage
while its traded population was 21% (3,399 of ~16,200 OOS bars).
Consequences that were being hidden:
- coveragePct >= minCoveragePct was tested against the wrong population,
so a model whose TRADED coverage falls under the 24.8% floor still read
as clearing it
- precSE = sqrt(p(1-p)/n) used n ~16,000 instead of n ~3,400, so the
EDGE_MIN_SIGMAS bar was ~2.2x too lenient on the real evidence
- the NMS replay declustered a different, larger stream than live, so
threshold-rejected bars consumed cluster slots and set alternation state
Gate quantities now read m_oosBuyFired/m_oosSellFired (the thresholded
population, already tracked for the live-precision line) and the NMS replay
runs on oDeploySignal. The threshold can only turn a direction into Neutral,
never flip a side, so the fired set is a strict subset and every per-bar
outcome is the one already computed.
Recall and logBuyPrecPct deliberately stay on the raw argmax: they measure
intrinsic class separation, and thresholding them would conflate "cannot
separate the classes" with "declines to act on the separation it found".
This is the 9a7c37f defect class, and the NMS block carried a comment
warning about it while committing it three lines above.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 19:34:32 -04:00
//--- oDeploySignal, NOT oPrevSignal: live NMS runs downstream of the confidence
//--- threshold (RefreshLatestSignal feeds NmsLiveAccept the ADJUSTED decision), so
//--- replaying it on the raw argmax declusters a different, strictly larger stream
//--- than the EA ever sees - different survivors, not just more of them, because rule 1
//--- collapses runs and rule 3 alternates over whatever sequence it is given. Bars the
//--- threshold rejects must not consume a cluster slot or set the alternation state.
ENUM_SIGNAL nmsDir = DoubleToSignal ( oDeploySignal ) ;
fix: NMS gates the TRADE, not just the arrow - one arrow is now one trade
NmsLiveAccept() appeared in exactly one place: wrapped around DrawObject().
It never touched dPrevSignal, and dPrevSignal is what LongCondition() /
ShortCondition() / SignedAIConfidence() read. So a declustered bar lost its
arrow and still opened a position.
Measured on SP500 H1 2026-08-09: CONV called a direction on 64% of bars,
so the ~500 bars visible on screen held ~320 decisions - and ~40 arrows
were drawn. Roughly one arrow per eight positions the EA would take.
And the survivors are not a random eighth. Rule 2 of the declustering
keeps the HIGHER-CONFIDENCE side of a cluster, so the visible set is
systematically the best member of each run. A chart showing the best of
every eight decisions and hiding the rest reads far better than the model
is - the same best-of-N selection error already corrected in the geometry
scan, the indicator tuner, the lag profile and the deploy gate, this time
on the display layer, where it is most likely to mislead the person
deciding whether to trade.
Fixed by neutralising dPrevSignal when NMS rejects, rather than adding a
"may trade" flag consulted at each read site: that leaves exactly ONE
definition of what the model decided this bar, so the arrow, the panel's
"Current signal", the confidence feeding sizing/SL/TP/trailing, the
refresh tally and the order itself cannot drift apart again.
Also reports the consequence instead of hiding it. Every OOS counter on
the era line still scores every directional call - a population ~8x larger
than what now trades - so the line carries a second figure:
| TRADED (declustered) NN% on N calls (edge +Npp)
replaying the identical rule over pass 3 (which walks OOS bars oldest to
newest, the same order the live sweep sees). Its cursors are separate
members from the live ones so a training pass can never disturb the live
chart's declustering.
Deliberately NOT switched into selectionScore yet. Declustering cuts
coverage from ~64% of bars to ~8%, well under
MIN_COVERAGE_FRACTION_OF_BASE_RATE, which would make every checkpoint
undeployable overnight - the minRR collision and the recall-floor catch-22
twice over. The floor gets re-derived from these measurements first.
Compiles clean: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:22:31 -04:00
if ( nmsDir = = Buy | | nmsDir = = Sell )
{
fix: the deploy gate graded the un-thresholded model
coveragePct, dirPrecPct and the declustered TRADED tally were all computed
from oPrevSignal - the RAW argmax - while the live order, the arrow and the
panel all run on oDeploySignal, which is argmax AFTER the confidence
threshold. The gate was certifying a strategy the EA does not trade.
Invisible until now: the threshold sat at ~0.02, so the two populations
were the same set. The held-out calibration slice (2189316) moved it to
0.14-0.40 and the gap opened immediately - PAI era 256 graded 100% coverage
while its traded population was 21% (3,399 of ~16,200 OOS bars).
Consequences that were being hidden:
- coveragePct >= minCoveragePct was tested against the wrong population,
so a model whose TRADED coverage falls under the 24.8% floor still read
as clearing it
- precSE = sqrt(p(1-p)/n) used n ~16,000 instead of n ~3,400, so the
EDGE_MIN_SIGMAS bar was ~2.2x too lenient on the real evidence
- the NMS replay declustered a different, larger stream than live, so
threshold-rejected bars consumed cluster slots and set alternation state
Gate quantities now read m_oosBuyFired/m_oosSellFired (the thresholded
population, already tracked for the live-precision line) and the NMS replay
runs on oDeploySignal. The threshold can only turn a direction into Neutral,
never flip a side, so the fired set is a strict subset and every per-bar
outcome is the one already computed.
Recall and logBuyPrecPct deliberately stay on the raw argmax: they measure
intrinsic class separation, and thresholding them would conflate "cannot
separate the classes" with "declines to act on the separation it found".
This is the 9a7c37f defect class, and the NMS block carried a comment
warning about it while committing it three lines above.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 19:34:32 -04:00
//--- Confidence for rule 2's cross-direction resolution comes from the same adjusted
//--- decision, matching NmsLiveAccept's input exactly.
double nmsConf = MathAbs ( oDeploySignal ) ;
fix: NMS gates the TRADE, not just the arrow - one arrow is now one trade
NmsLiveAccept() appeared in exactly one place: wrapped around DrawObject().
It never touched dPrevSignal, and dPrevSignal is what LongCondition() /
ShortCondition() / SignedAIConfidence() read. So a declustered bar lost its
arrow and still opened a position.
Measured on SP500 H1 2026-08-09: CONV called a direction on 64% of bars,
so the ~500 bars visible on screen held ~320 decisions - and ~40 arrows
were drawn. Roughly one arrow per eight positions the EA would take.
And the survivors are not a random eighth. Rule 2 of the declustering
keeps the HIGHER-CONFIDENCE side of a cluster, so the visible set is
systematically the best member of each run. A chart showing the best of
every eight decisions and hiding the rest reads far better than the model
is - the same best-of-N selection error already corrected in the geometry
scan, the indicator tuner, the lag profile and the deploy gate, this time
on the display layer, where it is most likely to mislead the person
deciding whether to trade.
Fixed by neutralising dPrevSignal when NMS rejects, rather than adding a
"may trade" flag consulted at each read site: that leaves exactly ONE
definition of what the model decided this bar, so the arrow, the panel's
"Current signal", the confidence feeding sizing/SL/TP/trailing, the
refresh tally and the order itself cannot drift apart again.
Also reports the consequence instead of hiding it. Every OOS counter on
the era line still scores every directional call - a population ~8x larger
than what now trades - so the line carries a second figure:
| TRADED (declustered) NN% on N calls (edge +Npp)
replaying the identical rule over pass 3 (which walks OOS bars oldest to
newest, the same order the live sweep sees). Its cursors are separate
members from the live ones so a training pass can never disturb the live
chart's declustering.
Deliberately NOT switched into selectionScore yet. Declustering cuts
coverage from ~64% of bars to ~8%, well under
MIN_COVERAGE_FRACTION_OF_BASE_RATE, which would make every checkpoint
undeployable overnight - the minRR collision and the recall-floor catch-22
twice over. The floor gets re-derived from these measurements first.
Compiles clean: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:22:31 -04:00
int lastSame = ( nmsDir = = Buy ) ? m_oosNmsLastBuyIdx : m_oosNmsLastSellIdx ;
//--- 1) same-direction contiguous collapse; last-seen advances either way so a whole
//--- run collapses to its first bar.
bool cont = ( lastSame > = 0 & & ( lastSame - oi ) < = m_signalClusterWindow ) ;
if ( nmsDir = = Buy )
m_oosNmsLastBuyIdx = oi ;
else
m_oosNmsLastSellIdx = oi ;
bool keep = ! cont ;
//--- 2) cross-direction resolution against the last KEPT opposite signal: flicker at
//--- one turn zone resolves to the more confident side.
if ( keep & & m_oosNmsKeptIdx > = 0 & & m_oosNmsKeptDir ! = nmsDir & &
m_oosNmsKeptDir ! = Neutral & & ( m_oosNmsKeptIdx - oi ) < = m_signalClusterWindow )
keep = ( nmsConf > m_oosNmsKeptConf ) ;
2026-08-22 00:25:52 -04:00
//--- 3) ALTERNATION, identical to NmsLiveAccept's rule 3.
2026-08-10 14:26:12 -04:00
if ( keep & & BothDirectionsTradeable ( ) & & m_oosNmsKeptIdx > = 0 & &
m_oosNmsKeptDir = = nmsDir )
keep = false ;
fix: NMS gates the TRADE, not just the arrow - one arrow is now one trade
NmsLiveAccept() appeared in exactly one place: wrapped around DrawObject().
It never touched dPrevSignal, and dPrevSignal is what LongCondition() /
ShortCondition() / SignedAIConfidence() read. So a declustered bar lost its
arrow and still opened a position.
Measured on SP500 H1 2026-08-09: CONV called a direction on 64% of bars,
so the ~500 bars visible on screen held ~320 decisions - and ~40 arrows
were drawn. Roughly one arrow per eight positions the EA would take.
And the survivors are not a random eighth. Rule 2 of the declustering
keeps the HIGHER-CONFIDENCE side of a cluster, so the visible set is
systematically the best member of each run. A chart showing the best of
every eight decisions and hiding the rest reads far better than the model
is - the same best-of-N selection error already corrected in the geometry
scan, the indicator tuner, the lag profile and the deploy gate, this time
on the display layer, where it is most likely to mislead the person
deciding whether to trade.
Fixed by neutralising dPrevSignal when NMS rejects, rather than adding a
"may trade" flag consulted at each read site: that leaves exactly ONE
definition of what the model decided this bar, so the arrow, the panel's
"Current signal", the confidence feeding sizing/SL/TP/trailing, the
refresh tally and the order itself cannot drift apart again.
Also reports the consequence instead of hiding it. Every OOS counter on
the era line still scores every directional call - a population ~8x larger
than what now trades - so the line carries a second figure:
| TRADED (declustered) NN% on N calls (edge +Npp)
replaying the identical rule over pass 3 (which walks OOS bars oldest to
newest, the same order the live sweep sees). Its cursors are separate
members from the live ones so a training pass can never disturb the live
chart's declustering.
Deliberately NOT switched into selectionScore yet. Declustering cuts
coverage from ~64% of bars to ~8%, well under
MIN_COVERAGE_FRACTION_OF_BASE_RATE, which would make every checkpoint
undeployable overnight - the minRR collision and the recall-floor catch-22
twice over. The floor gets re-derived from these measurements first.
Compiles clean: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:22:31 -04:00
if ( keep )
{
m_oosNmsKeptIdx = oi ;
m_oosNmsKeptDir = nmsDir ;
m_oosNmsKeptConf = nmsConf ;
m_oosNmsFired + + ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//--- Label agreement of the surviving (position-becoming) calls.
bool nmsHit = ( nmsDir = = Buy ) ? oBuy : oSell ;
if ( nmsHit )
fix: NMS gates the TRADE, not just the arrow - one arrow is now one trade
NmsLiveAccept() appeared in exactly one place: wrapped around DrawObject().
It never touched dPrevSignal, and dPrevSignal is what LongCondition() /
ShortCondition() / SignedAIConfidence() read. So a declustered bar lost its
arrow and still opened a position.
Measured on SP500 H1 2026-08-09: CONV called a direction on 64% of bars,
so the ~500 bars visible on screen held ~320 decisions - and ~40 arrows
were drawn. Roughly one arrow per eight positions the EA would take.
And the survivors are not a random eighth. Rule 2 of the declustering
keeps the HIGHER-CONFIDENCE side of a cluster, so the visible set is
systematically the best member of each run. A chart showing the best of
every eight decisions and hiding the rest reads far better than the model
is - the same best-of-N selection error already corrected in the geometry
scan, the indicator tuner, the lag profile and the deploy gate, this time
on the display layer, where it is most likely to mislead the person
deciding whether to trade.
Fixed by neutralising dPrevSignal when NMS rejects, rather than adding a
"may trade" flag consulted at each read site: that leaves exactly ONE
definition of what the model decided this bar, so the arrow, the panel's
"Current signal", the confidence feeding sizing/SL/TP/trailing, the
refresh tally and the order itself cannot drift apart again.
Also reports the consequence instead of hiding it. Every OOS counter on
the era line still scores every directional call - a population ~8x larger
than what now trades - so the line carries a second figure:
| TRADED (declustered) NN% on N calls (edge +Npp)
replaying the identical rule over pass 3 (which walks OOS bars oldest to
newest, the same order the live sweep sees). Its cursors are separate
members from the live ones so a training pass can never disturb the live
chart's declustering.
Deliberately NOT switched into selectionScore yet. Declustering cuts
coverage from ~64% of bars to ~8%, well under
MIN_COVERAGE_FRACTION_OF_BASE_RATE, which would make every checkpoint
undeployable overnight - the minRR collision and the recall-floor catch-22
twice over. The floor gets re-derived from these measurements first.
Compiles clean: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:22:31 -04:00
m_oosNmsHits + + ;
}
}
}
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
// Same confusion counts keyed by what the model actually PREDICTED this bar, not the
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
// true label - see m_oos.buyPredicted's declaration comment for why recall alone can
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
// hide an over-firing class.
switch ( DoubleToSignal ( oPrevSignal ) )
{
case Buy :
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
m_oos . buyPredicted + + ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
if ( hit )
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
m_oos . buyPredictedHits + + ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
break ;
case Sell :
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
m_oos . sellPredicted + + ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
if ( hit )
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
m_oos . sellPredictedHits + + ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
break ;
default :
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
m_oos . neutralPredicted + + ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
if ( hit )
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
m_oos . neutralPredictedHits + + ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
break ;
}
2026-08-22 00:25:52 -04:00
//--- Live-decision precision: scores the bars on which the deployed EA would
//--- actually cast a directional vote, using the prior-corrected (logit-adjusted)
//--- posterior - see AdjustedSignalFromSoftmax()/RefreshLatestSignal().
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
if ( m_outputNeuronsCount = = 3 )
{
double adjSig = AdjustedSignalFromSoftmax ( ) ;
ENUM_SIGNAL adjEnum = DoubleToSignal ( adjSig ) ;
if ( adjEnum ! = Neutral )
{
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//--- Same currency as everywhere else in this block: label agreement.
bool fireHit = ( adjEnum = = Buy ) ? oBuy : oSell ;
2026-08-22 00:25:52 -04:00
//--- Bucket the same fire by confidence tier - see m_oosTierFired. It does
//--- not. ConfidenceTier() reads dPrevSignal, and dPrevSignal is assigned in
//--- PASS 1 only (the in-sample pass) - never anywhere in this OOS scan.
feat(rank): AI models rank their own confidence tiers from held-out outcomes
Closes the caveat 4858507 shipped with: the vote is a confidence percentage,
but only to the extent the pattern weights are measured. AI tier weights sat
at their designed defaults (25/50/75/100) because AI rows only ever arrive
from LIVE journaling, of which a training run produces almost none.
AND A STALE-TIER BUG THAT MADE THE EVIDENCE MEANINGLESS. The OOS scan bucketed
every scanned bar by ConfidenceTier(), which reads dPrevSignal - and
dPrevSignal is assigned in PASS 1 only, never anywhere in the OOS scan. So an
entire era's fires were bucketed by one stale, unrelated bar's confidence and
landed in a SINGLE tier. That is the "tier prec T0:72%(828) T1:n/a(0)
T2:n/a(0) T3:n/a(0)" symptom recorded on 2026-08-16 and attributed to the
calibration clamp. The clamp was real and was fixed then; this is a second,
independent cause of the identical output that survived that fix untouched -
which is why the log kept reading the same afterwards. Two causes, one symptom.
Now ConfidenceTierFor(adjSig): the bar this iteration actually scored.
WHY THIS DOES NOT WRITE ROWS TO THE SIGNAL DB, which was the obvious reading of
"fill the database during training". The user's own observation is the reason:
a classic Pattern_2 is a fixed geometric condition, so its win rate is
legitimately accumulated over years, but an AI Pattern_2 means "confidence
landed in tier 2" and tier 2 under era 100's weights is a different statement
from tier 2 under era 500's. The DB's value is ACCUMULATION, and accumulation
is exactly what is wrong here - it would average together models that no
longer exist, while colliding with the per-table row cap and mixing
measured-on-holdout outcomes into the live ledger's own tables. What the DB
actually supplies is a measured win rate per pattern, and pass 3 already
computes that on held-out bars, thousands at a time. So the model ranks itself
once per era, REPLACING rather than accumulating, which makes the weights
describe the current weights by construction.
ESTIMATOR. Not WinRateFromCounts(): it returns NO_DATA below 100 raw trades
BEFORE shrinking, which here would fire on every tier every era and hand all
four the pooled rate - the tiers could never separate and the mechanism would
be inert. Shrinkage is the answer to a small sample; a floor in front of it
means the shrinkage never runs. Instead: a Beta prior of TIER_PRIOR_EFF_N
pseudo-observations centred on the model's pooled holdout rate, counted in
EFFECTIVE observations, because overlapping triple-barrier labels mean 800 raw
fires can be worth ~12 independent ones. Rounded to the integer, not to the
decade NormalizeWinRate() uses, which would collapse the shrunk tiers back
into one number.
NO SAME-ERA CIRCULARITY, and it falls out of the ordering rather than a guard:
weights are computed at the END of era N, so the vote scored during era N was
cast with era N-1's weights. The deploy gate never grades a vote whose weights
were fitted on the bars it is scoring. Residual leakage remains - the same OOS
bars each era under a different model - and is stated in the code rather than
papered over.
Both DB clobber paths are closed: ApplyPatternWeight() declines once
self-ranked, and UpdateSignalsWeights()' filter.Weight() call is guarded by
SelfRanked() - guarding only the tiers would have let the hourly ranking pass
undo half the self-ranking.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:00:32 -04:00
int fireTier = ConfidenceTierFor ( adjSig ) ;
feat(ai): measure precision per confidence tier; fix stale metric labels
Two things the 2026-07-30 run exposed.
1. Every user-facing message still called the selection metric "balanced
accuracy". It has ranked on directional precision since a142749, so
"CONVERGED ... balanced accuracy 32.5%" was reporting a 32.5%
PRECISION as if it were macro-recall, while the same era logged an
actual balanced accuracy of 49%. Two different numbers under one
name, in the line that announces a deploy. Relabelled at every site,
including the stage-3 refusal, which still described the per-class
recall floor that stopped being the gate.
2. Precision is now bucketed by confidence tier and logged per era,
both per-tier and cumulatively from each tier upward:
| tier prec T0:19%(410)[>=28%/1204] T1:31%(520)[>=34%/794] ...
The per-tier number says whether confidence is calibrated to
correctness at all; if it does not rise T0->T3, raising the floor
buys nothing and that is the finding. The ">=" number is what a floor
would actually deliver, with its fire count, so the coverage cost is
visible in the same line. Tier weights are 25/50/75/100, so for an
AI-only config Min_Vote_Open maps straight across: 50 = ">=T1",
75 = ">=T2", 100 = ">=T3".
Bucketing happens at the existing live-fired accounting site, so it
measures exactly the population that trades - not the raw argmax.
Both builds compile 0 errors, 0 warnings. No retrain needed for either.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 11:47:15 -04:00
if ( fireTier > = 0 & & fireTier < 4 )
{
m_oosTierFired [ fireTier ] + + ;
if ( fireHit )
m_oosTierHits [ fireTier ] + + ;
}
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
if ( adjEnum = = Buy )
{
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
m_oos . buyFired + + ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
if ( fireHit )
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
m_oos . buyFiredHits + + ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
}
else
{
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
m_oos . sellFired + + ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
if ( fireHit )
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
m_oos . sellFiredHits + + ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
}
}
}
if ( hit )
{
dOosForecast + = ( 100 - dOosForecast ) / Net . recentAverageSmoothingFactor ;
dOosError - = dOosError / Net . recentAverageSmoothingFactor ;
}
else
{
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
dOosForecast - = dOosForecast / Net . recentAverageSmoothingFactor ;
dOosError + = ( 100 - dOosError ) / Net . recentAverageSmoothingFactor ;
}
}
//--- Predicted-class tally for the OOS window, which pass 1 used to compute from its
//--- own (now-removed) redundant feedForward on this same bar - see the
//--- laterPassForwards comment there.
switch ( DoubleToSignal ( oPrevSignal ) )
{
case Buy :
m_countBuySignals + + ;
break ;
case Sell :
m_countSellSignals + + ;
break ;
default :
m_countNeutralSignals + + ;
break ;
}
// Chart annotation for this (OOS) bar, using post-training weights - pass 1 no longer
// draws these at all (it used to, from a pre-training snapshot that this then overwrote).
m_lastBarTime = m_Time . GetData ( oi ) ;
if ( oi > 0 )
{
// NMS on: record only (the era-end sweep renders); off: draw inline. See pass 1's note.
if ( m_signalClusterWindow > 0 )
{
if ( oi < ArraySize ( m_arrowSignalCache ) )
m_arrowSignalCache [ oi ] = oDeploySignal ;
}
else
if ( DoubleToSignal ( oDeploySignal ) = = Neutral )
DeleteObject ( m_lastBarTime ) ;
else
DrawObject ( m_lastBarTime , oDeploySignal , m_Close . GetData ( oi ) ) ;
}
}
//--- Time OR stop - see pass 2's matching comment.
2026-08-23 11:40:15 -04:00
if ( m_oosScoreIndex - 1 > = 2 & & ( IsStopped ( ) | | era . BudgetSpent ( ) ) )
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
{
//--- yield: save enough to resume PASS 3 mid-walk on the next call - m_isPass3Active and
//--- m_oosScoreIndex (both members) carry the actual resume position.
StashEraResume ( era . bars , era . totalIter , era . oosCutoff , era . addLoop , era . i ) ;
return ;
}
}
m_isPass3Active = false ;
//--- Scoring finished - resume the normal always-adapting statistics (see the freeze at pass-3
//--- start) before anything else runs a forward pass.
Net . SetBatchNormFrozen ( false ) ;
//--- Pass 3 done => every scored bar's prediction is now in m_arrowSignalCache. Collapse each
//--- same-direction cluster to its earliest bar so the chart shows one arrow per real turn.
PruneDirectionalClusters ( era . bars ) ;
//--- ...and now that this era's per-tier outcomes are complete, turn them into the vote weights
//--- the NEXT era (and live trading) will use. See RankTiersFromOos().
RankTiersFromOos ( ) ;
//--- ...and, once per run and only if asked, put two completely different learners on this exact
refactor(baselines): the first real module - a class, not an #included partial
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>
2026-08-23 12:00:18 -04:00
//--- matrix so "the net is flat" can be told apart from "the matrix is flat". See CBaselineComparator.
m_baselines . RunBaselineComparison ( era . bars , era . totalIter , era . oosCutoff ) ;
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
}
}
//+------------------------------------------------------------------+
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
//| Shared preamble for the three exclusive walks. |
//| |
//| Each takes a whole Train() call, and each has to tell TWO |
//| watchdogs the same thing: the stall reporter which branch is |
//| running, and the era-barrier watchdog that this member is BUSY |
//| rather than stuck. Written out three times, it was three chances |
//| for a new walk to be added with only one of them. |
//+------------------------------------------------------------------+
void CExpertSignalAIBase : : ClaimCallForWalk ( const string branch )
{
ReportTrainStall ( branch ) ;
NoteBarrierProgress ( ) ;
}
//+------------------------------------------------------------------+
//| Why this member is idle at the ensemble era barrier. |
//| |
//| Resetting the stall watchdog was once the only thing the hold |
//| branch did, so a held member left no record anywhere. It reports |
//| on a cadence rather than on entry: a brief hold every era is the |
//| DESIGN - the fast member waits a few seconds here every era - |
//| and printing on entry logged ~950 lines per member per day. |
//+------------------------------------------------------------------+
void CExpertSignalAIBase : : ReportBarrierHold ( void )
{
//--- AND SAY SO IN THE JOURNAL. Resetting the watchdog above is right (a held member is idle,
//--- not stalled) but it was the ONLY thing this branch did, so a held member left no record
//--- anywhere.
uint nowTick = GetTickCount ( ) ;
//--- ARM SILENTLY, REPORT ONLY WHEN THE HOLD OUTLASTS THE INTERVAL (2026-08-19). Printing on
//--- entry logged ~950 lines/member/day, because a brief hold at the barrier is the DESIGN -
//--- the fast member waits a few seconds here every era.
bool justHeld = ( m_barrierHoldReportTick = = 0 ) ;
if ( justHeld )
m_barrierHoldReportTick = nowTick ;
if ( ( VerboseMode & & justHeld ) | |
nowTick - m_barrierHoldReportTick > = ENSEMBLE_BARRIER_REPORT_MS )
{
m_barrierHoldReportTick = nowTick ;
long minEra = EnsembleMinTrainingEra ( ) ;
string blockers = " " ;
for ( int bi = 0 ; bi < ArraySize ( g_warriorEnsemble ) ; bi + + )
{
CExpertSignalAIBase * bm = g_warriorEnsemble [ bi ] ;
if ( CheckPointer ( bm ) = = POINTER_INVALID )
continue ;
if ( bm . m_trainingComplete | | bm . m_trainingStopRequested | | bm . m_trainingPaused | |
! bm . m_isInitialized | | bm . m_barrierExcluded )
continue ;
if ( bm . m_eraCount < = minEra )
blockers + = ( blockers = = " " ? " " : " , " ) + bm . ID ;
}
if ( EnsembleLeadCapHolds ( ) )
PrintFormat ( " %s: HELD BY THE ENSEMBLE LEAD CAP - this member is at era %d, the slowest member "
" on the chart is at era %d, and %d eras is as far ahead as any member may get. "
" That slower member has ALREADY been dropped from the barrier for not advancing, "
" so nothing will resolve this on its own: diagnose it. Until it catches up the "
" combined vote cannot be scored and no joint checkpoint can be taken, so training "
" past this point would produce weights no gate could ever certify. " ,
ID , ( int ) m_eraCount , ( int ) EnsembleMinEraAnyMember ( ) , ENSEMBLE_MAX_ERA_LEAD ) ;
else
PrintFormat ( " %s: HELD AT THE ERA BARRIER - this member is at era %d and the ensemble minimum is "
" %d, so it is idle until [%s] catch up. It is NOT stalled and its weights are "
" untouched. If this line keeps repeating, the member(s) named are the ones to "
" diagnose - after %d minutes with no era AND no preparation-phase progress they "
" are dropped from the barrier and this member resumes, up to %d eras ahead. " ,
ID , ( int ) m_eraCount , ( int ) minEra , blockers = = " " ? " (none - resolving) " : blockers ,
( int ) ( ENSEMBLE_BARRIER_STUCK_MS / 60000 ) , ENSEMBLE_MAX_ERA_LEAD ) ;
}
}
//+------------------------------------------------------------------+
//| Does this call belong to training at all? |
//| |
//| Six ways it does not: paused, stopping, deploying an approved |
//| ensemble checkpoint, held at the era barrier, or occupied by one |
//| of the three exclusive walks. Each answers for the WHOLE call. |
//| |
//| None of this is training, which is why it is no longer inside |
//| Train(). What is left there now reads as the era lifecycle it |
//| always was, instead of opening with a hundred and twenty lines |
//| of reasons not to run. |
//| |
//| Writes era.stop, which the caller needs either way. |
//+------------------------------------------------------------------+
bool CExpertSignalAIBase : : TrainCallPreempted ( STrainEra & era )
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
{
//---
//--- Never block the calling thread while paused/stopped - just decline this call (or finalize a
//--- run that just got stopped) and let the next scheduled call check again, so Pause/Resume/Stop
//--- and everything else on the control panel stays responsive instead of Sleep()-ing the one
//--- MQL5 thread this chart has.
if ( m_trainingPaused & & ! IsStopped ( ) & & ! m_trainingStopRequested )
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
return true ;
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
era . stop = IsStopped ( ) | | m_trainingStopRequested ;
if ( era . stop )
{
if ( m_trainRunActive )
FinalizeTrainRun ( ) ;
refactor(online-learning): OnlineLearning is a real collaborator, not a raw-include partial (S5)
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>
2026-08-23 22:59:02 -04:00
m_onlineLearning . AbortSimIfActive ( ) ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
return true ;
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
}
//--- ENSEMBLE DEPLOY, approved by the ensemble gate on some member's era end (see
//--- EnsembleEraVerdict). Each member restores its own half of that checkpoint, so the quartet
//--- that goes live is the one the vote was measured on.
if ( m_ensembleMember & & g_ensDeployApproved & & ! m_trainingComplete & &
m_haveOosCheckpoint & & m_checkpointEra = = g_ensBestEra )
{
m_trainingComplete = true ;
Print ( ID + " : ENSEMBLE DEPLOY - restoring this model's weights from the joint checkpoint at era " +
IntegerToString ( ( int ) g_ensBestEra ) + " and switching to live inference. The combined vote, "
" not this model alone, is what cleared the gate. " ) ;
if ( m_trainRunActive )
FinalizeTrainRun ( ) ;
//--- Same one-shot pattern-database backfill the solo path arms at its era end, and for the
//--- same reason (see StartPatternDatabaseBackfill): a deployed model has to be RANKED the
//--- instant it goes live, not an hour of real trades later.
StartPatternDatabaseBackfill ( m_resumeBars , m_resumeTotalIter , m_resumeOosCutoff ) ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
return true ;
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
}
//--- ENSEMBLE ERA BARRIER (user request 2026-08-16): members advance era by era TOGETHER,
//--- because the number that matters - the combined-vote OOS score - is only well-defined when
//--- every member's pass 3 describes the same era, and because live trading is the members
//--- voting together, not four models drifting apart in training age.
BarrierEraHeartbeat ( ) ;
if ( EnsembleEraBarrierHolds ( ) )
{
//--- deliberate idleness, not a stall - keep the stall watchdog's era clock current and say
//--- what is happening on the member's panel line instead of freezing its last progress text
m_lastEraCompleteTick = GetTickCount ( ) ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
ReportBarrierHold ( ) ;
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
PublishStatus ( StringFormat ( " Waiting at era %d for slower ensemble members (min era %d) - donating its compute until they catch up " ,
( int ) m_eraCount , ( int ) EnsembleMinTrainingEra ( ) ) ) ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
return true ;
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
}
m_barrierHoldReportTick = 0 ;
//--- Evaluation-only continual-learning OOS simulation walk in progress (see
//--- StartOosContinualSimulation): give it exclusive occupancy of this call, same chunked budget
//--- as the real era loop below, so a large OOS window can't freeze the UI in one shot.
refactor(online-learning): OnlineLearning is a real collaborator, not a raw-include partial (S5)
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>
2026-08-23 22:59:02 -04:00
if ( m_onlineLearning . SimRunActive ( ) )
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
{
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
ClaimCallForWalk ( " OOS continual-learning simulation walk " ) ;
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
AdvanceOosSimulationChunk ( ) ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
return true ;
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
}
//--- One-shot pattern-database backfill in progress (see StartPatternDatabaseBackfill) - same
//--- exclusive-occupancy/chunking treatment as the simulation walk above.
refactor(online-learning): OnlineLearning is a real collaborator, not a raw-include partial (S5)
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>
2026-08-23 22:59:02 -04:00
if ( m_onlineLearning . BackfillActive ( ) )
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
{
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
ClaimCallForWalk ( " pattern-database backfill walk " ) ;
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
AdvancePatternDatabaseBackfill ( ) ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
return true ;
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
}
//--- Eager label-cache pre-build in progress (see StartLabelCachePrebuild/AdvanceLabelCachePrebuild) -
//--- same exclusive-occupancy/chunking treatment as the OOS simulation walk above, so it can't freeze
//--- the UI on a large study window either. m_trainRunActive stays false for its whole duration, so
//--- once it completes, Train() falls through to the normal !m_trainRunActive setup below and era 0
//--- starts from the measured class distribution it just seeded.
if ( m_labelPrebuildActive )
{
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
ClaimCallForWalk ( " label-cache prebuild scan " ) ;
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
AdvanceLabelCachePrebuild ( ) ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
return true ;
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
}
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
//--- Nothing claimed this call: it is a training call.
return false ;
}
//+------------------------------------------------------------------+
//| EVERYTHING AN ERA DOES AFTER ITS LAST PASS SCORES. |
//| |
//| Calibrate confidence, read the recalls, run the deploy gate, |
//| fill the telemetry, rank this era against the best so far, |
//| capture or restore a checkpoint, advance the learning-rate and |
//| plateau ladders, test stability, and persist. One era's verdict. |
//| |
//| Lifted whole rather than split, and deliberately so: its parts |
//| share thirty-odd locals - the recalls, the gate verdict, the |
//| better/worse flags - and threading those through three signatures |
//| would recreate the eight-locals-across-four-passes problem that |
//| STrainEra was built to end. Splitting this further needs an |
//| era-outcome object first, not more parameters. |
//| |
//| Nothing here returns early, which is why it could move at all: |
//| Train() still runs the era line, the finalizer and the backfill |
//| after it on every path. |
//+------------------------------------------------------------------+
void CExpertSignalAIBase : : CompleteEra ( STrainEra & era , SEraTelemetry & tel )
{
const int STABILITY_WINDOW = 3 ; // consecutive eras the OOS accuracy must hold steady for
const double STABILITY_TOLERANCE = 2.0 ; // max spread (percentage points) across that window
if ( ! era . stop )
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
{
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
dError = Net . getRecentAverageError ( ) ;
if ( era . addLoop )
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
{
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
if ( m_oosSamples > 0 )
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
{
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
// Confidence calibration (classification head only - see m_confidenceCalScale's
// declaration comment): compare this era's actual OOS accuracy against the average
// confidence magnitude the model claimed, EMA-blend the resulting scale into
// m_confidenceCalScale so SignedAIConfidence() reports something closer to a real
// probability instead of the raw, uncalibrated softmax value.
if ( m_outputNeuronsCount = = 3 & & m_oos . confidenceSum > 0.0 )
refactor(training): each of an era's four passes is its own method
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>
2026-08-23 11:30:31 -04:00
{
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
//--- accuracy / mean claimed confidence. Both terms would divide by the same sample
//--- count, so it cancels - which matters, because m_oosSamples is RUN-level while
//--- these tallies are per-era. Writing the ratio directly removes the chance that
//--- someone later logs or gates on one half and gets a number that decays with era
//--- count. If either term is ever needed alone, it needs a per-era denominator.
double eraScale = MathMax ( 0.3 , MathMin ( 1.5 , ( double ) m_oos . Hits ( ) / m_oos . confidenceSum ) ) ;
m_confidenceCalScale + = ( eraScale - m_confidenceCalScale ) / Net . recentAverageSmoothingFactor ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
}
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
// Per-class recall, reported and fed to the gate's two-sidedness test. A class with
// FEWER than MIN_OOS_CLASS_SAMPLES_FOR_GATE true OOS samples this era doesn't block
// (recallPct == -1 => treated as passing) so a thin OOS window doesn't deadlock
// convergence early in a run.
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
int buyRecallPct = m_oos . BuyRecallPct ( MIN_OOS_CLASS_SAMPLES_FOR_GATE ) ;
int sellRecallPct = m_oos . SellRecallPct ( MIN_OOS_CLASS_SAMPLES_FOR_GATE ) ;
int neutralRecallPct = m_oos . NeutralRecallPct ( MIN_OOS_CLASS_SAMPLES_FOR_GATE ) ;
2026-08-23 11:31:04 -04:00
tel . buyRecall = buyRecallPct ;
tel . sellRecall = sellRecallPct ;
tel . neutralRecall = neutralRecallPct ;
m_lastBuyRecallPct = buyRecallPct ;
m_lastSellRecallPct = sellRecallPct ;
//--- Predicted-rate (share of ALL OOS bars this era the model called this class,
//--- regardless of whether that call was right) and precision (of just those calls,
//--- how many were right) - see tel.buyPred's declaration comment above for why
//--- this is worth logging alongside recall.
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
int oosEraBars = m_oos . Bars ( ) ;
//--- ONE denominator for all of these, so the log's columns are directly comparable
//--- rather than nearly so; -1 = nothing to divide by. Neutral is the RESIDUAL on both
//--- layers: every OOS bar gets exactly one call, so what is not Buy and not Sell is
//--- Neutral by construction.
tel . buyTrue = m_oos . PctOfBars ( m_oos . buyTotal ) ;
tel . sellTrue = m_oos . PctOfBars ( m_oos . sellTotal ) ;
tel . neutralTrue = m_oos . PctOfBars ( m_oos . neutralTotal ) ;
tel . neutralPred = m_oos . NeutralPredictedShare ( ) ;
//--- The traded layer: candidates that cleared m_dirConfThreshold. A bar the operating
//--- point rejects is a bar the model sits out.
tel . buyFired = m_oos . PctOfBars ( m_oos . buyFired ) ;
tel . sellFired = m_oos . PctOfBars ( m_oos . sellFired ) ;
tel . neutralFired = m_oos . NeutralFiredShare ( ) ;
tel . buyPred = m_oos . PctOfBars ( m_oos . buyPredicted ) ;
tel . sellPred = m_oos . PctOfBars ( m_oos . sellPredicted ) ;
tel . buyPrec = SOosTally : : Pct ( m_oos . buyPredictedHits , m_oos . buyPredicted ) ;
tel . sellPrec = SOosTally : : Pct ( m_oos . sellPredictedHits , m_oos . sellPredicted ) ;
//--- Live-fired precision (what actually trades - see m_oos.buyFired): of the directional
2026-08-23 11:31:04 -04:00
//--- calls that cleared the confidence floor under the live/prior-corrected rule this era,
//--- how many were right. Cached for the panel/log; -1 = the model fired none this era.
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
tel . buyFiredPrec = SOosTally : : Pct ( m_oos . buyFiredHits , m_oos . buyFired ) ;
tel . sellFiredPrec = SOosTally : : Pct ( m_oos . sellFiredHits , m_oos . sellFired ) ;
2026-08-23 11:31:04 -04:00
m_lastBuyFiredPrecPct = tel . buyFiredPrec ;
m_lastSellFiredPrecPct = tel . sellFiredPrec ;
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
m_lastBuyFired = m_oos . buyFired ;
m_lastSellFired = m_oos . sellFired ;
2026-08-23 11:31:04 -04:00
//--- SELECTION METRIC. Ranking moved off balanced accuracy (macro-recall) 2026-07-30
//--- because that metric is maximized by exactly the model this system must never
//--- deploy.
refactor(gate): the member gate and the ensemble gate were one rule written twice
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>
2026-08-23 14:19:44 -04:00
//--- THE WHOLE DEPLOY DECISION, evaluated off the tally in one call. Tradeability,
//--- the bar it had to clear and the ranking key all come out together, because they
//--- are one decision - see Training\DeployGate.mqh for why splitting them was wrong.
SDeployVerdict gate ;
gate . Evaluate ( m_oos , EffectiveSampleSize ( ( double ) m_oos . DirCalls ( ) ) ,
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
buyRecallPct , sellRecallPct ) ;
refactor(oos): twenty-one counters with one lifetime become one object
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>
2026-08-23 14:15:22 -04:00
int oosDirCalls = m_oos . DirCalls ( ) ;
int oosDirHits = m_oos . DirHits ( ) ;
int oosDirTrue = m_oos . DirTrue ( ) ;
refactor(gate): the member gate and the ensemble gate were one rule written twice
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>
2026-08-23 14:19:44 -04:00
bool coverageMeasurable = gate . measurable ;
double coveragePct = gate . coveragePct ;
double dirPrecPct = gate . precPct ;
double chancePrecPct = gate . chancePct ;
2026-08-23 11:31:04 -04:00
tel . coverage = ( int ) MathRound ( coveragePct ) ;
tel . dirPrec = ( int ) MathRound ( dirPrecPct ) ;
tel . chancePrec = ( chancePrecPct > = 0.0 ) ? ( int ) MathRound ( chancePrecPct ) : -1 ;
//--- Deployability. Replaces the per-class recall floor as the gate the checkpoint
//--- selection and the plateau ladder's "is there anything safe to deploy" test
//--- read. Observed 2026-08-01: the perceptron deployed at edge +0pp.
refactor(gate): the member gate and the ensemble gate were one rule written twice
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>
2026-08-23 14:19:44 -04:00
double precSE = gate . precSE ;
double edgeFloorPct = gate . edgeFloorPct ;
2026-08-23 11:31:04 -04:00
//--- PUBLISHED so the era line can state the bar instead of leaving it implicit.
refactor(gate): the member gate and the ensemble gate were one rule written twice
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>
2026-08-23 14:19:44 -04:00
//--- Nothing will ever clear an unreachable bar, and until this line printed it the
//--- symptom was indistinguishable from "the models are close but not quite".
2026-08-23 11:31:04 -04:00
m_lastEdgeFloorPct = edgeFloorPct ;
m_lastPrecSE = precSE ;
refactor(gate): the member gate and the ensemble gate were one rule written twice
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>
2026-08-23 14:19:44 -04:00
m_lastEffN = gate . effN ;
2026-08-23 11:31:04 -04:00
//--- CONTRIBUTE THIS ERA'S EVIDENCE TO THE CROSS-INSTRUMENT POOL, then read the pool
//--- back. See PooledGate.mqh.
if ( coverageMeasurable & & dirPrecPct > = 0.0 & & chancePrecPct > 0.0 )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
{
2026-08-23 11:31:04 -04:00
PublishPoolRecord ( chancePrecPct , dirPrecPct , m_lastEffN ) ;
m_lastPoolPasses = PooledGatePasses ( m_lastPoolReport ) ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
}
2026-08-23 11:31:04 -04:00
//--- BOTH sides must still be alive - see DEPLOY_MIN_SIDE_RECALL_PCT. A negative recall
//--- means "not measurable this era" (no true bars of that class in the OOS window), and
//--- that must not be read as a dead side, so it passes.
2026-08-23 14:40:29 -04:00
bool bothSidesLive = gate . twoSided ;
refactor(gate): the member gate and the ensemble gate were one rule written twice
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>
2026-08-23 14:19:44 -04:00
//--- Tradeability is ALSO the lexicographic ranking key (isBetterEra) and the g_eta-
//--- decay trigger, not merely a deploy-time check - see DeployGate.mqh.
bool tradeableOK = gate . tradeable ;
double selectionScore = gate . selectionScore ;
2026-08-23 11:31:04 -04:00
//--- Under precision ranking the degenerate era is the one that called NOTHING
//--- directional (precision undefined, nothing to trade), not one whose per-class
//--- recall touched zero - a sparse high-precision model legitimately has low recall.
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//--- The old per-class recall FLOOR is gone with the rest of the anti-collapse devices:
//--- the logit-adjusted loss is the one imbalance mechanism, and the gate's twoSided
//--- test already refuses a one-class model at deploy time.
refactor(gate): the member gate and the ensemble gate were one rule written twice
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>
2026-08-23 14:19:44 -04:00
bool isFullyCollapsedEra = gate . degenerate ;
2026-08-23 11:31:04 -04:00
//--- N for the family-wise deployment gate. Every era that COULD have won is
//--- counted, whether it did or not - that is precisely the set the maximum was
//--- taken over.
if ( coverageMeasurable & & ! isFullyCollapsedEra )
m_deployCandidateEras + + ;
//--- Lexicographic "better than the best-so-far" ordering: passing the directional
//--- recall floor always outranks not passing it, regardless of blended
//--- dOosForecast; only WITHIN the same pass/fail category does blended accuracy
//--- break the tie.
//--- tradeableOK / selectionScore, not directionalRecallOK / balancedOosEra - see
//--- the SELECTION METRIC note above.
fix(training): a new best must beat the noise; the blank-chart census must name its cause
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>
2026-08-24 15:58:34 -04:00
//--- SAME NOISE BAND as the ensemble's isBetter (see PLATEAU_NEW_BEST_SIGMAS). A solo
//--- run plateaus on exactly the same mechanism, so it must ratchet on exactly the same
//--- rule - the two orderings are documented as identical and have to stay that way.
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
double newBestBandEra = ( m_bestSelectionScore > = 0.0 )
feat(deploy): ship on positive EXPECTANCY, and let the chart draw before convergence
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>
2026-08-24 16:15:06 -04:00
? PLATEAU_NEW_BEST_SIGMAS * gate . scoreSE : 0.0 ;
2026-08-23 11:31:04 -04:00
bool isBetterEra = ( tradeableOK & & ! m_bestPassedRecall ) | |
( tradeableOK = = m_bestPassedRecall & & bothSidesLive & & ! m_bestBothSidesLive ) | |
( tradeableOK = = m_bestPassedRecall & & bothSidesLive = = m_bestBothSidesLive & &
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
! isFullyCollapsedEra & & selectionScore > m_bestSelectionScore + newBestBandEra ) ;
2026-08-23 11:31:04 -04:00
//--- The recall-pass-loss clause used to fire on ANY drop out of a full 3-way recall
//--- pass, even a near-miss on one class at unchanged accuracy (e.g. observed:
//--- Buy:56% Sell:41% Neutral:34% - Neutral alone missing the 40% floor by a few
//--- points) - treating that identically to a total collapse back to Neutral-only.
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
bool isWorseEra = selectionScore < m_bestSelectionScore - ETA_DECAY_REGRESSION_PCT ;
2026-08-23 11:31:04 -04:00
//--- ENSEMBLE: ranking and checkpointing belong to the ensemble as a unit (see
//--- EnsembleCommitJointCheckpoint). The g_eta recovery bump still applies: that is
//--- this net's own learning-rate dynamics, not a deployment decision.
if ( isBetterEra & & m_ensembleMember )
g_eta = MathMin ( m_etaCeiling , g_eta / ETA_DECAY_FACTOR ) ;
if ( isBetterEra & & ! m_ensembleMember )
{
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//--- Snapshot BOTH scores at the checkpoint: m_bestSelectionScore is what ranking
//--- compares against next era; m_bestOosForecast keeps the blended value
//--- FinalizeTrainRun() and the restore branch reset dOosForecast to.
2026-08-23 11:31:04 -04:00
m_bestOosForecast = dOosForecast ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
m_bestSelectionScore = selectionScore ;
2026-08-23 11:31:04 -04:00
m_bestPassedRecall = tradeableOK ;
m_bestBothSidesLive = bothSidesLive ;
//--- Raw significance inputs for the family-wise gate, taken at the same instant as the
//--- weight snapshot below so the test always describes the weights that would ship.
//--- selectionScore cannot substitute: it is precision x coverage credit, and the test
//--- needs the unweighted precision plus the n that sets its standard error.
m_bestDirPrecPct = dirPrecPct ;
m_bestChancePrecPct = chancePrecPct ;
m_bestDirCalls = oosDirCalls ;
//--- The operating point is part of the model, not of the run: these OOS numbers were
//--- produced by these weights UNDER this threshold, and restoring one without the
//--- other would deploy a model whose coverage and precision are not the ones the gate
//--- cleared. Captured at the same instant as the weight snapshot below.
m_bestDirConfThreshold = m_dirConfThreshold ;
//--- eval candidates are throwaway - track the score (above) but never write a
//--- checkpoint file; m_haveOosCheckpoint=false then also skips the worse-era
//--- RestoreWeights() restore.
m_haveOosCheckpoint = Net . CaptureWeights ( ) ;
//--- Recovery bump: ETA_DECAY_FACTOR-only ever shrinks g_eta, and previously
//--- nothing ever grew it back - a losing streak early in a run (even a since-
//--- corrected one) would permanently cap how fast every later era could learn
//--- for the rest of the run, all the way down to ETA_MIN with no way back.
g_eta = MathMin ( m_etaCeiling , g_eta / ETA_DECAY_FACTOR ) ;
}
else
if ( isWorseEra & & m_bestOosForecast > 0 )
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus
The NN now has a target that is not per-bar direction (closed, best-of-999
p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of
cost). One net for all 52 pattern-sides, AIType=AI_META.
- NetForward.mqh: the host-side softmax+CE gradient generalized total==3 ->
2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no
compute backend changes.
- SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on
disk (decoupled from the config fingerprint that burned four S1 runs); the
GMT->server offset is measured PER ROW against entryPrice vs bar open
(DST-immune, histogram logged); a window-span regime filter drops the
pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the
input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR).
- Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate
calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell
lets checkpoint selection, the edge floor, the plateau ladder and the
family-wise deploy gate run UNCHANGED: precision reads as win rate among
traded candidates, chance as the base win rate, recalls as sensitivity/
specificity. Era-end META line: coverage x (p - break-even) vs the null.
- Labels are the side-conditional triple-barrier win caches - never the DB's
stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base
rate). Live inference + online learning guarded off until S3.
- Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename
slot keep meta models fully separate from direction models.
Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with
AIType=AI_META; S3 wires the votes via the per-side hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
{
2026-08-23 11:31:04 -04:00
// Decaying g_eta alone only softens FUTURE steps - it does nothing to undo the
// regression this era already baked into the weights, so a run could (and in
// practice did) spend 15+ eras compounding forward from one bad era's damage,
// each new era fighting the last one's overshoot instead of building on the best
// state found so far. Restore the last checkpointed-good weights before continuing
// (mirrors what FinalizeTrainRun() does at the END of a run, just applied live so
// the oscillation can't compound within a single run) - this is what actually turns
// "reduce LR on regression" into "step back, then retry slower", not just "drift
// slower".
//
// BOTH the restore AND the g_eta decay below are gated on m_bestPassedRecall: before
// ANY era has ever cleared the per-class recall floor, isBetterEra's own
// lexicographic ordering degrades to a pure blended-accuracy tiebreak
// (directionalRecallOK==false on both sides of the comparison), so "best checkpoint"
// during that phase just means "called Neutral most confidently so far" - restoring
// it would actively defend the majority-class collapse against any era that trades
// some accuracy for real Buy/Sell recall, which is exactly the bias this whole
// recall-gate mechanism exists to prevent (see isBetterEra's own comment above).
// Observed in practice: era 1-3 all "improved" on accuracy alone
// (24.9%->41.4%->52.3%) while Buy/Sell recall stayed at a flat 0% the entire time -
// restoring pre-pass would have locked training into that trajectory instead of
// letting it explore past it. Decaying g_eta has the same bias one step removed:
// every regression relative to a Neutral-collapse "best" shrinks g_eta a little more,
// steadily strangling the exploration needed to escape that collapse until g_eta
// bottoms out at ETA_MIN with no real solution ever found and no checkpoint to fall
// back on either - observed in practice as a run whose best-ever blended accuracy
// kept landing on 0%/0%/100% Buy/Sell/Neutral recall eras, each one triggering
// another decay on the very next era, until g_eta floored out around era 20 and the
// remaining eras just oscillated between collapse states with no way to make a
// large-enough move to escape and no way to reset. Once m_bestPassedRecall is true,
// there IS a genuinely good state worth protecting, and both restoring the
// checkpoint and decaying g_eta on regression are safe/correct again.
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
// Defend the checkpoint only once it sits clearly above the zero-skill rate it was
// measured against - restoring (and decaying g_eta toward) a best that is itself
// chance-level strangles the exploration needed to escape it.
bool bestWorthDefending = ( m_bestChancePrecPct > 0.0 & &
m_bestSelectionScore >
m_bestChancePrecPct + WORTH_DEFENDING_MARGIN_PCT ) ;
2026-08-23 11:31:04 -04:00
//--- PATIENCE (see ETA_DECAY_PATIENCE_ERAS). The loop is self-sustaining and
//--- cannot discover anything, because rolling the weights back is precisely
//--- what removes the exploration that would end it.
m_consecutiveRegressions + + ;
if ( ( m_bestPassedRecall | | bestWorthDefending ) & &
m_consecutiveRegressions > = ETA_DECAY_PATIENCE_ERAS )
2026-08-13 09:03:40 -04:00
{
2026-08-23 11:31:04 -04:00
m_consecutiveRegressions = 0 ;
if ( m_haveOosCheckpoint & & Net . RestoreWeights ( ) )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
{
perf(deinit): I/O-free chart cleanup, dead-panel purge, skip clean weight saves
The 18:23 terminal close (20260825.log) killed two of six charts inside
OnDeinit: they printed "shutting down" then nothing for 5.9 s until
"Abnormal termination", stranding ~700 objects each - including the one
family no prefix sweep can reach, the control panel (CAppDialog names
its 15 objects <numeric instance id><control>, and a re-attach mints a
new id, so a killed panel is a permanent ghost; XTIUSD carried one
across sessions). The stall sat in the two file writes that preceded
all visible cleanup while the four sibling charts flooded the same
2013-era disk - the ~4x18MB-per-chart shutdown weight saves.
Three changes:
1. OnDeinit touches no file until the chart is clean. CVoteArrowStore
splits Save() into Snapshot() (the chart scan, in memory) and
WriteSnapshot() (the disk half, consuming). New order: status label,
vote-arrow snapshot, prefix sweep, panel destroy - all object ops -
then member sidecars, final sweep, timings, and only then the
visibility file, the vote-arrow write and the weight saves.
2. PurgeOrphanedPanelObjects() at OnInit: deletes numeric-prefix
CAppDialog ghosts by name (6 chrome + 9 buttons), qualifying a
prefix only when >=4 of OUR button names carry it, so a foreign
dialog sharing stock chrome names is never touched.
3. m_netDirty: set by every net mutation (both backProp sites, both
RestoreWeights sites, online learning conservatively, panel reset),
cleared only on a successful Net.Save. Shutdown AND the per-bar
autosave now skip the ~18MB write when the net is provably unchanged
- for converged ensembles that is every save - which removes the
very flood that starved the sibling charts. .stats still writes
every time (small; carries the vote record and calibration). A
skipped save leaves the .nnw header dtStudied stale, which is the
already-handled attach-after-offline-gap case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:46:51 -04:00
m_netDirty = true ; // an in-memory weight swap is a mutation like any other
2026-08-23 11:31:04 -04:00
dOosForecast = m_bestOosForecast ;
//--- The operating point goes back with the weights it was fitted for.
//--- Leaving the current one in place would pair restored weights with a
//--- threshold chosen for the rejected ones - see m_bestDirConfThreshold.
m_dirConfThreshold = m_bestDirConfThreshold ;
//--- 2026-08-09 audit, F3: the snapshot restores WEIGHTS only, so without
//--- this the Adam moments still encode the just-rejected trajectory and
//--- the first updates after the restore push straight back toward the
//--- state that was rolled back - the restore -> regress-again -> restore
//--- oscillation. A restore is a new starting point; it gets a fresh
//--- optimizer.
Net . ResetOptimizerState ( ) ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
}
2026-08-23 11:31:04 -04:00
if ( g_eta > ETA_MIN )
g_eta = MathMax ( ETA_MIN , g_eta * ETA_DECAY_FACTOR ) ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
Print ( ID + " : OOS selection score (coverage-weighted dir-precision) regressed from best " + DeployScoreText ( m_bestSelectionScore ) +
fix(deploy): print the selection score in the unit it is actually in
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>
2026-08-24 17:21:57 -04:00
" to " + DeployScoreText ( selectionScore ) + " (blended " + DoubleToString ( m_bestOosForecast , 1 ) +
2026-08-23 11:31:04 -04:00
" %-> " + DoubleToString ( dOosForecast , 1 ) + " %) - restoring best checkpoint and decaying learning rate to " + DoubleToString ( g_eta , 6 ) ) ;
feat(search): stop on the IN-SAMPLE plateau, and shrink every best-of-K effect before quoting it
Points 3 and 4 of the four-point plan.
1. IN-SAMPLE EARLY STOP - and the reason it is worth having is not compute.
The plateau ladder stops on the OOS SELECTION score. That is a peek: by the time it
fires, every one of those eras has been evaluated out of sample, so all of them sit
in the family the deploy gate corrects over (g_ensCandidateEras, Sidak). Training
longer therefore does not merely cost time - it RAISES the bar the eventual winner
has to clear.
The new stop reads the TRAINING error, which the gate never looks at. When the
optimiser has stopped improving on data it can see, more eras will not find a better
model; they will only enlarge the OOS family. Ending there shrinks the correction,
and the shrinkage is legitimate precisely BECAUSE the stopping rule never consulted
an out-of-sample number.
That distinction is the whole point and it is the one this project has got wrong four
times: stop on IS and the family really is smaller; stop on OOS and those eras were
searched and still count. Both stops now exist; only this one buys a lower bar.
Deliberately more patient than the OOS ladder (IS_ERROR_PATIENCE_MULT = 3x): training
error is noisy per era - mini-batch order alone moves it - and ending a run that is
still learning costs far more than a few wasted eras. Improvement is RELATIVE
(IS_ERROR_IMPROVE_FRAC = 1%), so it does not depend on the loss's absolute scale, and
it only acts when a checkpoint exists, since otherwise it would end a run with
nothing to deploy. Reset per RUN alongside the ladder, so a resumed run cannot
early-stop on its first era against a previous run's best.
2. WINNER'S-CURSE SHRINKAGE ON THE BARRIER-GEOMETRY WINNER.
The family-wise permutation gate already establishes that the RANKING is not noise.
It says nothing about the SIZE of the winner's effect - and a best-of-K maximum is
biased upward by construction, being the largest of K noisy draws. The adoption
message quotes that raw maximum and compares it against the incumbent, so the number
a reader plans on is the inflated one.
The penalty is now measured, not assumed: the same permutation draws that produce the
p-value also produce, per draw, the MAXIMUM excess across all candidates under pure
noise. The mean of those maxima is exactly what a best-of-K selection is expected to
report when there is nothing there. This is the empirical form of the sqrt(2 ln K) x SE
penalty the SQX EdgeFinder plugin applies to every maximum it reports (Stats.java:79-88),
and it needs no normality assumption because the draws ARE the null distribution.
Applied in James-Stein form - effect x max(0, 1 - penalty^2/effect^2) - so a large
effect is nearly untouched and a marginal one collapses toward zero.
Reported, not gated. The adoption decision still turns on the permutation p-value,
which is the right test for "is the ranking real"; the shrunk number is there so the
magnitude quoted beside it is one worth planning on. Closes the first of the two
EdgeFinder ports identified on 2026-08-12.
NOTE on the second EdgeFinder port, deliberately not done here: "let the measurement
steer the target" is already true where it matters most - ReportGeometryExpectancyScan
ADOPTS the winning barrier geometry under the family-wise gate rather than advising
it, and the MI excursion suite publishes a verdict per instrument per config. What is
still missing is steering the TRAINING TARGET itself (direction vs excursion) off
those verdicts, and that is a design change rather than a surgical one - direction is
a closed verdict while excursion SIZE keeps clearing, so the honest version of that
change is a target-selection policy, not a flag.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 16:29:00 -04:00
}
else
2026-08-23 11:31:04 -04:00
//--- THROTTLED (2026-08-19): this no-action branch repeated ~600x/day while
//--- noise wandered below a best it was never going to displace. The acting
//--- branch above (restore + g_eta decay) still always prints - it changes state.
if ( TrainLogDue ( ) )
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
Print ( ID + " : OOS selection score (coverage-weighted dir-precision) regressed from best " + DeployScoreText ( m_bestSelectionScore ) +
fix(deploy): print the selection score in the unit it is actually in
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>
2026-08-24 17:21:57 -04:00
" to " + DeployScoreText ( selectionScore ) + " (blended " + DoubleToString ( m_bestOosForecast , 1 ) +
2026-08-23 11:31:04 -04:00
" %-> " + DoubleToString ( dOosForecast , 1 ) + " %) - best so far is still within " +
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
DoubleToString ( WORTH_DEFENDING_MARGIN_PCT , 1 ) + " pp of its own chance rate, so there is nothing worth " +
2026-08-23 11:31:04 -04:00
" restoring yet - continuing to explore without decaying the learning rate (still " + DoubleToString ( g_eta , 6 ) + " ) " ) ;
feat(search): stop on the IN-SAMPLE plateau, and shrink every best-of-K effect before quoting it
Points 3 and 4 of the four-point plan.
1. IN-SAMPLE EARLY STOP - and the reason it is worth having is not compute.
The plateau ladder stops on the OOS SELECTION score. That is a peek: by the time it
fires, every one of those eras has been evaluated out of sample, so all of them sit
in the family the deploy gate corrects over (g_ensCandidateEras, Sidak). Training
longer therefore does not merely cost time - it RAISES the bar the eventual winner
has to clear.
The new stop reads the TRAINING error, which the gate never looks at. When the
optimiser has stopped improving on data it can see, more eras will not find a better
model; they will only enlarge the OOS family. Ending there shrinks the correction,
and the shrinkage is legitimate precisely BECAUSE the stopping rule never consulted
an out-of-sample number.
That distinction is the whole point and it is the one this project has got wrong four
times: stop on IS and the family really is smaller; stop on OOS and those eras were
searched and still count. Both stops now exist; only this one buys a lower bar.
Deliberately more patient than the OOS ladder (IS_ERROR_PATIENCE_MULT = 3x): training
error is noisy per era - mini-batch order alone moves it - and ending a run that is
still learning costs far more than a few wasted eras. Improvement is RELATIVE
(IS_ERROR_IMPROVE_FRAC = 1%), so it does not depend on the loss's absolute scale, and
it only acts when a checkpoint exists, since otherwise it would end a run with
nothing to deploy. Reset per RUN alongside the ladder, so a resumed run cannot
early-stop on its first era against a previous run's best.
2. WINNER'S-CURSE SHRINKAGE ON THE BARRIER-GEOMETRY WINNER.
The family-wise permutation gate already establishes that the RANKING is not noise.
It says nothing about the SIZE of the winner's effect - and a best-of-K maximum is
biased upward by construction, being the largest of K noisy draws. The adoption
message quotes that raw maximum and compares it against the incumbent, so the number
a reader plans on is the inflated one.
The penalty is now measured, not assumed: the same permutation draws that produce the
p-value also produce, per draw, the MAXIMUM excess across all candidates under pure
noise. The mean of those maxima is exactly what a best-of-K selection is expected to
report when there is nothing there. This is the empirical form of the sqrt(2 ln K) x SE
penalty the SQX EdgeFinder plugin applies to every maximum it reports (Stats.java:79-88),
and it needs no normality assumption because the draws ARE the null distribution.
Applied in James-Stein form - effect x max(0, 1 - penalty^2/effect^2) - so a large
effect is nearly untouched and a marginal one collapses toward zero.
Reported, not gated. The adoption decision still turns on the permutation p-value,
which is the right test for "is the ranking real"; the shrunk number is there so the
magnitude quoted beside it is one worth planning on. Closes the first of the two
EdgeFinder ports identified on 2026-08-12.
NOTE on the second EdgeFinder port, deliberately not done here: "let the measurement
steer the target" is already true where it matters most - ReportGeometryExpectancyScan
ADOPTS the winning barrier geometry under the family-wise gate rather than advising
it, and the MI excursion suite publishes a verdict per instrument per config. What is
still missing is steering the TRAINING TARGET itself (direction vs excursion) off
those verdicts, and that is a design change rather than a surgical one - direction is
a closed verdict while excursion SIZE keeps clearing, so the honest version of that
change is a target-selection policy, not a flag.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 16:29:00 -04:00
}
2026-08-23 11:31:04 -04:00
//=== IN-SAMPLE ERROR PLATEAU: THE HONEST EARLY STOP ====================================
//--- The ladder below stops on the OOS SELECTION score. This stop reads the TRAINING
//--- error instead, which the gate never looks at. Both stops exist; only this one
//--- buys a lower bar.
if ( dError > = 0.0 & & MathIsValidNumber ( dError ) )
{
//--- Relative improvement, so this does not depend on the loss's absolute scale.
if ( m_bestIsError < 0.0 | | dError < m_bestIsError * ( 1.0 - IS_ERROR_IMPROVE_FRAC ) )
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
{
2026-08-23 11:31:04 -04:00
m_bestIsError = dError ;
m_erasSinceBestIsError = 0 ;
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
}
else
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
{
2026-08-23 11:31:04 -04:00
m_erasSinceBestIsError + + ;
//--- Deliberately more patient than the OOS ladder: training error is noisy
//--- per era (mini-batch order alone moves it), and ending a run that is still
//--- learning is far more expensive than a few wasted eras.
if ( m_erasSinceBestIsError > = TrainPlateauPatienceEras ( ) * IS_ERROR_PATIENCE_MULT & &
m_haveOosCheckpoint & & ! m_isErrorPlateaued )
{
Print ( ID + " : IN-SAMPLE ERROR PLATEAU - training error has not improved by " +
DoubleToString ( 100.0 * IS_ERROR_IMPROVE_FRAC , 1 ) + " % in " +
IntegerToString ( m_erasSinceBestIsError ) + " eras (best " +
DoubleToString ( m_bestIsError , 4 ) + " , now " + DoubleToString ( dError , 4 ) +
" ). The optimiser has stopped learning from the data it CAN see, so further "
" eras cannot find a better model - they would only add candidates to the "
" family the deploy gate corrects over, raising the bar the winner has to "
" clear. Ending the search and deploying the best checkpoint. This stop "
" never read an out-of-sample number, which is what makes the smaller "
" family legitimate rather than a peek. " ) ;
//--- LATCH FIRST, and let the LATCH - not m_plateauStage - be what the deploy
//--- conditions read. m_plateauStage is mirrored from the shared ensemble ladder
//--- on every era (EnsembleEraVerdict), so writing the decision there meant it
//--- survived until the next verdict and no longer. See m_isErrorPlateaued.
m_isErrorPlateaued = true ;
m_plateauStage = PLATEAU_STAGE_DEPLOY ;
}
perf(autotune): replace the genetic search with a filter score - hours to seconds
MEASURED COST OF THE GA, which is what retired it. Per generation:
rung 0: 8 cand x 3 seeds x 3 eras = 72 eras
rung 1: 4 cand x 3 seeds x 8 eras = 96
rung 2: 2 cand x 3 seeds x 20 eras = 120
= 288 eras/generation x 4 generations = 1152 eras BEFORE the winner's
real training began. Against the observed era times on SP500 H1:
PAI 29.1 s/era -> 9.3 h (matches the observed 00:37 -> 09:22)
CONV 41.3 s/era -> 13.2 h
LSTM 150.4 s/era -> 48.1 h
HYBRID 154.6 s/era -> 49.5 h
Two days to tune is not a first-run experience, and it is the phase in
which the panel goes quiet, which is what made it look like a hang.
It also bought nothing. The space is 90 points (10 MA periods x 9 MA
types), so 1152 evaluations revisited each point ~13 times; and rungs of
3 and 8 eras cannot separate two MA periods at all. The 2026-08-01 run
proves it: every finalist scored 25.0-25.9% balanced accuracy - below the
33.3% one-class floor, i.e. indistinguishable noise - and the search then
"deployed the winner" of that.
THE ERROR WAS THE SCORING FUNCTION, not its constants. Using a full
training run to choose a feature's period is a wrapper method paying
wrapper prices for a decision that does not need one. The reference book
does not do this: ch. 3.3 selects inputs by measuring each candidate
indicator's CORRELATION with the target and dropping the ones with none,
with no network involved.
So: rank candidates by the MUTUAL INFORMATION between the resulting
feature vector and the triple-barrier label. MI rather than correlation
because the label is 3-class categorical and the features are not
monotonically related to it. Equal-FREQUENCY binning (rank-based),
because these features are ATR-normalised and heavy-tailed - fixed-width
bins put nearly everything in one bucket and report ~0 information for a
genuinely useful feature.
Scoring is arithmetic over the feature cache, so it costs seconds and its
cost is independent of topology: LSTM now tunes as fast as the MLP.
Coordinate sweep, not product sweep - cost is the SUM of per-parameter
candidate counts, so enabling every indicator stays affordable - with a
second pass that breaks early once nothing moves.
Sampling is IS-ONLY. Letting the OOS window influence which indicator
settings ship would mean the holdout had been used for selection and had
stopped being a holdout.
HONEST LIMIT, recorded because it is the price: MI is marginal, so a
parameter that only pays off in combination with another can be missed
(Guyon & Elisseeff 2003, filter vs wrapper). Given the wrapper it
replaces was ranking pure noise at 48 h a run, this is strictly better.
Deleted with it: GaRungEras/GaExtract/GaStore/GaMutate/GaRandomCandidate/
GaBlockCrossover/GaSortAliveByScoreDesc/GaBreedNextGeneration, 14 m_ga*
members, the GA_*/TUNE_POP_* constants, and ComputeTuneTrialBudget.
AND m_evalMode/m_evalEraBudget, because nothing set them any more - 28
read sites all permanently inert. That is not a tidy-up: the `if
(!m_evalMode)` guard on UpdateClassPriors is exactly what silently
disabled the imbalance correction for entire runs two commits ago. Dead
machinery that still reads like live machinery is this codebase's most
expensive recurring bug, and leaving 28 more instances of it would have
been indefensible.
The panel's tuning-progress state goes too - tuning no longer takes long
enough to need one.
Both builds compile 0 errors / 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:29:31 -04:00
}
2026-08-23 11:31:04 -04:00
}
//=== PLATEAU LADDER ====================================================================
//--- Neither branch above fires in the dead zone between "new best" and "regressed
//--- by more than ETA_DECAY_REGRESSION_PCT". This is the response to sitting in it:
//--- count eras since the last new best and escalate.
if ( m_ensembleMember )
{
EnsembleStashEraStats ( dirPrecPct , chancePrecPct , oosDirCalls , tradeableOK , bothSidesLive ,
selectionScore , dOosForecast ) ;
//--- m_eraCount was already incremented at the top of this block, so the era that just
//--- finished - the one the vote buffer is stamped with - is m_eraCount - 1.
EnsembleOosPassComplete ( m_eraCount - 1 , g_eta ) ;
}
else
if ( isBetterEra )
{
//--- Moving again: retire the ladder AND the restart boost. The checkpoint just
//--- snapshotted this era regardless. The normal per-era g_eta schedule takes
//--- over.
if ( m_plateauStage > 0 )
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
Print ( ID + " : new best selection score " + DeployScoreText ( m_bestSelectionScore ) +
2026-08-23 11:31:04 -04:00
" % - plateau escape worked, clearing plateau stage " + IntegerToString ( m_plateauStage ) ) ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
m_erasSinceBest = 0 ;
2026-08-23 11:31:04 -04:00
m_plateauStage = 0 ;
m_restartBoostErasLeft = 0 ;
//--- Patience is about CONSECUTIVE regressions - an era that improves clears it, so a
//--- run that alternates improve/regress never accumulates its way into a decay.
m_consecutiveRegressions = 0 ;
}
else
{
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
m_erasSinceBest + + ;
int dueStage = m_erasSinceBest / TrainPlateauPatienceEras ( ) ;
2026-08-23 11:31:04 -04:00
if ( dueStage > m_plateauStage )
perf(autotune): replace the genetic search with a filter score - hours to seconds
MEASURED COST OF THE GA, which is what retired it. Per generation:
rung 0: 8 cand x 3 seeds x 3 eras = 72 eras
rung 1: 4 cand x 3 seeds x 8 eras = 96
rung 2: 2 cand x 3 seeds x 20 eras = 120
= 288 eras/generation x 4 generations = 1152 eras BEFORE the winner's
real training began. Against the observed era times on SP500 H1:
PAI 29.1 s/era -> 9.3 h (matches the observed 00:37 -> 09:22)
CONV 41.3 s/era -> 13.2 h
LSTM 150.4 s/era -> 48.1 h
HYBRID 154.6 s/era -> 49.5 h
Two days to tune is not a first-run experience, and it is the phase in
which the panel goes quiet, which is what made it look like a hang.
It also bought nothing. The space is 90 points (10 MA periods x 9 MA
types), so 1152 evaluations revisited each point ~13 times; and rungs of
3 and 8 eras cannot separate two MA periods at all. The 2026-08-01 run
proves it: every finalist scored 25.0-25.9% balanced accuracy - below the
33.3% one-class floor, i.e. indistinguishable noise - and the search then
"deployed the winner" of that.
THE ERROR WAS THE SCORING FUNCTION, not its constants. Using a full
training run to choose a feature's period is a wrapper method paying
wrapper prices for a decision that does not need one. The reference book
does not do this: ch. 3.3 selects inputs by measuring each candidate
indicator's CORRELATION with the target and dropping the ones with none,
with no network involved.
So: rank candidates by the MUTUAL INFORMATION between the resulting
feature vector and the triple-barrier label. MI rather than correlation
because the label is 3-class categorical and the features are not
monotonically related to it. Equal-FREQUENCY binning (rank-based),
because these features are ATR-normalised and heavy-tailed - fixed-width
bins put nearly everything in one bucket and report ~0 information for a
genuinely useful feature.
Scoring is arithmetic over the feature cache, so it costs seconds and its
cost is independent of topology: LSTM now tunes as fast as the MLP.
Coordinate sweep, not product sweep - cost is the SUM of per-parameter
candidate counts, so enabling every indicator stays affordable - with a
second pass that breaks early once nothing moves.
Sampling is IS-ONLY. Letting the OOS window influence which indicator
settings ship would mean the holdout had been used for selection and had
stopped being a holdout.
HONEST LIMIT, recorded because it is the price: MI is marginal, so a
parameter that only pays off in combination with another can be missed
(Guyon & Elisseeff 2003, filter vs wrapper). Given the wrapper it
replaces was ranking pure noise at 48 h a run, this is strictly better.
Deleted with it: GaRungEras/GaExtract/GaStore/GaMutate/GaRandomCandidate/
GaBlockCrossover/GaSortAliveByScoreDesc/GaBreedNextGeneration, 14 m_ga*
members, the GA_*/TUNE_POP_* constants, and ComputeTuneTrialBudget.
AND m_evalMode/m_evalEraBudget, because nothing set them any more - 28
read sites all permanently inert. That is not a tidy-up: the `if
(!m_evalMode)` guard on UpdateClassPriors is exactly what silently
disabled the imbalance correction for entire runs two commits ago. Dead
machinery that still reads like live machinery is this codebase's most
expensive recurring bug, and leaving 28 more instances of it would have
been indefensible.
The panel's tuning-progress state goes too - tuning no longer takes long
enough to need one.
Both builds compile 0 errors / 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:29:31 -04:00
{
2026-08-23 11:31:04 -04:00
m_plateauStage = dueStage ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
string stageNote = IntegerToString ( m_erasSinceBest ) + " eras with no new best selection score (best " +
DeployScoreText ( m_bestSelectionScore ) + " ) " ;
2026-08-23 11:31:04 -04:00
if ( m_plateauStage = = PLATEAU_STAGE_RESTART | | m_plateauStage = = PLATEAU_STAGE_ANNEAL )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
{
2026-08-23 11:31:04 -04:00
//--- BOOSTED WARM RESTART: a plateau needs a bigger step to climb out of
//--- its basin, not a smaller one - and "back to the ceiling" was a NO-OP
//--- whenever the run plateaued without ever tripping the regression decay,
//--- because g_eta was still AT the ceiling (2026-08-09 audit, F2).
double etaBefore = g_eta ;
g_eta = m_etaCeiling * PLATEAU_RESTART_BOOST ;
m_restartBoostErasLeft = TrainPlateauPatienceEras ( ) ;
//--- A restart is a new schedule: replaying the plateau's own accumulated
//--- Adam momentum at 5x the rate would retrace the same basin, harder.
Net . ResetOptimizerState ( ) ;
//--- The focal-gamma anneal that used to accompany this went with focal
//--- loss on 2026-07-31.
Print ( ID + " : PLATEAU stage " + IntegerToString ( m_plateauStage ) + " - " + stageNote +
" . Boosted warm restart: learning rate " + DoubleToString ( etaBefore , 6 ) + " -> " + DoubleToString ( g_eta , 6 ) +
" (annealing back to " + DoubleToString ( m_etaCeiling , 6 ) + " over " + IntegerToString ( TrainPlateauPatienceEras ( ) ) +
" eras), optimizer momentum reset. Best checkpoint is safe - this only changes how the NEXT eras train. " ) ;
}
else
if ( m_plateauStage > = PLATEAU_STAGE_DEPLOY )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
{
2026-08-23 11:31:04 -04:00
//--- Exhausted: both escapes were tried and neither found a better
//--- model, so this IS the best this configuration reaches. Safety: only
//--- ever auto-deploys a checkpoint that CLEARED the per-class recall
//--- floor (m_bestPassedRecall).
double zBest = 0.0 , pFam = 1.0 ;
int nTried = 0 ;
bool survivesSelection = BestCheckpointSurvivesSelection ( zBest , pFam , nTried ) ;
string selectionNote = " | best-of- " + IntegerToString ( nTried ) + " test: edge " +
DoubleToString ( m_bestDirPrecPct - m_bestChancePrecPct , 1 ) + " pp on " +
IntegerToString ( m_bestDirCalls ) + " calls = " + DoubleToString ( zBest , 2 ) +
" sigma, family-wise p= " + DoubleToString ( pFam , 4 ) +
" (need <= " + DoubleToString ( DEPLOY_FAMILY_WISE_ALPHA , 2 ) + " ) " ;
2026-08-24 21:01:08 -04:00
if ( m_bestPassedRecall & & m_haveOosCheckpoint & & survivesSelection )
2026-08-23 11:31:04 -04:00
Print ( ID + " : PLATEAU stage " + IntegerToString ( PLATEAU_STAGE_DEPLOY ) + " - " + stageNote +
" across " + IntegerToString ( PLATEAU_STAGE_DEPLOY - 1 ) + " warm restarts. Training has converged on what this "
+ " configuration can reach - deploying the best checkpoint (dir-precision "
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
+ DeployScoreText ( m_bestSelectionScore ) + " , blended " + DoubleToString ( m_bestOosForecast , 1 ) + " %). "
2026-08-23 11:31:04 -04:00
+ selectionNote + " - CLEARS. " ) ;
else
if ( m_bestPassedRecall & & m_haveOosCheckpoint )
{
//--- Passed the per-era floor but not the selection correction:
//--- this is a maximum that a pure-noise search of this length
//--- produces routinely.
perf(autotune): replace the genetic search with a filter score - hours to seconds
MEASURED COST OF THE GA, which is what retired it. Per generation:
rung 0: 8 cand x 3 seeds x 3 eras = 72 eras
rung 1: 4 cand x 3 seeds x 8 eras = 96
rung 2: 2 cand x 3 seeds x 20 eras = 120
= 288 eras/generation x 4 generations = 1152 eras BEFORE the winner's
real training began. Against the observed era times on SP500 H1:
PAI 29.1 s/era -> 9.3 h (matches the observed 00:37 -> 09:22)
CONV 41.3 s/era -> 13.2 h
LSTM 150.4 s/era -> 48.1 h
HYBRID 154.6 s/era -> 49.5 h
Two days to tune is not a first-run experience, and it is the phase in
which the panel goes quiet, which is what made it look like a hang.
It also bought nothing. The space is 90 points (10 MA periods x 9 MA
types), so 1152 evaluations revisited each point ~13 times; and rungs of
3 and 8 eras cannot separate two MA periods at all. The 2026-08-01 run
proves it: every finalist scored 25.0-25.9% balanced accuracy - below the
33.3% one-class floor, i.e. indistinguishable noise - and the search then
"deployed the winner" of that.
THE ERROR WAS THE SCORING FUNCTION, not its constants. Using a full
training run to choose a feature's period is a wrapper method paying
wrapper prices for a decision that does not need one. The reference book
does not do this: ch. 3.3 selects inputs by measuring each candidate
indicator's CORRELATION with the target and dropping the ones with none,
with no network involved.
So: rank candidates by the MUTUAL INFORMATION between the resulting
feature vector and the triple-barrier label. MI rather than correlation
because the label is 3-class categorical and the features are not
monotonically related to it. Equal-FREQUENCY binning (rank-based),
because these features are ATR-normalised and heavy-tailed - fixed-width
bins put nearly everything in one bucket and report ~0 information for a
genuinely useful feature.
Scoring is arithmetic over the feature cache, so it costs seconds and its
cost is independent of topology: LSTM now tunes as fast as the MLP.
Coordinate sweep, not product sweep - cost is the SUM of per-parameter
candidate counts, so enabling every indicator stays affordable - with a
second pass that breaks early once nothing moves.
Sampling is IS-ONLY. Letting the OOS window influence which indicator
settings ship would mean the holdout had been used for selection and had
stopped being a holdout.
HONEST LIMIT, recorded because it is the price: MI is marginal, so a
parameter that only pays off in combination with another can be missed
(Guyon & Elisseeff 2003, filter vs wrapper). Given the wrapper it
replaces was ranking pure noise at 48 h a run, this is strictly better.
Deleted with it: GaRungEras/GaExtract/GaStore/GaMutate/GaRandomCandidate/
GaBlockCrossover/GaSortAliveByScoreDesc/GaBreedNextGeneration, 14 m_ga*
members, the GA_*/TUNE_POP_* constants, and ComputeTuneTrialBudget.
AND m_evalMode/m_evalEraBudget, because nothing set them any more - 28
read sites all permanently inert. That is not a tidy-up: the `if
(!m_evalMode)` guard on UpdateClassPriors is exactly what silently
disabled the imbalance correction for entire runs two commits ago. Dead
machinery that still reads like live machinery is this codebase's most
expensive recurring bug, and leaving 28 more instances of it would have
been indefensible.
The panel's tuning-progress state goes too - tuning no longer takes long
enough to need one.
Both builds compile 0 errors / 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:29:31 -04:00
Print ( ID + " : PLATEAU stage " + IntegerToString ( PLATEAU_STAGE_DEPLOY ) + " - " + stageNote +
2026-08-23 11:31:04 -04:00
" . The best checkpoint clears the per-era deployability floor but DOES NOT clear the "
+ " null of the MAXIMUM over the eras it was chosen from " + selectionNote +
" . A best-of-N this large happens routinely when every era is a noise draw, so the "
+ " ranking carries no evidence of an edge and this model is not safe to trade. "
+ " Restarting the plateau ladder and continuing to train; the "
+ IntegerToString ( m_maxErasPerRun ) + " -era cap remains the backstop. " ) ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
m_erasSinceBest = 0 ;
2026-08-23 11:31:04 -04:00
m_plateauStage = 0 ;
}
perf(autotune): replace the genetic search with a filter score - hours to seconds
MEASURED COST OF THE GA, which is what retired it. Per generation:
rung 0: 8 cand x 3 seeds x 3 eras = 72 eras
rung 1: 4 cand x 3 seeds x 8 eras = 96
rung 2: 2 cand x 3 seeds x 20 eras = 120
= 288 eras/generation x 4 generations = 1152 eras BEFORE the winner's
real training began. Against the observed era times on SP500 H1:
PAI 29.1 s/era -> 9.3 h (matches the observed 00:37 -> 09:22)
CONV 41.3 s/era -> 13.2 h
LSTM 150.4 s/era -> 48.1 h
HYBRID 154.6 s/era -> 49.5 h
Two days to tune is not a first-run experience, and it is the phase in
which the panel goes quiet, which is what made it look like a hang.
It also bought nothing. The space is 90 points (10 MA periods x 9 MA
types), so 1152 evaluations revisited each point ~13 times; and rungs of
3 and 8 eras cannot separate two MA periods at all. The 2026-08-01 run
proves it: every finalist scored 25.0-25.9% balanced accuracy - below the
33.3% one-class floor, i.e. indistinguishable noise - and the search then
"deployed the winner" of that.
THE ERROR WAS THE SCORING FUNCTION, not its constants. Using a full
training run to choose a feature's period is a wrapper method paying
wrapper prices for a decision that does not need one. The reference book
does not do this: ch. 3.3 selects inputs by measuring each candidate
indicator's CORRELATION with the target and dropping the ones with none,
with no network involved.
So: rank candidates by the MUTUAL INFORMATION between the resulting
feature vector and the triple-barrier label. MI rather than correlation
because the label is 3-class categorical and the features are not
monotonically related to it. Equal-FREQUENCY binning (rank-based),
because these features are ATR-normalised and heavy-tailed - fixed-width
bins put nearly everything in one bucket and report ~0 information for a
genuinely useful feature.
Scoring is arithmetic over the feature cache, so it costs seconds and its
cost is independent of topology: LSTM now tunes as fast as the MLP.
Coordinate sweep, not product sweep - cost is the SUM of per-parameter
candidate counts, so enabling every indicator stays affordable - with a
second pass that breaks early once nothing moves.
Sampling is IS-ONLY. Letting the OOS window influence which indicator
settings ship would mean the holdout had been used for selection and had
stopped being a holdout.
HONEST LIMIT, recorded because it is the price: MI is marginal, so a
parameter that only pays off in combination with another can be missed
(Guyon & Elisseeff 2003, filter vs wrapper). Given the wrapper it
replaces was ranking pure noise at 48 h a run, this is strictly better.
Deleted with it: GaRungEras/GaExtract/GaStore/GaMutate/GaRandomCandidate/
GaBlockCrossover/GaSortAliveByScoreDesc/GaBreedNextGeneration, 14 m_ga*
members, the GA_*/TUNE_POP_* constants, and ComputeTuneTrialBudget.
AND m_evalMode/m_evalEraBudget, because nothing set them any more - 28
read sites all permanently inert. That is not a tidy-up: the `if
(!m_evalMode)` guard on UpdateClassPriors is exactly what silently
disabled the imbalance correction for entire runs two commits ago. Dead
machinery that still reads like live machinery is this codebase's most
expensive recurring bug, and leaving 28 more instances of it would have
been indefensible.
The panel's tuning-progress state goes too - tuning no longer takes long
enough to need one.
Both builds compile 0 errors / 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:29:31 -04:00
else
2026-08-23 11:31:04 -04:00
{
Print ( ID + " : PLATEAU stage " + IntegerToString ( PLATEAU_STAGE_DEPLOY ) + " - " + stageNote +
" , but no checkpoint has ever cleared the deployability floor (directional calls on " +
" at least a quarter as many bars as actually swing, at a precision above that base rate, with BOTH Buy and Sell "
+ " recall at or above " + DoubleToString ( DEPLOY_MIN_SIDE_RECALL_PCT , 0 ) + " %), so there is nothing safe to "
+ " deploy. Restarting the plateau ladder and continuing to train rather than deploying a "
+ " one-class model; the " + IntegerToString ( m_maxErasPerRun ) + " -era cap remains the backstop. " ) ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
m_erasSinceBest = 0 ;
2026-08-23 11:31:04 -04:00
m_plateauStage = 0 ;
}
}
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
}
2026-08-23 11:31:04 -04:00
}
//--- Restart-boost anneal (see PLATEAU_RESTART_BOOST): walk g_eta geometrically from
//--- boost x ceiling back down to the ceiling over PLATEAU_PATIENCE_ERAS eras, one
//--- step per completed era - the SGDR-style decaying half of the cycle, which is
//--- what makes the boost a bounded kick instead of a new permanent rate.
if ( m_restartBoostErasLeft > 0 )
{
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
g_eta = MathMax ( m_etaCeiling , g_eta * MathPow ( PLATEAU_RESTART_BOOST , -1.0 / TrainPlateauPatienceEras ( ) ) ) ;
m_restartBoostErasLeft - - ;
}
m_oosWindow . Add ( dOosForecast ) ;
while ( m_oosWindow . Total ( ) > STABILITY_WINDOW )
m_oosWindow . Delete ( 0 ) ;
m_oosStable = false ;
if ( m_oosWindow . Total ( ) > = STABILITY_WINDOW )
{
double oosMin = m_oosWindow . At ( 0 ) , oosMax = m_oosWindow . At ( 0 ) ;
for ( int w = 1 ; w < m_oosWindow . Total ( ) ; w + + )
{
oosMin = MathMin ( oosMin , m_oosWindow . At ( w ) ) ;
oosMax = MathMax ( oosMax , m_oosWindow . At ( w ) ) ;
}
m_oosStable = ( oosMax - oosMin ) < = STABILITY_TOLERANCE ;
}
//--- The dError<0.1 RMS-error floor is meaningful for the single-neuron regression
//--- head (m_outputNeuronsCount==1), where it's the only convergence signal
//--- available.
bool errorGateOK = ( m_outputNeuronsCount = = 3 ) ? true : ( dError < 0.1 ) ;
//--- Convergence (unlike isBetterEra's ranking) FINALIZES the model, so both
//--- directional classes must have actually been MEASURED this era.
bool directionalRecallMeasured = ( m_outputNeuronsCount ! = 3 ) | | ( buyRecallPct > = 0 & & sellRecallPct > = 0 ) ;
//--- VALIDITY of this era's model, no longer "did it hit a target accuracy". Neither
//--- is what "train to the best result" means.
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
m_objectiveMet = errorGateOK & & directionalRecallMeasured ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
}
//--- Only mark the persisted model "complete" once it actually converged this era - an
//--- interruption (stop) or an ordinary in-progress era must stay flagged incomplete so
//--- a restart resumes training instead of quietly treating a partial run as done.
//--- ...and the family-wise selection gate, for the same reason the deploy branch applies it:
//--- these two conditions MUST stay identical or the flag persisted into the .nnw disagrees
//--- with the decision to stop, and a reload would run inference on a model the ladder had
//--- refused to deploy. Cheap enough to re-evaluate per era (one normal-tail evaluation).
double zConv = 0.0 , pConv = 1.0 ;
int nConv = 0 ;
//--- ENSEMBLE: the verdict is the ensemble's, so the flag persisted into this member's
//--- .nnw has to be the ensemble's too - otherwise a reload would run one member live
//--- against three still training, which is not the model that was measured.
m_trainingComplete = m_ensembleMember
? ( g_ensDeployApproved & & m_haveOosCheckpoint & & m_checkpointEra = = g_ensBestEra )
: ( ( m_plateauStage > = PLATEAU_STAGE_DEPLOY | | m_isErrorPlateaued ) & & m_bestPassedRecall & & m_haveOosCheckpoint
& & BestCheckpointSurvivesSelection ( zConv , pConv , nConv ) ) ;
double currentIndicatorParams [ ] ;
m_indicatorTuner . Flatten ( currentIndicatorParams ) ;
if ( ! Net . Save ( m_activeFileName + " .nnw " , dError , dUndefine , dForecast , dtStudied , m_activeFileCommon , m_eraCount , m_trainingComplete , currentIndicatorParams ) )
Print ( __FUNCTION__ + " : ERROR - era-end Net.Save failed for " + m_activeFileName + " .nnw (era " + IntegerToString ( m_eraCount ) + " ). Training continues but this era's checkpoint was NOT persisted - a crash/restart now would resume from an older era. " ) ;
perf(deinit): I/O-free chart cleanup, dead-panel purge, skip clean weight saves
The 18:23 terminal close (20260825.log) killed two of six charts inside
OnDeinit: they printed "shutting down" then nothing for 5.9 s until
"Abnormal termination", stranding ~700 objects each - including the one
family no prefix sweep can reach, the control panel (CAppDialog names
its 15 objects <numeric instance id><control>, and a re-attach mints a
new id, so a killed panel is a permanent ghost; XTIUSD carried one
across sessions). The stall sat in the two file writes that preceded
all visible cleanup while the four sibling charts flooded the same
2013-era disk - the ~4x18MB-per-chart shutdown weight saves.
Three changes:
1. OnDeinit touches no file until the chart is clean. CVoteArrowStore
splits Save() into Snapshot() (the chart scan, in memory) and
WriteSnapshot() (the disk half, consuming). New order: status label,
vote-arrow snapshot, prefix sweep, panel destroy - all object ops -
then member sidecars, final sweep, timings, and only then the
visibility file, the vote-arrow write and the weight saves.
2. PurgeOrphanedPanelObjects() at OnInit: deletes numeric-prefix
CAppDialog ghosts by name (6 chrome + 9 buttons), qualifying a
prefix only when >=4 of OUR button names carry it, so a foreign
dialog sharing stock chrome names is never touched.
3. m_netDirty: set by every net mutation (both backProp sites, both
RestoreWeights sites, online learning conservatively, panel reset),
cleared only on a successful Net.Save. Shutdown AND the per-bar
autosave now skip the ~18MB write when the net is provably unchanged
- for converged ensembles that is every save - which removes the
very flood that starved the sibling charts. .stats still writes
every time (small; carries the vote record and calibration). A
skipped save leaves the .nnw header dtStudied stale, which is the
already-handled attach-after-offline-gap case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:46:51 -04:00
else
m_netDirty = false ; // disk now holds exactly these weights - see PersistWeightsOnShutdown
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
if ( ! SaveModelStats ( m_activeFileName , m_activeFileCommon ) ) // keep calibration state paired with the just-saved weights
Print ( __FUNCTION__ + " : ERROR - SaveModelStats failed for " + m_activeFileName + " (era " + IntegerToString ( m_eraCount ) + " ). Calibration/online-learning state not persisted this era. " ) ;
SaveShadowNet ( currentIndicatorParams ) ;
}
}
}
//+------------------------------------------------------------------+
//| START OF A TRAINING RUN - everything that happens once, before |
//| era 0 rather than before every era. |
//| |
//| Waits (bounded, non-blocking across calls) for the terminal to |
//| finish syncing history, sizes the study window, and arms the |
//| one-shot preparatory walks. Several of its branches DEFER: they |
//| decline this call and let the next scheduled one try again, which |
//| is why this reports whether Train() should return rather than |
//| falling through. |
//| |
//| True = this call is spent. False = the run is live, carry on. |
//+------------------------------------------------------------------+
fix(train): BeginTrainRun read Train()'s parameter from a scope it no longer had
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>
2026-08-23 14:58:35 -04:00
bool CExpertSignalAIBase : : BeginTrainRun ( STrainEra & era , const datetime startTrainBar )
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
{
if ( ! m_trainRunActive )
{
//--- Wait (briefly, bounded, non-blocking across calls) for the terminal to finish syncing
//--- this symbol/period's history from the broker before computing the training window.
if ( ! SeriesInfoInteger ( m_symbol . Name ( ) , PERIOD_CURRENT , SERIES_SYNCHRONIZED ) )
{
uint syncNowTick = GetTickCount ( ) ;
if ( m_syncWaitStartTick = = 0 )
m_syncWaitStartTick = syncNowTick ;
if ( syncNowTick - m_syncWaitStartTick < 5000 )
{
ReportTrainStall ( " waiting for history sync " ) ;
return true ; // retry on the next scheduled call instead of blocking here
}
Print ( ID + " : WARNING - history for " + m_symbol . Name ( ) + " " + EnumToString ( PERIOD_CURRENT ) + " did not finish syncing after 5s; training window may still grow as more history arrives " ) ;
}
m_syncWaitStartTick = 0 ;
//--- 3 no-op passes before the era loop ever runs for a fresh start (see m_warmupPassesRemaining's
//--- declaration comment) - each is its own separately-scheduled Train() call (this whole method
//--- just returns, deferring to the next "New Bar"/timer-driven call), giving MT5's history sync
//--- several real, wall-clock-separated chances to settle on top of the 5s soft wait just above,
//--- before training commits to a bar count and starts populating the label cache below.
if ( m_warmupPassesRemaining > 0 )
{
ReportTrainStall ( " history-settle warm-up pass " ) ;
m_warmupPassesRemaining - - ;
PrintVerbose ( ID + " : warm-up pass " + IntegerToString ( 3 - m_warmupPassesRemaining ) + " of 3 (letting history sync settle before training starts) " ) ;
return true ;
}
//--- ALL available history, floored by MinTrainYear - see TrainWindowStart().
fix(train): BeginTrainRun read Train()'s parameter from a scope it no longer had
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>
2026-08-23 14:58:35 -04:00
dtStudied = TrainWindowStart ( startTrainBar ) ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
//--- OOS-based objective + stability tracking: training only "converges" once the objective
//--- is met AND OOS accuracy has held inside a tight band for the last few eras, so a single
//--- lucky era can't get locked in as the final model.
m_oosWindow . Clear ( ) ;
m_bestOosForecast = -1 ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
m_bestSelectionScore = -1 ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
m_bestPassedRecall = false ;
m_bestBothSidesLive = false ;
m_haveOosCheckpoint = false ;
m_checkpointEra = -1 ; // the joint-checkpoint era stamp goes with the snapshot it describes
m_oosStable = false ;
m_objectiveMet = false ;
//--- Family-wise deployment gate state, reset with the checkpoint tracking it describes: N counts
//--- the eras THIS run selects a maximum over, so carrying it across runs would test the winner
//--- against a search that never happened.
m_bestDirPrecPct = -1.0 ;
m_bestChancePrecPct = -1.0 ;
m_bestDirCalls = 0 ;
m_deployCandidateEras = 0 ;
m_erasSinceCooldown = 0 ;
m_eraResumePending = false ;
//--- Plateau ladder starts fresh with this run, so it re-walks the escalation from its own
//--- starting point. (The focal-gamma anneal that used to reset here went with focal loss on
//--- 2026-07-31 - the ladder's real escape is the learning-rate warm restart.)
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
m_erasSinceBest = 0 ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
m_plateauStage = 0 ;
m_restartBoostErasLeft = 0 ;
//--- Per-RUN like the ladder above, and for the same reason: a resumed run restarts the search, so
//--- carrying a previous run's best training error would let it early-stop on the first era.
m_bestIsError = -1.0 ;
m_erasSinceBestIsError = 0 ;
m_isErrorPlateaued = false ;
//--- ENSEMBLE: the shared gate state is per-RUN for the same reason the per-member state
//--- above is - N must count the eras THIS run's maximum was taken over, so carrying it
//--- across runs would test the winner against a search that never happened.
bool ensembleRunAlreadyOpen = false ;
if ( m_ensembleMember )
for ( int mi = 0 ; mi < ArraySize ( g_warriorEnsemble ) ; mi + + )
{
CExpertSignalAIBase * mm = g_warriorEnsemble [ mi ] ;
if ( CheckPointer ( mm ) ! = POINTER_INVALID & & mm ! = GetPointer ( this ) & & mm . m_trainRunActive )
{
ensembleRunAlreadyOpen = true ;
break ;
2026-08-23 11:31:04 -04:00
}
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
}
if ( m_ensembleMember & & ! ensembleRunAlreadyOpen )
{
g_ensLastVerdictEra = -1 ;
g_ensBestScore = -1.0 ;
g_ensBestTradeable = false ;
g_ensBestTwoSided = false ;
g_ensBestPrecPct = -1.0 ;
g_ensBestChancePct = -1.0 ;
g_ensBestCalls = 0 ;
g_ensBestEra = -1 ;
2026-08-26 01:14:51 -04:00
g_ensBestCoveragePct = -1.0 ;
g_ensBestMinCoverPct = -1.0 ;
g_ensBestEdgeFloorPct = -1.0 ;
feat(vote): exit-on-reversal boolean, pin the threshold, retry the atomic rename
THE EXIT KNOB. Exit_On_Reversal_Vote (default false) replaces the deleted
Signal_ThresholdClose with one boolean: false pins the close threshold to an
arithmetically unreachable 101, true pins it to the SAME threshold the entry
uses - the seed at first, then the derived value, republished together whenever
it moves. A second threshold was always redundant; "the bot now says the other
way" is one question.
It also arms CExpertSignalCustom::m_holdToBarrier, which was DEAD CODE:
HoldToBarrier(bool) had no caller anywhere in the build, so the flag had been
permanently false and the disabled close threshold was carrying the whole
hold-to-barrier policy alone. Both halves now move together.
Default stays false because the reason is statistical: the gate certifies
P(label agrees | vote fired) against a label that runs to the barrier, so an
early close trades something never measured. Turning it on is a different
strategy, not a tightening of this one.
THE PIN. The live threshold now moves only when an era's weights become the
checkpoint, and freezes once g_ensDeployApproved. Every era still derives its own
rung - that is how the best one is found - but the rung that TRADES belongs to
the checkpoint, exactly as the weights do. Two reasons, one measured and one
structural: the per-era rung moves on 6-34% of steps (the live run flapped
SP500 15 -> 10 -> 15 within a minute of starting), and without the pin a later
era's rung could end up applied to an earlier era's deployed model. A ladder
restart releases the pin, since clearing the checkpoint clears what it pinned.
The era line now prints the rung its own numbers came from, so it stays honest
when that differs from the pinned one.
THE ATOMIC RENAME retried zero times. Six charts share the TrainPool and AltData
directories, so a publish regularly lands while a peer chart holds the
destination open and FileMove returns 5004 - 27 times in one day on the live
fleet. Nothing was lost (the temp keeps the new content, the old file stays
intact) but the row did not update until the next publish. Now four attempts at
25ms, on the FAILURE PATH ONLY - a successful rename never sleeps - and skipped
in the tester, where the contention cannot happen and Sleep would distort a pass.
A rescued retry is logged, so worsening contention is visible.
Retrain-neutral. Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:53:56 -04:00
//--- The pin belongs to the checkpoint, so clearing the checkpoint releases it. The next
//--- scoring era takes the checkpoint unconditionally and re-pins from its own measurement.
g_ensDerivedThreshold = -1.0 ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
g_ensGateTestedEra = -1 ; // no gate test belongs to a run that has not happened yet
g_ensCandidateEras = 0 ;
g_ensErasSinceBest = 0 ;
g_ensPlateauStage = 0 ;
g_ensIsPlateauAnnounced = false ;
g_ensDeployApproved = false ;
}
//--- One-time eager pre-scan for a fresh start (see m_labelCachePrebuilt's declaration
//--- comment) - kick it off and defer era 0 until it's done, so era 0 can start with a real
//--- class-balance oversampling ratio instead of the reps=1 fallback.
if ( ! m_labelCachePrebuilt )
{
ReportTrainStall ( " arming the first label-cache prebuild " ) ;
StartLabelCachePrebuild ( ) ;
return true ;
}
m_trainRunActive = true ;
}
//--- Set up and ready to run this call's chunk.
return false ;
}
//+------------------------------------------------------------------+
//| START OF ONE ERA, or resumption of one that yielded mid-chunk. |
//| |
//| Fresh era: re-measure the class priors, clear every per-era tally |
//| together, size the IS/OOS split, and reset the four passes. The |
//| resume arm restores the bar cursor exactly where the last chunk |
//| left it - the two are one decision and stay in one place. |
//| |
//| Defers on the same contract as BeginTrainRun: true = this call is |
//| spent, false = the era is ready to run passes. |
//+------------------------------------------------------------------+
bool CExpertSignalAIBase : : BeginEra ( STrainEra & era )
{
if ( ! m_eraResumePending )
{
//--- COLD-INDICATOR BACKOFF (2026-08-13). Give them a few quiet seconds instead; the stall
//--- reporter stays the loud diagnosis if it persists.
if ( m_coldSweepTick ! = 0 )
{
if ( GetTickCount ( ) - m_coldSweepTick < 5000 )
{
ReportTrainStall ( " cold-indicator backoff (all windows failed on a transient cause) " ) ;
return true ;
}
m_coldSweepTick = 0 ;
}
int barsNow = ( int ) MathMin ( Bars ( m_symbol . Name ( ) , PERIOD_CURRENT , dtStudied , TimeCurrent ( ) ) + m_historyBars , Bars ( m_symbol . Name ( ) , PERIOD_CURRENT ) ) ;
//--- PRIME, THEN SETTLE, THEN SWEEP.
if ( ! ResizeBuffers ( barsNow ) | | ! RefreshData ( ) )
{
PrintFormat ( " %s: era start ABORTED - price/indicator buffers would not prepare for %d bars "
" (priming ResizeBuffers/RefreshData failed); ending this training run, it re-arms "
" on the next scheduled call " , ID , barsNow ) ;
FinalizeTrainRun ( ) ;
return true ;
}
//--- Now wait out the depth rather than snapshotting it. Returns 0 while the count is still moving.
int settled = SettledBars ( barsNow , " training sweep " ) ;
if ( settled < = 0 )
{
ReportTrainStall ( " priming indicator history (holding the sweep until the calculated depth "
" stops changing) " ) ;
return true ;
}
//--- The floor is the one piece of policy that stays here: below TRAIN_MIN_CLAMPED_BARS a settled
//--- depth is too thin to train anything worth measuring, so the run holds and the stall reporter
//--- stays the loud diagnosis rather than producing a meaningless era.
if ( settled < barsNow )
{
if ( settled < TRAIN_MIN_CLAMPED_BARS )
{
ReportTrainStall ( StringFormat ( " indicator depth settled at %d bars, below the %d-bar floor "
" for a trainable era " , settled , TRAIN_MIN_CLAMPED_BARS ) ) ;
return true ;
}
barsNow = settled ;
//--- Re-prepare at the clamped depth, and ONLY when it actually changed: the primer above
//--- already left every buffer refreshed at the full depth, so an unconditional second pass
//--- would be a wasted CopyBuffer over every buffer, every era, on the charts that need none.
if ( ! ResizeBuffers ( barsNow ) | | ! RefreshData ( ) )
{
//--- The ONLY exit from Train() that tears down the whole run, and it used to be completely
//--- silent - a transient buffer/history hiccup ended the run, FinalizeTrainRun() pushed
//--- dtStudied to the last scanned bar, and the next era simply never started. Indistinguishable
//--- from a hang while it was quiet, so it says so (2026-08-10).
PrintFormat ( " %s: era start ABORTED - price/indicator buffers would not prepare for the "
" settled depth of %d bars (ResizeBuffers/RefreshData failed); ending this "
" training run, it re-arms on the next scheduled call " , ID , barsNow ) ;
FinalizeTrainRun ( ) ;
return true ;
}
}
era . bars = barsNow ;
diag(training): report the window an era ACTUALLY trains on
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>
2026-08-24 15:12:05 -04:00
ReportEraWindow ( barsNow ) ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
//--- Cross-asset panel is indexed against exactly this bar grid, so it is (re)built wherever
//--- the grid is - never per bar. Non-fatal on failure; see BuildCrossAssetPanel().
BuildCrossAssetPanel ( barsNow ) ;
EnsureSpreadSeries ( barsNow ) ;
era . addLoop = false ;
//--- Label/feature cache invalidation: MQL5 timeseries indices are always relative to "now"
//--- (index 0 = current bar), so every new closed candle shifts every older bar's index - a
//--- cache keyed by index would silently misalign the moment that happens.
int barsBefore = m_labelCacheBars ;
datetime anchorBefore = m_labelCacheAnchorTime ;
datetime anchorNow = m_Time . GetData ( 0 ) ;
if ( EnsureBarCachesCapacity ( era . bars ) & & m_labelCachePrebuilt )
{
//--- The failure mode: the era and the prebuild disagree about `bars`, or about which bar
//--- is index 0, and re-arm each other forever - caches wiped, relabelled, wiped again, no
//--- era ever runs.
string sizeKey = ( era . bars ! = barsBefore )
? StringFormat ( " SIZE CHANGED %d -> %d " , barsBefore , era . bars ) : " size unchanged " ;
string anchorKey = ( anchorNow ! = anchorBefore )
? StringFormat ( " ANCHOR MOVED %s -> %s " , TimeToString ( anchorBefore ) ,
TimeToString ( anchorNow ) ) : " anchor unchanged " ;
ReportTrainStall ( StringFormat ( " cache invalidated at era start - %s, %s (era sized %d bars, cache "
" held %d). An anchor that moves EVERY era with the size steady is "
" a new candle each pass or a Time buffer that is not being "
" refreshed; a size that moves is the era/prebuild disagreement. " ,
sizeKey , anchorKey , era . bars , barsBefore ) ) ;
StartLabelCachePrebuild ( ) ;
return true ;
}
//--- freeze the just-finished era's true class totals for this new era's priors (see
//--- m_prevEraTrueBuyCount's declaration comment) before resetting the live counters below - EXCEPT
//--- right after StartLabelCachePrebuild()/AdvanceLabelCachePrebuild() seeded them for era 0: the
//--- live m_trueBuyCount/Sell/Neutral tally is still all-zero at that point (nothing trained yet),
//--- so copying it here would silently stomp the real upfront tally back to an empty distribution.
if ( m_prebuildSeedPending )
m_prebuildSeedPending = false ;
else
{
m_prevEraTrueBuyCount = m_trueBuyCount ;
m_prevEraTrueSellCount = m_trueSellCount ;
m_prevEraTrueNeutralCount = m_trueNeutralCount ;
}
//--- Natural class base rates for the live logit-adjusted decision (see
//--- AdjustedSignalFromSoftmax): derived from the same just-finished-era true class totals
//--- the oversampling ratio uses, so live calibrates to exactly the distribution the model
//--- was measured against.
refactor(meta): remove meta-labeling entirely - RETRAIN-NEUTRAL
~2,300 lines. META had real, repeatedly measured ranking skill and ZERO
operating points that ever cleared break-even (0/350 H1 eras, 1/999 H4
pre-2-sigma, 0/8 pooled fitted points). The clinching arithmetic was edge x
width = 0.095 ATR/trade against spread 0.099 ATR/trade, and the
dose-response showed the high-conviction tail is temporally unstable -
the precision-vs-threshold slope flips sign between calib and test on 3 of
4 symbols, so no ex-ante threshold rule exists. It shipped default-off and
never gated a live entry. The self-measured tier weights are what actually
rank the vote, and all six H4 instruments converged on them alone.
RETRAIN-NEUTRAL, and that is the property that made this safe:
- The weights fingerprint emitted "|TGT:META2" or "|TGT:SWG1" from an
if/else. Every direction model already took the SWG1 arm, so
collapsing it to an unconditional append is byte-identical. No .nnw or
.cfg is orphaned or re-keyed.
- NetInputWidth() lost its "+ MetaDescWidth()" term. MetaDescWidth()
returned 0 for every direction model, so the input layer is unchanged.
- DbLegacyAiSlot()'s slot 5 was reachable only with all four Use_* NNs
off AND meta on - a config that never shipped. Every existing .db keeps
its filename.
Deleted outright: Signals/SignalMETA.mqh, Expert/Trading/MetaGate.mqh (the
directory is now empty), Expert/Training/{MetaCorpus,MetaCandidateStore,
MetaFamilies}.mqh, Tests/Test_MetaFamilies.mq5, Meta_Labeling_Design.md.
Unwound in place, the delicate part: Training.mqh carried four
IsMetaTarget() branches whose else-arm WRAPPED the direction body (pass 1
queueing, pass 2 backprop, pass 2.5 calibration, pass 3 OOS scoring). Each
wrapper is removed and the direction body promoted back to its original
nesting - the bodies were never re-indented when the wrappers were added,
so the promoted code is byte-identical to what ran before META existed.
Also gone: the ensemble verdict's meta-veto replay and its
approved/vetoed/unscored counters, the per-family/per-side OOS
decomposition arrays, the m_isTrainQueueCand parallel queue and its
lockstep shuffle, and the S2 era report.
Also removed: the CMetaGate abstraction and the live CheckOpenPosition
veto; m_gates plus AddFilter's non-voter routing and IsVotingSignal()
(META was the only non-voting child, so m_gates was always empty);
m_parentSignal/SetParentSignal (existed only to reach the root's gate);
SweepPrepare/SweepPrepareIndicator (only caller was the corpus sweep);
IsMetaTarget() from all four view interfaces and their adapters;
Use_MetaLabeling, EnableMETA, Meta_ExportDataset, m_trainTarget.
EvalShift is KEPT - HistoricalNetVote() uses it for the filtered overlay,
not just the corpus sweep; only its comment changed. The 2-output softmax
arm in NetForward.mqh is kept too: it costs nothing and is the reusable
binary-head path, now commented as unclaimed rather than as META's.
Compile-verified in _claude_stage: 0 errors, 0 warnings, matching the
pre-edit baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:44:52 -04:00
UpdateClassPriors ( m_prevEraTrueBuyCount , m_prevEraTrueSellCount , m_prevEraTrueNeutralCount ) ;
//--- Re-install the training-time logit offsets from the priors just measured, so this
//--- era's gradient tracks the distribution the era is scored against.
ApplyLogitAdjustment ( ) ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
m_countBuySignals = 0 ;
m_countSellSignals = 0 ;
m_countNeutralSignals = 0 ;
m_trueBuyCount = 0 ;
m_trueSellCount = 0 ;
m_trueNeutralCount = 0 ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
//--- All the OOS confusion counts at once. They are read together at era end, so they
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
//--- must be cleared together - see 7452bd1 for what a partial reset of a tally group costs.
m_oos . Reset ( ) ;
//--- Declustered tally + its replay cursors. -1 / Neutral is "nothing seen yet this era", which is
//--- what makes the first directional call of an era always survive rule 1.
m_oosNmsFired = 0 ;
m_oosNmsHits = 0 ;
m_oosNmsLastBuyIdx = -1 ;
m_oosNmsLastSellIdx = -1 ;
m_oosNmsKeptIdx = -1 ;
m_oosNmsKeptConf = 0.0 ;
m_oosNmsKeptDir = Neutral ;
ArrayInitialize ( m_oosTierFired , 0 ) ;
ArrayInitialize ( m_oosTierHits , 0 ) ;
// Nearest-to-present slice of this era's bars is held out as OOS and never backprop'd on;
// the rest (older bars) is the IS/training slice.
era . totalIter = ( int ) MathMax ( era . bars - MathMax ( m_historyBars , 0 ) , 0 ) ;
era . oosCutoff = ( int ) ( MathMax ( 0 , MathMin ( 100 , m_oosSplitPct ) ) / 100.0 * era . totalIter ) ;
era . i = ( int ) ( era . bars - MathMax ( m_historyBars , 0 ) - 1 ) ;
//--- Fresh era: reset pass 2's shuffled-backprop queue (see m_isTrainQueue's declaration
//--- comment).
ArrayResize ( m_isTrainQueue , era . totalIter * 4 ) ;
m_isTrainQueueCount = 0 ;
//--- Heartbeat baseline for this era - see the member declarations for why this exists.
m_eraStartTick = GetTickCount ( ) ;
m_passFeatUs = 0 ;
m_passNetUs = 0 ;
m_passWindowOk = 0 ;
m_passWindowFail = 0 ;
m_passHeartbeatPrints = 0 ;
m_lastHeartbeatTick = 0 ;
m_isTrainCursor = 0 ;
m_isPass2Active = false ;
m_isPass2Done = false ;
m_isCalibActive = false ;
m_isCalibDone = false ;
m_isPass3Active = false ;
//--- Fresh per-era predicted-signal cache for the end-of-era NMS sweep (see PruneDirectionalClusters).
//--- -2 = "not scored this era" so stale bars from a longer prior era can't draw phantom arrows.
if ( m_signalClusterWindow > 0 )
{
ArrayResize ( m_arrowSignalCache , era . bars ) ;
ArrayInitialize ( m_arrowSignalCache , -2.0 ) ;
}
}
else
{
//--- resuming a chunk that yielded mid-bar-loop last call - pick up exactly where it left off
era . bars = m_resumeBars ;
era . totalIter = m_resumeTotalIter ;
era . oosCutoff = m_resumeOosCutoff ;
era . addLoop = m_resumeAddLoop ;
era . i = m_resumeBarIndex ;
m_eraResumePending = false ;
}
//--- Set up and ready to run this call's chunk.
return false ;
}
//+------------------------------------------------------------------+
//| WHAT PASS 1 FOUND, said out loud. Reporting only - it decides |
//| nothing and the era proceeds identically either way. |
//| |
//| Two outcomes worth a line. A sweep that produced NO usable |
//| window at all gets a full autopsy naming the lookback slot, the |
//| guard that rejected it and the per-indicator depth, because the |
//| era is discarded and restarts and the symptom is otherwise a |
//| silent loop - the backoff this arms was dead until 2026-08-17, |
//| which is why the USDJPY/XAUUSD stall never recovered. |
//| |
//| A HEALTHY first sweep is the one moment the assembled feature |
//| vector is known readable and not yet trained on, and the first |
//| point at which the bar grid, the barrier geometry and the label |
//| lifespan are all real numbers rather than defaults - so the |
//| block-level autopsy and the detectability report belong here and |
//| nowhere else. |
//+------------------------------------------------------------------+
void CExpertSignalAIBase : : ReportPass1Outcome ( STrainEra & era )
{
//--- PASS 1 IS OVER (the yield above is the only other way out of that loop). This is the
//--- point that decides whether the era does any work at all, and until now it said nothing.
//--- add_loop is exactly "m_passWindowOk > 0".
if ( ! era . stop )
{
if ( ! era . addLoop )
{
//--- SELF-HEAL BEFORE RESTARTING. A sweep that produced no usable window at all will
//--- produce exactly the same result next time unless something changes, because every
//--- bar it touched is now answered from the feature cache.
ArrayInitialize ( m_featureCacheHasValue , false ) ;
//--- Routed through ReportTrainStall rather than printed directly: a discarded era
//--- restarts immediately, so this condition repeats as fast as pass 1 can sweep, and
//--- an unthrottled line would bury the journal.
string whyLine ;
if ( m_windowFailSlot = = -2 )
whyLine = " no window has been attempted yet this run (m_windowFailSlot unset) - the "
" failure is upstream of BuildFeatureWindow " ;
else
if ( m_windowFailSlot < 0 )
whyLine = StringFormat ( " every lookback bar was ACCEPTED and the window was still "
" short: %d of %d values. A feature block emitted fewer values "
" than m_neuronsCount promises " , m_windowFailTotal ,
( int ) m_historyBars * m_neuronsCount ) ;
else
//--- THE BLOCK, NOT JUST THE SLOT. A total failure (ok=0) is itself evidence: it
//--- means the newest anchors failed too, which no depth shortfall can cause.
2026-08-23 11:31:04 -04:00
{
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
string byBlock = m_featureFailBlock ;
if ( byBlock = = " " )
byBlock = " (no guard recorded - the rejection came from a TempData.Add failure, "
" not a data guard) " ;
whyLine = StringFormat ( " lookback slot %d of %d REJECTED the bar at series index %d "
" (window had %d of %d values). REJECTED BY: %s. Slot 0 is the "
" DEEPEST lookback of the window, so with ok=0 the newest anchors "
" failed as well - which rules out a plain history-edge read and "
" points at a buffer that is unreadable at every index. "
" Per-indicator depth:%s " ,
m_windowFailSlot , ( int ) m_historyBars , m_featureFailIdx ,
m_windowFailTotal , ( int ) m_historyBars * m_neuronsCount ,
byBlock , IndicatorDepthReport ( ) ) ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
}
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
ReportTrainStall ( StringFormat ( " pass 1 finished but NOT ONE of %d scanned bars produced a "
" usable feature window, so the era is discarded and restarts "
" from scratch (feature cache dropped so the next sweep "
" recomputes) - windows ok=%d failed=%d, BuildFeatureWindow "
" needs %d values per bar (historyBars=%d x featuresPerBar=%d) "
" over %d bars | LAST FAILURE: %s " ,
era . totalIter , m_passWindowOk , m_passWindowFail ,
( int ) m_historyBars * m_neuronsCount ,
( int ) m_historyBars , m_neuronsCount , era . bars , whyLine ) ) ;
//--- transient cause (cold indicator) -> arm the era-start backoff instead of
//--- resweeping at full speed; see the backoff block at the top of the fresh-era
//--- branch. The mechanism was right; nothing reached it. THIS BACKOFF WAS DEAD UNTIL
//--- 2026-08-17 and that is why the USDJPY/XAUUSD stall never recovered.
m_coldSweepTick = GetTickCount ( ) ;
}
else
{
//--- Healthy pass 1. FIRST HEALTHY SWEEP is the only moment the assembled feature
//--- vector is known to be readable and not yet been trained on - so it is where the
//--- block-level autopsy belongs.
ReportFeatureHealth ( era . bars ) ;
//--- Same moment, same reason: the first sweep that produced usable windows is the
fix(topology): size the network against observations, not bars
The capacity budget is stated in weights per INDEPENDENT observation
and divides by the mean label lifespan to get there. It never once
did: EstimatedInSampleBars() deflates via m_labelOverlap, but it is
only ever called from InitNeuralNetwork, where the label cache does
not exist yet (that same function sets m_labelCachePrebuilt = false
a few lines below), so MeanLifespan() returned its "nothing measured"
default of 1.0 at every call. Every fresh model was sized as though
its labels did not overlap - over-budgeting the first dense layer by
a factor of L, which is several rungs of a power-of-two ladder. The
"expect overfitting, reduce the feature set or pool instruments"
warning is the branch that should fire on H1 and structurally could
not.
Fixed at the source rather than by reordering the boot sequence (the
prebuild is chunked across Train() calls and cannot complete inside
init): MeasureSwingGeometry() walks the ZigZag ONCE at init and
answers both questions from it - the median leg gives the window,
and the leg series gives the mean label lifespan analytically.
SwingPivotDirectionLabel resolves bar i when the SECOND pivot after
it commits, so a bar d bars before pivot P waits d + (the leg
leaving P); summed over every bar of every leg that is exactly the
mean the label walk accumulates.
That also closes the coherence gap the swing target opened: the
window was measured with a private +/-12-bar fractal while the label
aimed at ZigZag(12,5,3) pivots, so it was sized against a leg
distribution the label never used. One pivot source now, the
label's.
Also:
- ResetWeights() re-derives the shape. It rebuilt from the members a
history-starved init had pinned and re-saved them - so the "let
history download, then reset from the panel" advice in both
fallback warnings did nothing at all.
- The CAPACITY line prints the measured lifespan beside the one the
topology was sized for, and warns when they differ by more than a
ladder rung. That is the check that makes the estimator falsifiable.
- Topology reads the view's symbol, not _Symbol (latent for pooling).
- Unmeasured geometry defaults to HISTORY_BARS_FALLBACK, never 1.0:
under-sizing is recoverable, over-sizing silently is not.
Compile: 0 errors, 0 warnings (stage).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:21:05 -04:00
//--- first point at which the era's bar grid and the measured label lifespan are real
//--- numbers rather than defaults.
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
ReportDetectability ( era . oosCutoff ) ;
const uint PASS1_LOUD_AFTER_MS = 10000 ;
//--- The calibration band is reported here, beside the queue count it is subtracted from, so
//--- the two are read together: a run where the band silently came out empty (see
//--- CalibBandBars) is one whose operating point is no longer being refitted at all, and the
//--- only place that is visible is next to the number it should have reduced.
string pass1Line = StringFormat ( " %s: era %d pass 1 done in %.0fs - %d of %d bars usable "
" (%d failed, normal over the oldest bars), %d queued for "
" backprop | %d bars held out to calibrate the operating "
" point (+2x%d purged around it) " , ID , ( int ) m_eraCount ,
( GetTickCount ( ) - m_eraStartTick ) / 1000.0 , m_passWindowOk ,
m_passWindowOk + m_passWindowFail , m_passWindowFail ,
m_isTrainQueueCount ,
CalibBandBars ( era . totalIter , era . oosCutoff ) , CalibPurgeBars ( ) ) ;
if ( GetTickCount ( ) - m_eraStartTick > = PASS1_LOUD_AFTER_MS )
Print ( pass1Line ) ;
else
PrintVerbose ( pass1Line ) ;
}
}
}
//+------------------------------------------------------------------+
//| THE ERA COMPLETED. Count it, age the shadow net, and decide |
//| whether the RUN ends here. |
//| |
//| Two ways it does. The plateau ladder (or the ensemble gate) says |
//| there is something worth deploying - the normal, wanted ending. |
//| Or the era cap is reached, which is not a verdict about the model |
//| at all: it asks the operator, and a "deploy anyway" is an explicit|
//| choice that the automatic ladder would have refused, so it says |
//| so plainly rather than letting the deploy read as a clean pass. |
//| |
//| Self-guarding on era.addLoop: an era that produced no usable bars |
//| is not an era and must not advance the counter, or the cap and |
//| the plateau ladder both measure work that never happened. |
//+------------------------------------------------------------------+
void CExpertSignalAIBase : : AdvanceEra ( STrainEra & era , SEraTelemetry & tel )
{
//--- era complete (ran out of bars) or a stop was requested mid-era
if ( era . addLoop )
{
m_eraCount + + ;
m_erasSinceCooldown + + ;
//--- EMA shadow-weight deployment: blend the shadow a small step (SHADOW_WEIGHT_TAU)
refactor(online-learning): OnlineLearning is a real collaborator, not a raw-include partial (S5)
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>
2026-08-23 22:59:02 -04:00
//--- toward Net's just-updated weights, every era - see COnlineLearning's class comment.
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
EnsureShadowNet ( ) ;
refactor(online-learning): OnlineLearning is a real collaborator, not a raw-include partial (S5)
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>
2026-08-23 22:59:02 -04:00
m_onlineLearning . BlendTowardNet ( ) ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
//--- Status-label progress is invisible with no chart (headless/optimization runs), and
//--- even in visual mode a long training run can otherwise look "stuck" for a long time
//--- with no Journal output at all - log progress at most every ~5s (real wall-clock, not
//--- simulated time) so an operator can tell it's actively working, not hung.
uint nowTick = GetTickCount ( ) ;
tel . shouldLog = ( nowTick - m_lastProgressLogTick > = 5000 ) ;
if ( tel . shouldLog )
m_lastProgressLogTick = nowTick ;
//--- Era cap. There used to be a second, much smaller cap here for throwaway auto-tune
//--- candidates; the filter tuner does not train candidates at all, so only the real one remains.
int effectiveEraCap = m_maxErasPerRun ;
//--- PLATEAU LADDER, terminal stage: training stopped improving and both escape attempts
//--- (two learning-rate warm restarts) failed to find anything better - see the ladder in
//--- the era-end block below, which is what raised m_plateauStage this far and already
//--- logged why.
bool deployNow = m_ensembleMember
? ( g_ensDeployApproved & & m_haveOosCheckpoint )
2026-08-24 21:01:08 -04:00
: ( ( m_plateauStage > = PLATEAU_STAGE_DEPLOY | | m_isErrorPlateaued ) & & m_bestPassedRecall & & m_haveOosCheckpoint ) ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
if ( deployNow )
{
era . stop = true ;
m_trainingComplete = true ;
}
else if ( effectiveEraCap > 0 & & m_erasSinceCooldown > = effectiveEraCap )
{
//--- Era cap reached without converging: ask the operator whether to keep training or
//--- deploy the best checkpoint and stop (see PromptContinuePastEraCap / m_maxErasPerRun).
if ( PromptContinuePastEraCap ( dOosForecast ) )
{
m_erasSinceCooldown = 0 ; // keep training - reset the cap window
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
Print ( ID + " : hit the " + IntegerToString ( m_maxErasPerRun ) + " -era cap (best score " + DeployScoreText ( m_bestSelectionScore ) + " , blended OOS " + DoubleToString ( dOosForecast , 1 ) + " %) - CONTINUING training by operator choice. " ) ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
}
else
{
//--- stop: end THIS era loop now; FinalizeTrainRun (reached via the stop path below,
//--- because stop==true) deploys the best checkpoint. See OnlineLearnStep()'s gate.
era . stop = true ;
//--- Operator DELIBERATELY chose to deploy this best checkpoint as the final model.
//--- Note the m_trainingComplete=(m_objectiveMet&&m_oosStable) line below is inside
//--- if(!stop), so it can't clobber this back to false on this path.
m_trainingComplete = true ;
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
Print ( ID + " : hit the " + IntegerToString ( m_maxErasPerRun ) + " -era cap before the plateau ladder finished (best score " + DeployScoreText ( m_bestSelectionScore ) + " , blended OOS " + DoubleToString ( dOosForecast , 1 ) + " %) - operator chose to DEPLOY the best checkpoint as final (marked complete; reloads will run inference, not retrain). Reaching this cap now means the run was still finding new bests, or never cleared the deployability floor - raise the era cap for the former. " ) ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
//--- NOT blocked - this branch is an explicit operator decision and stays one. But the
//--- automatic ladder would refuse this model, so say so plainly rather than letting the
//--- deploy read as a clean pass. See DEPLOY_FAMILY_WISE_ALPHA.
ReportSelectionGateVerdict ( " era-cap deploy " ) ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
}
}
2026-08-23 11:31:04 -04:00
}
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
}
//+------------------------------------------------------------------+
void CExpertSignalAIBase : : Train ( datetime StartTrainBar = 0 )
{
//--- THIS CALL'S WORKING STATE. One object rather than eight locals threaded through four
//--- passes - see STrainEra for why the passes could not be separated while it was eight.
STrainEra era ;
//--- Max wall-clock work per call before yielding - see m_trainRunActive's declaration comment
perf(train): DLL-side mini-batch apply + 300ms slice - the era bottleneck
"Hundreds of times slower than a regular EA" decomposed into two
multiplied factors, both measured:
1. THE OPTIMIZER STEP RAN IN INTERPRETED MQL5. The CPU tier shipped
the F4 accumulate exports with deliberately no matching apply
(WarriorCPU.h said so), so on the DLL backend - this box - every
TRAIN_BATCH_SIZE=8 batch fell to the host loop in ApplyAccumToBlock:
a per-weight MQL5 pass through CBufferDouble.At()/Update() plus four
full weight-matrix BufferRead/Write round trips. The 2026-07-26
profile had already shown the per-sample Adam step at 81% of ALL
runtime (feedForward: 8%; feature building: 0.35%) - sqrt+divide
per weight vs one multiply-add; moving it into MQL5 made it worse.
New CPU_ApplyAccumAdam / CPU_ApplyAccumMomentum: one element-wise
ParallelFor takes the batch-mean step and zeroes the accumulator
DLL-side, generic over any flat block (dense/conv/LSTM/batch-norm -
all apply paths funnel through ApplyAccumToBlock, which now tries
the DLL first, with the same one-warning failure latch as the
OpenCL fast path). Math is the shipped step to the last clamp:
sqrt-stored v, ClampDelta, AdamW decay, ClampWeight.
batch_accum_check extended (check 6) and ALL PASS: apply == host
reference at B=8/B=4, accumulator zeroed, and B=1 accumulate+apply
== the unbatched Adam kernel BIT-EXACTLY (kernel-vs-kernel, no
transcription). DLL rebuilt with the shipped /fp:fast recipe.
2. A 24% DUTY CYCLE. Train sliced 120ms per 500ms timer period
(30ms/member x4), leaving the chart thread idle 76% of the time.
Now 300ms total (75ms/member): ~60% duty, ~2.5x, click latency
bounded at ~300ms while training runs - between the fully-reactive
120 and the documented "sticky drag" 480.
DEPLOYMENT COUPLING: the new .ex5 #imports the new exports, so it will
NOT LOAD against the old WarriorCPU.dll ("cannot find function"). Copy
DirectML\WarriorCPU.dll into MQL5\Libraries (terminal closed) in the
same step as deploying the new .ex5.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 19:32:15 -04:00
//--- for why chunking exists at all.
//--- ENSEMBLE: the members share the one chart thread and their chunks queue back-to-back within
//--- one timer event, so the TOTAL across members - not the per-member share - is the worst-case
//--- latency between a panel click and a free thread. 480ms total (4 x 120) was the documented
//--- "drags stickily, buttons miss clicks" regime (2026-08-15); 120ms total was fully reactive
//--- but left the chart thread idle 76% of every 500ms timer period - a 4x dilution on top of
//--- everything else, on a box where training is the bottleneck. 300ms total (2026-08-25,
//--- operator prioritised training throughput over UI smoothness) sits between the two: ~60%
//--- duty cycle, ~2.5x the old throughput, click latency bounded at ~300ms while training runs.
era . budgetMs = m_ensembleMember ? ( uint ) ( 300 / MathMax ( EnsembleActiveTrainers ( ) , 1 ) ) : 300 ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
if ( TrainCallPreempted ( era ) )
return ;
fix(train): BeginTrainRun read Train()'s parameter from a scope it no longer had
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>
2026-08-23 14:58:35 -04:00
if ( BeginTrainRun ( era , StartTrainBar ) )
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
return ;
if ( BeginEra ( era ) )
return ;
// Restore this model's own learning-rate trajectory into the shared global right before this
// chunk's backProp() calls touch it - see m_modelEta's declaration comment.
g_eta = m_modelEta ;
era . chunkStartTick = GetTickCount ( ) ;
// Iterate over the bars - skipped entirely when resuming straight into pass 2, OR when resuming
// into a still-unfinished pass 3 (see m_isPass2Done's declaration comment for why checking
// m_isPass2Active alone isn't enough to detect the latter case): pass 1 already fully completed
// in an earlier call either way.
fix(training): a yielded pass is not a finished pass - Train() must return
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>
2026-08-24 15:23:20 -04:00
//--- EVERY PASS BELOW CAN YIELD MID-CHUNK, and a yield means THIS CALL is spent, not that the pass
//--- finished. Each one signals it by going through StashEraResume - the single writer of
//--- m_eraResumePending - and then returning from its own method. Those returns used to be returns
//--- from Train() itself; when the four passes were extracted into methods (08c2cec) they became
//--- returns from a void helper, and this function carried on regardless: it reported pass 1 as
//--- "done" after one 120ms budget, trained pass 2 on the sliver pass 1 had queued so far, scored
//--- an OOS slice of it and let AdvanceEra count an era. Measured 2026-08-24 on SP500 H4: ~1,400
//--- "eras" in ten minutes, each one a ~1,200-bar chunk of a 16,264-bar window, and the oldest 90%
//--- of the history never reached at all.
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
if ( ! m_isPass2Active & & ! m_isPass2Done )
{
RunPass1 ( era ) ;
fix(training): a yielded pass is not a finished pass - Train() must return
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>
2026-08-24 15:23:20 -04:00
//--- Before ReportPass1Outcome, deliberately: that report decides whether the era did any work
//--- at all, and a yielded pass 1 has no answer to give yet.
if ( m_eraResumePending )
return ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
ReportPass1Outcome ( era ) ;
}
//--- Pass 2: replay the bars pass 1 queued into m_isTrainQueue for backProp, in a freshly
//--- shuffled order - see m_isTrainQueue's declaration comment for the full rationale.
RunPass2 ( era ) ;
fix(training): a yielded pass is not a finished pass - Train() must return
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>
2026-08-24 15:23:20 -04:00
if ( m_eraResumePending )
return ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
//--- Pass 2.5: the CALIBRATION walk.
RunCalibrationPass ( era ) ;
fix(training): a yielded pass is not a finished pass - Train() must return
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>
2026-08-24 15:23:20 -04:00
if ( m_eraResumePending )
return ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
//--- Pass 3: OOS scoring, chronological, AFTER pass 2 has actually trained on this era's IS data -
//--- see m_isPass3Active's declaration comment for why this can no longer happen inline during
//--- pass 1's scan.
RunOosPass ( era ) ;
fix(training): a yielded pass is not a finished pass - Train() must return
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>
2026-08-24 15:23:20 -04:00
if ( m_eraResumePending )
return ;
refactor(train): Train() is the era lifecycle again, not the whole of it
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>
2026-08-23 14:50:59 -04:00
//--- What this era has to report, filled in below and rendered by ReportEraProgress. Every
//--- field starts at -1 = not measured, which is what era 0 and any stopped era report.
SEraTelemetry tel ;
AdvanceEra ( era , tel ) ;
CompleteEra ( era , tel ) ;
2026-08-23 11:31:04 -04:00
ReportEraProgress ( tel ) ;
//--- Genuine convergence THIS era (not a stale m_trainingComplete carried over from a
//--- previous run) - (re)start the evaluation-only continual-learning OOS walk.
if ( ! era . stop & & m_trainingComplete )
{
Print ( ID + " : training CONVERGED at era " + IntegerToString ( m_eraCount ) + " - this is the best this configuration reached: dir-precision " +
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
DeployScoreText ( m_bestSelectionScore ) + " , blended OOS " + DoubleToString ( dOosForecast , 1 ) + " %, IS error " + DoubleToString ( dError , 2 ) +
" . No new best for " + IntegerToString ( m_erasSinceBest ) + " eras across " +
2026-08-23 11:31:04 -04:00
IntegerToString ( PLATEAU_STAGE_DEPLOY - 1 ) + " learning-rate warm restarts. " +
" Weights saved, switching to live inference. " ) ;
StartOosContinualSimulation ( era . bars , era . oosCutoff ) ;
}
if ( era . stop | | m_trainingComplete )
FinalizeTrainRun ( ) ;
//--- Deliberately AFTER FinalizeTrainRun(): that call restores the DEPLOYED checkpoint's weights
//--- (which may differ from the last era's, if the plateau ladder's best era wasn't the last one
//--- run), and this backfill must score with exactly what is about to trade live.
if ( ! era . stop & & m_trainingComplete )
StartPatternDatabaseBackfill ( era . bars , era . totalIter , era . oosCutoff ) ;
//--- else: this era is done but the run continues - the next Train() call (re-triggered via
//--- ScheduleTrainingIfNeeded()'s custom event, same mechanism as always) starts the next era
//--- fresh, since m_eraResumePending is false while m_trainRunActive stays true
//--- Save this model's own learning-rate trajectory back out of the shared global before
//--- returning - see m_modelEta's declaration comment. Covers every path that reaches here
//--- (natural era completion, whether or not the run itself just finalized).
m_modelEta = g_eta ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
}
//+------------------------------------------------------------------+
//| Ends the current Train() run: restores the best-scoring era's |
//| checkpointed weights (if any beat the era the loop happened to |
//| end on), persists final state, and clears the resumable-run |
2026-08-22 00:30:14 -04:00
//| flags. |
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
//+------------------------------------------------------------------+
bool CExpertSignalAIBase : : PromptContinuePastEraCap ( double bestOos )
{
//--- No GUI in the Strategy Tester/optimizer - MessageBox() is unavailable there and would just
//--- stall a headless run, so deploy the best checkpoint found so far and stop (the safe default).
if ( MQLInfoInteger ( MQL_TESTER ) | | MQLInfoInteger ( MQL_OPTIMIZATION ) | | MQLInfoInteger ( MQL_FORWARD ) )
return false ;
2026-08-22 00:25:52 -04:00
//--- Reaching this cap is now the UNUSUAL outcome: a run normally ends itself when the plateau
//--- ladder runs out of escapes (see the PLATEAU_* constants), which is a statement about the
//--- run having stopped improving rather than about any accuracy number.
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
string neutralNote = ( m_priorNeutral > 0.0 )
? ( " inflated by the ~ " + IntegerToString ( ( int ) MathRound ( m_priorNeutral * 100.0 ) ) + " % Neutral base rate " )
: " inflated by the dominant Neutral class " ;
string reasons = " " ;
if ( ! m_bestPassedRecall )
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
reasons + = " - No era has ever cleared the deployability floor, so there is no model safe to \n " +
" auto-deploy yet (a one-sided or chance-level model must never ship) \n " ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
else
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
reasons + = " - Still improving: " + IntegerToString ( m_erasSinceBest ) + " eras since the last new best, plateau stage " +
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
IntegerToString ( m_plateauStage ) + " of " + IntegerToString ( PLATEAU_STAGE_DEPLOY ) + " (the run ends itself at stage " +
IntegerToString ( PLATEAU_STAGE_DEPLOY ) + " ) \n " ;
if ( ! m_objectiveMet )
feat(target): delete the barrier/geometry stack - the label is the verdict
Step 3 of the swing-pivot plan, whole-hog. The swing label is now the ONE
target and the era verdict is precision + recall per class against the
label's own base rate - no win rate, no break-even, no expectancy, no
geometry anywhere in training.
DELETED
- Expert/Excursion/ (4), Expert/BarrierHorizon/ (4), GeometrySweep,
FirstPassageLadder, Labeling/TripleBarrier.mqh (CLabelOverlap survives
in Labeling/LabelOverlap.mqh), 3 test EAs.
- TripleBarrierLabel + walk, fractal label, geometry derivation/scan/
adoption, exit-policy replay, excursion MI targets, the drift verdict
(DIRECTION_INTELLIGENT), the recall floor, balanced-accuracy telemetry,
the barrier defines, the .cfg geometry adopt (slots kept as zeros for
the positional layout), the derived-geometry live-order override.
- TRAINING_TARGET input/enum: direction models are always swing; META2
re-keys the meta head onto label agreement (descriptor loses its two
geometry slots).
REWORKED
- Labels.mqh (1795 -> ~370 lines): AdvanceSwingLabelState with
FINALITY-GATED CACHING - an unresolved bar (pivot pair uncommitted) is
never cached, so it can never freeze as a false Neutral; training,
calibration, OOS scoring and online learning all skip unresolved bars.
- SDeployVerdict: significance-only; SOosTally chance = larger
directional class share; pooled gate poolability = timeframe (record v2).
- Purge/embargo/declustering gaps: the measured mean label resolution
lag (LabelResolutionBars), not a barrier horizon.
- Pool purge key + backfill DB rows: marked at the bar the label
resolved on (m_labelResolveAge), not a fabricated barrier touch.
- Online learning frontier: finality, not a horizon delay.
- m_bestBalancedOos -> m_bestSelectionScore, m_erasSinceBestBalanced ->
m_erasSinceBest, ensemble vote outcome arrays -> label arrays.
STEP 4 folded in: Entry_Multiplier / SL_Mode / TP_Mode / tradingdirection
are inputs again - trade management is the tester GA's search space.
Fingerprints: every direction model re-keys (TGT:SWG1 now unconditional,
CUT token gone); META1 -> META2. Full retrain, as planned.
Compile-verified in _claude_stage: Warrior_EA + both surviving test EAs,
0 errors, 0 warnings each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 20:42:31 -04:00
reasons + = " - The latest era did not produce a valid model (per-class recall not measured) \n " ;
string balancedStr = ( m_bestSelectionScore > 0.0 )
? ( " \n Best directional precision, coverage-weighted (the metric the deployed \n checkpoint is chosen on): " + DeployScoreText ( m_bestSelectionScore ) +
fix(deploy): print the selection score in the unit it is actually in
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>
2026-08-24 17:21:57 -04:00
" \n Best blended OOS accuracy: " + DoubleToString ( bestOos , 1 ) + " % ( " + neutralNote + " ) \n " )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
: " " ;
string msg = ID + " : training reached the " + IntegerToString ( m_maxErasPerRun ) +
" -era cap before it finished on its own. \n \n " +
" Training now runs until it stops improving, then deploys its best model. Status: \n " +
reasons +
balancedStr +
" \n Continue training? \n \n " +
" Yes = keep training for another " + IntegerToString ( m_maxErasPerRun ) + " eras \n " +
" No = deploy the best checkpoint so far and stop training " ;
int res = MessageBox ( msg , " Warrior EA - training " , MB_YESNO | MB_ICONQUESTION ) ;
return ( res = = IDYES ) ;
}
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| See the declaration comment - the single deploy-persistence path. |
//+------------------------------------------------------------------+
void CExpertSignalAIBase : : PersistDeployedModel ( void )
{
if ( CheckPointer ( Net ) = = POINTER_INVALID )
return ;
double currentIndicatorParams [ ] ;
m_indicatorTuner . Flatten ( currentIndicatorParams ) ;
if ( ! Net . Save ( m_activeFileName + " .nnw " , dError , dUndefine , dForecast , dtStudied , m_activeFileCommon , m_eraCount , m_trainingComplete , currentIndicatorParams ) )
Print ( __FUNCTION__ + " : ERROR - Net.Save failed for " + m_activeFileName + " .nnw. The deployed model was NOT persisted to disk. " ) ;
perf(deinit): I/O-free chart cleanup, dead-panel purge, skip clean weight saves
The 18:23 terminal close (20260825.log) killed two of six charts inside
OnDeinit: they printed "shutting down" then nothing for 5.9 s until
"Abnormal termination", stranding ~700 objects each - including the one
family no prefix sweep can reach, the control panel (CAppDialog names
its 15 objects <numeric instance id><control>, and a re-attach mints a
new id, so a killed panel is a permanent ghost; XTIUSD carried one
across sessions). The stall sat in the two file writes that preceded
all visible cleanup while the four sibling charts flooded the same
2013-era disk - the ~4x18MB-per-chart shutdown weight saves.
Three changes:
1. OnDeinit touches no file until the chart is clean. CVoteArrowStore
splits Save() into Snapshot() (the chart scan, in memory) and
WriteSnapshot() (the disk half, consuming). New order: status label,
vote-arrow snapshot, prefix sweep, panel destroy - all object ops -
then member sidecars, final sweep, timings, and only then the
visibility file, the vote-arrow write and the weight saves.
2. PurgeOrphanedPanelObjects() at OnInit: deletes numeric-prefix
CAppDialog ghosts by name (6 chrome + 9 buttons), qualifying a
prefix only when >=4 of OUR button names carry it, so a foreign
dialog sharing stock chrome names is never touched.
3. m_netDirty: set by every net mutation (both backProp sites, both
RestoreWeights sites, online learning conservatively, panel reset),
cleared only on a successful Net.Save. Shutdown AND the per-bar
autosave now skip the ~18MB write when the net is provably unchanged
- for converged ensembles that is every save - which removes the
very flood that starved the sibling charts. .stats still writes
every time (small; carries the vote record and calibration). A
skipped save leaves the .nnw header dtStudied stale, which is the
already-handled attach-after-offline-gap case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:46:51 -04:00
else
m_netDirty = false ; // disk now holds exactly these weights - see PersistWeightsOnShutdown
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
//--- Deploy-time gate: does this model's pure-MQL5 forward pass match the backend? If so, an
//--- inference-only backtest can run DLL-free (see ValidateCpuInference / CNet::SetCpuInference).
//--- Persisted into the .stats written next. Chart-only; safe-false everywhere else.
m_mqlInferenceValidated = ValidateCpuInference ( ) ;
if ( ! SaveModelStats ( m_activeFileName , m_activeFileCommon ) ) // keep calibration state paired with the just-saved weights
Print ( __FUNCTION__ + " : ERROR - SaveModelStats failed for " + m_activeFileName + " . Calibration state not persisted. " ) ;
SaveShadowNet ( currentIndicatorParams ) ;
}
//+------------------------------------------------------------------+
void CExpertSignalAIBase : : FinalizeTrainRun ( void )
{
2026-08-22 00:25:52 -04:00
//--- A run stopped mid-pass-2.5 or mid-pass-3 never reached that pass's own unfreeze, so lift
//--- the scoring freeze here before anything else touches the net - the deployed model must
//--- adapt live (see the freeze at pass-3 start, and the identical one the calibration walk
//--- takes for the same reason).
fix: training-stability audit fixes F1/F2/F3/F5 - unbiased shuffle, real plateau escapes, fresh optimizer state on restore, pure OOS metric
Four of the six findings from research/training_pipeline_audit_2026-08-09.md
(F4 mini-batching and F6 feature re-encode deliberately deferred - see the
report's implementation-status section for why):
- F1: pass-2 Fisher-Yates (and AutoTune's MI block shuffle) used MathRand()%,
which is 15-bit - provably non-uniform on every full-history era over 32,768
queued samples. New 30-bit ShuffleRandomIndex().
- F2: plateau warm restarts were a no-op whenever eta already sat at its
ceiling (the normal state of a non-regressing plateau) - the ladder was just
a 24-era countdown. Restarts now overshoot to 5x the ceiling
(PLATEAU_RESTART_BOOST) and anneal geometrically back over the patience
window, SGDR-style; ETA_MIN widened 1e-4 -> 1e-5 so the decay schedule has
real range.
- F3: checkpoint restores put weights back but kept the rejected trajectory's
Adam moments, so the optimizer immediately pushed back toward the rolled-back
state (the restore->regress->restore oscillation). CNet::ResetOptimizerState()
zeroes moments/momentum/step counters (weights, BN statistics, gamma/beta
untouched) on every mid-run restore, every boosted restart, and the
deploy-time restore that online learning continues from.
- F5: batch-norm running statistics now freeze for the pass-3 OOS scoring walk,
so the selection metric the checkpoint ranking and deploy gate read is a pure
function of the checkpoint instead of partly measuring BN drift. Defensive
unfreeze in FinalizeTrainRun covers stop-mid-pass; live/online adaptation and
the OOS continual-learning simulation stay adaptive by design.
Compiled clean (0 errors, 0 warnings) via the staged-tree recipe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 10:54:09 -04:00
if ( CheckPointer ( Net ) ! = POINTER_INVALID )
feat: mini-batch gradient accumulation (F4), front-end-aware capacity budget (F6), split Wyckoff categoricals (N1)
Completes the 2026-08-09 training audit. FORCES A RETRAIN of every
Wyckoff-enabled config (N1 re-keys the fingerprint), and BOTH DLLs must be
redeployed alongside the .ex5 - they carry new exports.
F4 - mini-batch accumulation, TRAIN_BATCH_SIZE=32. Training was pure online
SGD (one weight update per bar), which is the mechanical source of the
era-to-era whipsaw every downstream guard was built to cope with. The O(n^2)
outer product is native - AccumulateWeightGrad / AccumulateWeightGradConv /
AccumulateBufferInto in Network.cl, WarriorCPU and WarriorDML - while the
optimizer step is host-side MQL5 shared by all tiers (ApplyAccumToBlock), so
there is one Adam/SGD implementation instead of four that can drift.
- the LSTM needs no outer-product kernel (WeightsGradient already holds the
sample's full dW) but could NOT simply be left un-zeroed between samples:
CPU_LSTMSeqBackward/DML_LSTMSeqBackward memset it on entry. Hence a
separate accumulator plus an elementwise add.
- batch-norm gamma/beta accumulate in host arrays, not new BatchOptions
slots - BN_OPT_STRIDE is baked into every persisted .nnw.
- scoped to pass 2; online learning keeps immediate updates. Every save /
checkpoint / scoring boundary flushes, scaling by the real sample count.
- degrades to per-sample updates (one log line) on a tier that cannot
accumulate, so old devices and DLL-free builds are unaffected.
- verified offline: DirectML/batch_accum_check.cpp drives the real exports
against an independent reference; at B=1 the accumulator matches the
shipped unbatched kernel's own gradient to 1.1e-16. Math only - the
in-situ check remains the per-layer dW/W report on a real era.
F6 - ComputeFirstLayerWidth budgeted against the RAW input width even where a
conv/LSTM front end had already reduced it, so an LSTM's dense stack was
charged for 1,280 inputs when it receives 64. Confirmed from the deployed
.cfg files: CONV, LSTM and HYBRID were all pinned at the 16-unit floor. Now
budgeted against the front-end output and capped at it (never fan out), with
the derivation reordered so both stages settle first.
N1 - EventCode/EventPhase/StructuralPhase are signed categoricals packing
direction and Wyckoff stage into one scalar across a sign discontinuity. Split
into direction + [0,1] magnitude, the same convention the base OHLC block uses.
Information-preserving; 13 readings now occupy 16 inputs.
Compiled clean (0 errors, 0 warnings); both DLLs rebuilt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 11:48:03 -04:00
{
fix: training-stability audit fixes F1/F2/F3/F5 - unbiased shuffle, real plateau escapes, fresh optimizer state on restore, pure OOS metric
Four of the six findings from research/training_pipeline_audit_2026-08-09.md
(F4 mini-batching and F6 feature re-encode deliberately deferred - see the
report's implementation-status section for why):
- F1: pass-2 Fisher-Yates (and AutoTune's MI block shuffle) used MathRand()%,
which is 15-bit - provably non-uniform on every full-history era over 32,768
queued samples. New 30-bit ShuffleRandomIndex().
- F2: plateau warm restarts were a no-op whenever eta already sat at its
ceiling (the normal state of a non-regressing plateau) - the ladder was just
a 24-era countdown. Restarts now overshoot to 5x the ceiling
(PLATEAU_RESTART_BOOST) and anneal geometrically back over the patience
window, SGDR-style; ETA_MIN widened 1e-4 -> 1e-5 so the decay schedule has
real range.
- F3: checkpoint restores put weights back but kept the rejected trajectory's
Adam moments, so the optimizer immediately pushed back toward the rolled-back
state (the restore->regress->restore oscillation). CNet::ResetOptimizerState()
zeroes moments/momentum/step counters (weights, BN statistics, gamma/beta
untouched) on every mid-run restore, every boosted restart, and the
deploy-time restore that online learning continues from.
- F5: batch-norm running statistics now freeze for the pass-3 OOS scoring walk,
so the selection metric the checkpoint ranking and deploy gate read is a pure
function of the checkpoint instead of partly measuring BN drift. Defensive
unfreeze in FinalizeTrainRun covers stop-mid-pass; live/online adaptation and
the OOS continual-learning simulation stay adaptive by design.
Compiled clean (0 errors, 0 warnings) via the staged-tree recipe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 10:54:09 -04:00
Net . SetBatchNormFrozen ( false ) ;
feat: mini-batch gradient accumulation (F4), front-end-aware capacity budget (F6), split Wyckoff categoricals (N1)
Completes the 2026-08-09 training audit. FORCES A RETRAIN of every
Wyckoff-enabled config (N1 re-keys the fingerprint), and BOTH DLLs must be
redeployed alongside the .ex5 - they carry new exports.
F4 - mini-batch accumulation, TRAIN_BATCH_SIZE=32. Training was pure online
SGD (one weight update per bar), which is the mechanical source of the
era-to-era whipsaw every downstream guard was built to cope with. The O(n^2)
outer product is native - AccumulateWeightGrad / AccumulateWeightGradConv /
AccumulateBufferInto in Network.cl, WarriorCPU and WarriorDML - while the
optimizer step is host-side MQL5 shared by all tiers (ApplyAccumToBlock), so
there is one Adam/SGD implementation instead of four that can drift.
- the LSTM needs no outer-product kernel (WeightsGradient already holds the
sample's full dW) but could NOT simply be left un-zeroed between samples:
CPU_LSTMSeqBackward/DML_LSTMSeqBackward memset it on entry. Hence a
separate accumulator plus an elementwise add.
- batch-norm gamma/beta accumulate in host arrays, not new BatchOptions
slots - BN_OPT_STRIDE is baked into every persisted .nnw.
- scoped to pass 2; online learning keeps immediate updates. Every save /
checkpoint / scoring boundary flushes, scaling by the real sample count.
- degrades to per-sample updates (one log line) on a tier that cannot
accumulate, so old devices and DLL-free builds are unaffected.
- verified offline: DirectML/batch_accum_check.cpp drives the real exports
against an independent reference; at B=1 the accumulator matches the
shipped unbatched kernel's own gradient to 1.1e-16. Math only - the
in-situ check remains the per-layer dW/W report on a real era.
F6 - ComputeFirstLayerWidth budgeted against the RAW input width even where a
conv/LSTM front end had already reduced it, so an LSTM's dense stack was
charged for 1,280 inputs when it receives 64. Confirmed from the deployed
.cfg files: CONV, LSTM and HYBRID were all pinned at the 16-unit floor. Now
budgeted against the front-end output and capped at it (never fan out), with
the derivation reordered so both stages settle first.
N1 - EventCode/EventPhase/StructuralPhase are signed categoricals packing
direction and Wyckoff stage into one scalar across a sign discontinuity. Split
into direction + [0,1] magnitude, the same convention the base OHLC block uses.
Information-preserving; 13 readings now occupy 16 inputs.
Compiled clean (0 errors, 0 warnings); both DLLs rebuilt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 11:48:03 -04:00
Net . FlushBatch ( ) ;
Net . SetBatchSize ( 1 ) ;
}
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
//--- deploy the most stable/best-scoring era's weights rather than whatever the run happened to
2026-08-22 00:25:52 -04:00
//--- end on (which may reflect drift after the objective was first hit, or an aborted run).
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
if ( m_haveOosCheckpoint )
{
if ( Net . RestoreWeights ( ) )
{
perf(deinit): I/O-free chart cleanup, dead-panel purge, skip clean weight saves
The 18:23 terminal close (20260825.log) killed two of six charts inside
OnDeinit: they printed "shutting down" then nothing for 5.9 s until
"Abnormal termination", stranding ~700 objects each - including the one
family no prefix sweep can reach, the control panel (CAppDialog names
its 15 objects <numeric instance id><control>, and a re-attach mints a
new id, so a killed panel is a permanent ghost; XTIUSD carried one
across sessions). The stall sat in the two file writes that preceded
all visible cleanup while the four sibling charts flooded the same
2013-era disk - the ~4x18MB-per-chart shutdown weight saves.
Three changes:
1. OnDeinit touches no file until the chart is clean. CVoteArrowStore
splits Save() into Snapshot() (the chart scan, in memory) and
WriteSnapshot() (the disk half, consuming). New order: status label,
vote-arrow snapshot, prefix sweep, panel destroy - all object ops -
then member sidecars, final sweep, timings, and only then the
visibility file, the vote-arrow write and the weight saves.
2. PurgeOrphanedPanelObjects() at OnInit: deletes numeric-prefix
CAppDialog ghosts by name (6 chrome + 9 buttons), qualifying a
prefix only when >=4 of OUR button names carry it, so a foreign
dialog sharing stock chrome names is never touched.
3. m_netDirty: set by every net mutation (both backProp sites, both
RestoreWeights sites, online learning conservatively, panel reset),
cleared only on a successful Net.Save. Shutdown AND the per-bar
autosave now skip the ~18MB write when the net is provably unchanged
- for converged ensembles that is every save - which removes the
very flood that starved the sibling charts. .stats still writes
every time (small; carries the vote record and calibration). A
skipped save leaves the .nnw header dtStudied stale, which is the
already-handled attach-after-offline-gap case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:46:51 -04:00
//--- The in-memory swap makes the live net differ from the last save until
//--- PersistDeployedModel (below) or the shutdown save writes it back down.
m_netDirty = true ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
dOosForecast = m_bestOosForecast ;
feat: fitted directional confidence threshold - selectivity gets a mechanism
The training loss and the selection metric wanted different things and only
the second one knew it. Logit-adjusted cross-entropy has no term for "how
often should I trade", so the head calls a direction on 87-91% of bars. The
selection metric is precision x coverage credit, saturating at the coverage
floor - above the floor extra calls earn NOTHING and only precision counts.
So selection wanted few good calls, the loss produced many mediocre ones, and
all selection could do was pick the least-bad era out of what it was handed.
Nothing pushed the model toward selectivity.
This gives the decision RULE the policy instead of distorting the loss (which
is estimating class probabilities correctly, and a probability estimate should
not be bent to encode a trading policy - Elkan 2001: estimate, then choose the
operating point separately). AdjustedSignalFromSoftmax now abstains unless the
winning direction's softmax margin over its best rival clears a fitted
threshold. Margin, not the winning probability: the latter moves with overall
calibration rather than with how close the decision actually was.
Fitted on IS, applied to OOS and live. Pass 2 already forward-passes every IS
sample, so the margin histogram is harvested there for free (primary
occurrences only, so the oversampled replay queue cannot skew the operating
point); the fit runs at the end of pass 2, BEFORE pass 3, so the deploy gate
grades the thresholded model on bars the threshold never saw. Fitting on
pass 3's own predictions would be choosing the operating point on the data
being graded - the best-of-N error corrected in five other places here.
Objective: maximise IS directional precision subject to still clearing the
SAME coverage floor the deploy gate uses (base rate x 0.25, re-derived
locally so the two cannot drift apart). Swept top-down in one pass; ties go
to the LOWER threshold, since equal precision for less coverage is strictly
worse. Under DIR_CONF_MIN_FIT_CALLS (200) it runs unthresholded rather than
on a guess.
The threshold is part of the MODEL, not the run: captured with
Net.CaptureWeights(), restored with the weights at both restore sites, and
appended to the .cfg under the same length-guard convention so a deployed
model reloads at the operating point its gate actually cleared. A pre-2026-08-09
.cfg reads 0.0, which is exactly the behaviour it was trained under.
Per-era line now prints "@margin>=X.XX" next to coverage, so a coverage drop
can be attributed to the operating point rather than guessed at.
Both build variants compile 0 errors / 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 15:04:37 -04:00
//--- Deploy the checkpoint's operating point alongside its weights - the OOS coverage and
//--- precision this run is about to report were measured with this pair together.
m_dirConfThreshold = m_bestDirConfThreshold ;
fix: training-stability audit fixes F1/F2/F3/F5 - unbiased shuffle, real plateau escapes, fresh optimizer state on restore, pure OOS metric
Four of the six findings from research/training_pipeline_audit_2026-08-09.md
(F4 mini-batching and F6 feature re-encode deliberately deferred - see the
report's implementation-status section for why):
- F1: pass-2 Fisher-Yates (and AutoTune's MI block shuffle) used MathRand()%,
which is 15-bit - provably non-uniform on every full-history era over 32,768
queued samples. New 30-bit ShuffleRandomIndex().
- F2: plateau warm restarts were a no-op whenever eta already sat at its
ceiling (the normal state of a non-regressing plateau) - the ladder was just
a 24-era countdown. Restarts now overshoot to 5x the ceiling
(PLATEAU_RESTART_BOOST) and anneal geometrically back over the patience
window, SGDR-style; ETA_MIN widened 1e-4 -> 1e-5 so the decay schedule has
real range.
- F3: checkpoint restores put weights back but kept the rejected trajectory's
Adam moments, so the optimizer immediately pushed back toward the rolled-back
state (the restore->regress->restore oscillation). CNet::ResetOptimizerState()
zeroes moments/momentum/step counters (weights, BN statistics, gamma/beta
untouched) on every mid-run restore, every boosted restart, and the
deploy-time restore that online learning continues from.
- F5: batch-norm running statistics now freeze for the pass-3 OOS scoring walk,
so the selection metric the checkpoint ranking and deploy gate read is a pure
function of the checkpoint instead of partly measuring BN drift. Defensive
unfreeze in FinalizeTrainRun covers stop-mid-pass; live/online adaptation and
the OOS continual-learning simulation stay adaptive by design.
Compiled clean (0 errors, 0 warnings) via the staged-tree recipe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 10:54:09 -04:00
//--- Same F3 reset as the mid-run restore: the deployed weights are the checkpoint's, so the
//--- optimizer state that continues from here (online continual learning backprops on this
//--- same net - see OnlineLearnStep) must not be the dead run's momentum.
Net . ResetOptimizerState ( ) ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
RefreshLatestSignal ( ) ;
2026-08-22 00:25:52 -04:00
//--- NOT during shutdown. RestoreWeights() above is an in-MEMORY swap, so the best
//--- checkpoint is already the live net by this line - and OnDeinit's
//--- PersistWeightsOnShutdown() is about to write exactly those weights anyway.
fix(deinit): a full model write was running ahead of the cheap cleanup
"Abnormal termination" is back, and this time it is not the arrows. The
timing names the culprit exactly:
16:02:31.547 OnDeinit: shutting down
16:02:36.003 Abnormal termination <- 4.46 s, MetaTrader gave up
16:02:36.226 chart signals - persisted <- cleanup finished 0.2 s LATE
OnDeinit called StopTraining() BEFORE the chart cleanup. StopTraining()
finalises an in-flight run, and FinalizeTrainRun() restores the best
checkpoint and then persists it - a full ~1MB model write per signal. So
the expensive step ran ahead of the cheap bounded one, which is precisely
the inversion the shutdown ordering exists to prevent. The previous fix
put PersistWeightsOnShutdown last and missed that StopTraining smuggles a
second save in at the front.
Two changes:
Cleanup now runs FIRST, then StopTraining, then the weight save. The
visible teardown is cheap and bounded, so it always completes even when
everything after it is killed.
And the deploy-persist inside FinalizeTrainRun is suppressed during
shutdown. RestoreWeights() is an in-MEMORY swap, so the best checkpoint
is already the live net by that line, and PersistWeightsOnShutdown writes
exactly those weights moments later. The old path wrote the same model
twice per signal - eight full writes across four charts - for no benefit.
A user-pressed Stop still persists immediately, because nothing else
would.
Compiles 0 errors / 0 warnings. Build tag deinit-order-v2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 16:06:40 -04:00
if ( ! m_shutdownInProgress )
PersistDeployedModel ( ) ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
}
}
//--- Clean up any legacy on-disk checkpoint from an older (file-based) build so it can't linger.
int checkpointFlags = m_activeFileCommon ? FILE_COMMON : 0 ;
if ( FileIsExist ( m_activeFileName + " _ckpt.tmp " , checkpointFlags ) )
FileDelete ( m_activeFileName + " _ckpt.tmp " , checkpointFlags ) ;
perf(autotune): replace the genetic search with a filter score - hours to seconds
MEASURED COST OF THE GA, which is what retired it. Per generation:
rung 0: 8 cand x 3 seeds x 3 eras = 72 eras
rung 1: 4 cand x 3 seeds x 8 eras = 96
rung 2: 2 cand x 3 seeds x 20 eras = 120
= 288 eras/generation x 4 generations = 1152 eras BEFORE the winner's
real training began. Against the observed era times on SP500 H1:
PAI 29.1 s/era -> 9.3 h (matches the observed 00:37 -> 09:22)
CONV 41.3 s/era -> 13.2 h
LSTM 150.4 s/era -> 48.1 h
HYBRID 154.6 s/era -> 49.5 h
Two days to tune is not a first-run experience, and it is the phase in
which the panel goes quiet, which is what made it look like a hang.
It also bought nothing. The space is 90 points (10 MA periods x 9 MA
types), so 1152 evaluations revisited each point ~13 times; and rungs of
3 and 8 eras cannot separate two MA periods at all. The 2026-08-01 run
proves it: every finalist scored 25.0-25.9% balanced accuracy - below the
33.3% one-class floor, i.e. indistinguishable noise - and the search then
"deployed the winner" of that.
THE ERROR WAS THE SCORING FUNCTION, not its constants. Using a full
training run to choose a feature's period is a wrapper method paying
wrapper prices for a decision that does not need one. The reference book
does not do this: ch. 3.3 selects inputs by measuring each candidate
indicator's CORRELATION with the target and dropping the ones with none,
with no network involved.
So: rank candidates by the MUTUAL INFORMATION between the resulting
feature vector and the triple-barrier label. MI rather than correlation
because the label is 3-class categorical and the features are not
monotonically related to it. Equal-FREQUENCY binning (rank-based),
because these features are ATR-normalised and heavy-tailed - fixed-width
bins put nearly everything in one bucket and report ~0 information for a
genuinely useful feature.
Scoring is arithmetic over the feature cache, so it costs seconds and its
cost is independent of topology: LSTM now tunes as fast as the MLP.
Coordinate sweep, not product sweep - cost is the SUM of per-parameter
candidate counts, so enabling every indicator stays affordable - with a
second pass that breaks early once nothing moves.
Sampling is IS-ONLY. Letting the OOS window influence which indicator
settings ship would mean the holdout had been used for selection and had
stopped being a holdout.
HONEST LIMIT, recorded because it is the price: MI is marginal, so a
parameter that only pays off in combination with another can be missed
(Guyon & Elisseeff 2003, filter vs wrapper). Given the wrapper it
replaces was ranking pure noise at 48 h a run, this is strictly better.
Deleted with it: GaRungEras/GaExtract/GaStore/GaMutate/GaRandomCandidate/
GaBlockCrossover/GaSortAliveByScoreDesc/GaBreedNextGeneration, 14 m_ga*
members, the GA_*/TUNE_POP_* constants, and ComputeTuneTrialBudget.
AND m_evalMode/m_evalEraBudget, because nothing set them any more - 28
read sites all permanently inert. That is not a tidy-up: the `if
(!m_evalMode)` guard on UpdateClassPriors is exactly what silently
disabled the imbalance correction for entire runs two commits ago. Dead
machinery that still reads like live machinery is this codebase's most
expensive recurring bug, and leaving 28 more instances of it would have
been indefensible.
The panel's tuning-progress state goes too - tuning no longer takes long
enough to need one.
Both builds compile 0 errors / 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:29:31 -04:00
//--- (dtStudied used to be held back while scoring a throwaway candidate - that marker belongs
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
//--- to the DEPLOYED model's "studied up to" state; a candidate eval must leave it untouched. The
//--- checkpoint block above is already inert in eval mode (m_haveOosCheckpoint stays false).
perf(autotune): replace the genetic search with a filter score - hours to seconds
MEASURED COST OF THE GA, which is what retired it. Per generation:
rung 0: 8 cand x 3 seeds x 3 eras = 72 eras
rung 1: 4 cand x 3 seeds x 8 eras = 96
rung 2: 2 cand x 3 seeds x 20 eras = 120
= 288 eras/generation x 4 generations = 1152 eras BEFORE the winner's
real training began. Against the observed era times on SP500 H1:
PAI 29.1 s/era -> 9.3 h (matches the observed 00:37 -> 09:22)
CONV 41.3 s/era -> 13.2 h
LSTM 150.4 s/era -> 48.1 h
HYBRID 154.6 s/era -> 49.5 h
Two days to tune is not a first-run experience, and it is the phase in
which the panel goes quiet, which is what made it look like a hang.
It also bought nothing. The space is 90 points (10 MA periods x 9 MA
types), so 1152 evaluations revisited each point ~13 times; and rungs of
3 and 8 eras cannot separate two MA periods at all. The 2026-08-01 run
proves it: every finalist scored 25.0-25.9% balanced accuracy - below the
33.3% one-class floor, i.e. indistinguishable noise - and the search then
"deployed the winner" of that.
THE ERROR WAS THE SCORING FUNCTION, not its constants. Using a full
training run to choose a feature's period is a wrapper method paying
wrapper prices for a decision that does not need one. The reference book
does not do this: ch. 3.3 selects inputs by measuring each candidate
indicator's CORRELATION with the target and dropping the ones with none,
with no network involved.
So: rank candidates by the MUTUAL INFORMATION between the resulting
feature vector and the triple-barrier label. MI rather than correlation
because the label is 3-class categorical and the features are not
monotonically related to it. Equal-FREQUENCY binning (rank-based),
because these features are ATR-normalised and heavy-tailed - fixed-width
bins put nearly everything in one bucket and report ~0 information for a
genuinely useful feature.
Scoring is arithmetic over the feature cache, so it costs seconds and its
cost is independent of topology: LSTM now tunes as fast as the MLP.
Coordinate sweep, not product sweep - cost is the SUM of per-parameter
candidate counts, so enabling every indicator stays affordable - with a
second pass that breaks early once nothing moves.
Sampling is IS-ONLY. Letting the OOS window influence which indicator
settings ship would mean the holdout had been used for selection and had
stopped being a holdout.
HONEST LIMIT, recorded because it is the price: MI is marginal, so a
parameter that only pays off in combination with another can be missed
(Guyon & Elisseeff 2003, filter vs wrapper). Given the wrapper it
replaces was ranking pure noise at 48 h a run, this is strictly better.
Deleted with it: GaRungEras/GaExtract/GaStore/GaMutate/GaRandomCandidate/
GaBlockCrossover/GaSortAliveByScoreDesc/GaBreedNextGeneration, 14 m_ga*
members, the GA_*/TUNE_POP_* constants, and ComputeTuneTrialBudget.
AND m_evalMode/m_evalEraBudget, because nothing set them any more - 28
read sites all permanently inert. That is not a tidy-up: the `if
(!m_evalMode)` guard on UpdateClassPriors is exactly what silently
disabled the imbalance correction for entire runs two commits ago. Dead
machinery that still reads like live machinery is this codebase's most
expensive recurring bug, and leaving 28 more instances of it would have
been indefensible.
The panel's tuning-progress state goes too - tuning no longer takes long
enough to need one.
Both builds compile 0 errors / 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:29:31 -04:00
if ( m_eraCount > 0 )
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
dtStudied = m_lastBarTime ;
m_trainRunActive = false ;
m_eraResumePending = false ;
m_haveOosCheckpoint = false ;
feat(ensemble): deploy gate on the COMBINED VOTE, with a joint checkpoint
The unit of evaluation in ensemble mode becomes the vote, because the
vote is what trades (user: "at the end of the day they will vote
together during live trading so that would make sense").
Four decisions move from the member to the ensemble:
* which era is "best" -> the era whose COMBINED VOTE scored best
* what is checkpointed -> a JOINT snapshot: every member's weights
at that one era
* when the run gives up -> one shared plateau ladder
* whether it may deploy -> family-wise gate on the vote
WHY THE JOINT CHECKPOINT IS THE POINT: per-member selection picks each
net's own best era, and those eras differ. The resulting quartet was
never measured together at any instant, so the vote it casts live is a
configuration no OOS number ever described. Capturing all four at the
era whose vote won makes the deployed ensemble exactly the measured one.
Correct because of the era barrier (b77e7b4): Train() runs at most one
era per call and a member that finished era N is held until every member
reaches N, so when the last member scores the vote no member's weights
have advanced past end-of-era-N. That makes the deferred simultaneous
capture a guarantee rather than a race. Each snapshot is era-STAMPED and
deploy requires every stamp to equal the winning era - otherwise a member
whose capture failed would still hold an older snapshot and the deployed
quartet would again be one nothing measured. Partial capture rolls the
era back out of "best" so the search continues instead of freezing
behind a checkpoint that does not exist.
Statistics mirror the per-member gate one for one - same coverage floor
(MIN_COVERAGE_FRACTION_OF_BASE_RATE), same always-call-one-direction
chance reference, same EDGE_MIN_SIGMAS margin, same Sidak correction over
the eras ranked (DEPLOY_FAMILY_WISE_ALPHA). Only the population differs:
the bars the VOTE fired on, at Min_Vote_Open, rather than the bars one
member called. Two-sidedness is required of the vote itself - a vote that
never goes short IS the always-long model the chance reference prices in.
Members keep their own per-era statistics and their own learning-rate
dynamics (regression restore, eta decay); those are per-net training
mechanics, not deployment decisions. The shared ladder is mirrored onto
each member so per-era log lines report the state that actually governs
them. Solo charts are untouched on every path.
Verified: full MetaEditor compile, 0 errors 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:25:25 -04:00
m_checkpointEra = -1 ; // the joint-checkpoint era stamp goes with the snapshot it describes
2026-08-22 00:25:52 -04:00
//--- Persist the arrows now drawn on the chart so a deploy/stop survives a later re-
//--- add/recompile without a retrain (durable even if the terminal never gets a clean OnDeinit).
fix(deinit): O(n^2) arrow prune blew the shutdown budget and littered 3 charts
Reported as "the perceptron correctly cleaned its chart on deinit, the
other 3 did not, abnormal termination". Measured from the 2026-08-01 log,
time from "OnDeinit: shutting down" to MetaTrader force-terminating:
PAI 3.75 s -> survived, chart cleaned
CONV 4.71 s -> Abnormal termination
LSTM 4.28 s -> Abnormal termination
HYBRID 4.16 s -> Abnormal termination
In all four the last line printed is the inference census, which is the
end of StopTraining() - so the overrun is inside ShutdownChartCleanup(),
i.e. between saving the arrows and purging them.
The cost is the prune loop at the end of SaveChartSignals():
for(int i = 0; i < prunedCount; i++)
ObjectDelete(0, SIG_ARROW_PREFIX + TimeToString(pruned[i]));
ObjectDelete is O(objects) on a crowded chart, so this is O(n^2). It was
harmless while the model called a direction on ~6% of bars. After the
triple-barrier relabel the models call on 83-94% of bars, the chart
carries many thousands of arrows, and the loop overran MetaTrader's
OnDeinit budget - so PurgeChart() never ran and the arrows stayed on
screen. The slow tidy-up starved the fast one.
The work was pure waste at that moment: ShutdownChartCleanup purges every
arrow with a single bulk ObjectsDeleteAll immediately afterwards.
Deleting them one at a time first has no effect except to prevent the
bulk delete from happening at all.
SaveChartSignals takes a pruneChartObjects flag, and the two shutdown
call sites pass false:
- ShutdownChartCleanup passes `preserveChartArrows`, which is exactly
right: prune when the arrows are STAYING (chart and sidecar must
agree), skip when they are about to be purged wholesale.
- FinalizeTrainRun passes !m_trainingStopRequested. Removing a chart
MID-ERA reaches StopTraining -> FinalizeTrainRun, which took the
expensive path a second time, even earlier, before anything had been
cleared. Same defect one call site up; it only escaped notice because
the observed removals happened to land between eras.
Normal convergence and the live per-era path are unchanged - they still
prune, which is what keeps the chart object count bounded.
This also restores the invariant the 2026-07 fix intended ("chart cleanup
runs BEFORE the heavy weight save so a stall cannot leave the chart
littered"). That fix moved cleanup ahead of the WEIGHT save, but cleanup
had since grown its own slow step ahead of its own fast one.
Both builds compile 0 errors / 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:38:36 -04:00
SaveChartSignals ( ! m_trainingStopRequested ) ;
refactor: split CExpertSignalAIBase implementation by responsibility
ExpertSignalAIBase.mqh was 8216 lines: the class declaration followed by 87
method bodies covering training, labelling, feature extraction, persistence,
chart drawing, online learning, the GA auto-tuner and inference, all in one
file. Train() alone is 1492 lines; a change to arrow drawing meant scrolling
past the era loop.
Moved the bodies into Expert\AIBase\, included at the bottom of the original
after the class declaration:
Training.mqh 1607 era loop, plateau ladder, checkpoint select, deploy
Features.mqh 1093 indicator creation + per-bar input feature vector
ChartUI.mqh 634 arrows, arrow persistence, status panel, cleanup
Persistence.mqh 492 .stats/.cfg sidecars, CPU-inference validation, copy
OnlineLearning.mqh 461 live continual learning, EMA shadow, OOS simulator
Labels.mqh 309 ZigZag pivot labels, async label-cache prebuild
AutoTune.mqh 275 genetic tuner (population, crossover, halving)
Inference.mqh 235 softmax, prior calibration, class priors
ExpertSignalAIBase.mqh 8216 -> 3131 (declaration + topology build only)
This is a pure relocation - verified mechanically, not by eye: HEAD's file
reconstructed from the eight partials plus the surviving remainder is
byte-identical to HEAD, span for span (scratchpad verify_split.py). No
declaration moved, no signature changed, no code rewritten, so behaviour is
unchanged by construction.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 00:42:45 -04:00
}
# endif // WARRIOR_AIBASE_TRAINING_MQH