Commit graph Warrior_EA/Expert/AIBase/Training.mqh
Author SHA1 Message Date
AnimateDread
15b028450b fix(vote): follow the derived rung until a checkpoint exists, pin thereafter
A LIVE DEFECT from combining today's two changes. The threshold pins ON
CHECKPOINT (ad4ae58) and the burn-in forbids checkpoints below era 20 (32eb5c5),
so nothing was published for the first 20 eras and those charts sat on the
Signal_ThresholdOpen seed of 25 - an ABSOLUTE WIN RATE under a currency that no
longer uses one. 25 is above what the vote can now reach:

    USDJPY  Filtered view: drew 0 arrow(s). Strongest vote 19.3% vs 25.0% threshold
    SP500   Filtered view: drew 268 arrow(s). Strongest vote 13.1% vs  5.0% threshold

Zero arrows AND zero trades on all three FX charts (eras 10/10/16), while the
three past era 20 published their derived rungs and ran normally.

Fix: publish the current era's derived rung while g_ensBestEra < 0. Before a
checkpoint exists there is nothing to protect, and an arbitrary seed is strictly
worse than the latest measurement. Once a checkpoint exists the pin takes over
unchanged.

HOW IT WAS FOUND: the user said the FX charts were visibly quiet while I was
reporting 17-18% coverage and had declared the quiet-chart problem fixed.
Era-verdict coverage says what the vote WOULD fire on in an OOS replay; it says
NOTHING about whether the live threshold is reachable. The log stated it
verbatim - "Strongest vote 19.3% against a 25.0% threshold" - and I had not
looked at the drawn view before claiming success. Verify a display or trading
claim on the ARROW COUNT, never on the scorer.

Compiled clean; NOT yet run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 16:36:20 -04:00
AnimateDread
32eb5c5f58 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
AnimateDread
b1c3a898aa 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
AnimateDread
ad4ae58814 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
AnimateDread
c6eb9085d5 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
AnimateDread
b9da557e4e diag(gate): report what the vote would score at every threshold rung
The gate could say "coverage too low" but never "and here is what it
would be one rung down", so the single parameter most responsible for a
refusal was the one its own output said least about. Working it out by
hand needed a model of the vote's quantisation (a weighted mean of member
tier weights, so the threshold is really a quorum) and that model could
not be checked: MT5 stores the input PER CHART in profiles\Charts\*
\chart*.chr, so an already-attached EA ignores a changed source default -
confirmed by a full close/recompile/relaunch after which the log still
read "fired at vote>=25%". There was no cheap A/B available.

Each era now reports coverage and precision at every PERCENTAGE_PRESETS
rung from 5% to 30%, measured on the same rows the verdict just scored,
marking the active rung and any rung that clears the coverage floor. It
is accumulated before the live threshold test so the sweep sees every
scored row, and gated by the same direction policy so its numbers are
comparable with what the gate certifies. Nothing reads it to decide
anything.

Motivation, measured overnight across 534 eras with zero runtime errors:
every symbol clears its precision bar and every symbol fails on coverage
(0.0-3.3% against a ~6.7-7.2% floor), while the members stay healthy
throughout at 22-27% precision against a 13-14% chance rate on 25-38% of
bars. Only the aggregation fails. SP500 was DEPLOYABLE at era 5 with 7.5%
coverage and sits at 0.7% by era 536 with precision unchanged - more
training is proven not to help, because a 25% threshold against ~30 tier
weights demands unanimity and the models diverge as they specialise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 04:17:27 -04:00
AnimateDread
1533365a85 diag(gate): the coverage refusal contradicted itself
The message I added one commit ago printed, verbatim:

  "Precision was 28.4% against a 39.1% bar, so the calls it DID make
   were NOT good enough: the vote is too selective, not too weak."

Those two clauses say opposite things. Only the "NOT" was conditional;
the diagnosis after the colon was hardcoded, so whenever precision missed
its bar the line asserted and denied the same thing in one sentence.

The two cases are opposite diagnoses and must not share a sentence:

- Precision CLEARED its bar -> the calls were good and there were too few
  of them. The vote is too selective.
- Precision MISSED its bar -> this is still not "the model is weak",
  because the exact-binomial floor is computed from the INDEPENDENT call
  count, so thin coverage inflates the very bar it is judged against.
  Reporting that as a second, separate failure sends a reader off to fix
  the model when coverage is what moved the target.

Caught by reading the diagnostic's own first live firing rather than by
review - the same way the two regressions before it were found.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 01:47:03 -04:00
AnimateDread
0f756faf2a diag(gate): name the deployability condition that actually failed
The stage-3 refusal read "no era's combined vote ever cleared the
deployability floor" and then listed all three conditions in one
parenthesis - fires on a quarter of the base rate, both directions alive,
precision above the reference by 2 sigma - without saying which one fired.
The three have nothing in common as fixes, so the list was not a
diagnosis. It cost real time to work out by hand tonight, and the answer
was coverage every time.

Keeps the best era's coverage, its floor and its precision bar alongside
the win rate already retained, and names the failing condition. The
coverage branch also states whether the calls it DID make cleared the
precision bar, because "too selective" and "too weak" are opposite
problems that the old message could not distinguish, and points at
Signal_ThresholdOpen being a quorum rather than at the models.

Cleared at both existing reset sites so a refusal can never describe an
era that is no longer the best.

Context: SP500 reached stage 3 at era 67 and was refused on coverage
0.5% against a 6.9% floor while its precision was 62.5% against a 61.4%
bar - i.e. the vote was too selective, not too weak. Same doctrine as
CTrainPoolReader::Announce's reject list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 01:14:51 -04:00
AnimateDread
6adb710a79 fix(binomial): correct tail calculation in BinomialUpperTailP and add tests for accuracy 2026-08-25 23:37:22 -04:00
AnimateDread
bd46374954 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
AnimateDread
781ae3a702 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
AnimateDread
484a9d8b0f 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
AnimateDread
1baa13c5b4 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
AnimateDread
b5d34a82b4 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
AnimateDread
12d9871650 chore(comments): drop two references to the deleted drift verdict
DIRECTION_INTELLIGENT and the drift verdict it fed were removed in
the step-3 demolition (8f21646); WarriorDirectionAllows() now
resolves purely from tradingdirection (LONG_ONLY/SHORT_ONLY/BOTH).
Two comments in the OOS-verdict certification path and the filtered-
overlay reconstruction still described the deleted mechanism -
found while auditing both paths for correctness. No behavior change.

Compile: 0 errors, 0 warnings (stage).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 22:11:50 -04:00
AnimateDread
4ad079aaed 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
AnimateDread
ec1692f348 feat(mi): the screen is an alarm, not a gate
The MI suite kept its one irreplaceable job - the label-alignment
lookahead scan, whose margin is priced by the headline permutation
null and whose validity is proven by the positive control. Everything
that judged or vetoed on top of that measurement is gone:

- m_dirEvidence deploy veto deleted from all four deploy sites. The
  policy is that screens are priors, not gates; the family-wise
  selection test on held-out precision is the deploy protection, and
  a marginal per-bar MI test cannot veto a model that reads the
  window jointly (the report itself said so on every print).
- Per-column CFeatureSelector deleted; BlockPermuteLabels (the null
  engine ScoreMiSample depends on, ragged-tail fix intact) moves to
  AutoTune.mqh as a free function.
- Feature-lag profile deleted, with its MI_LAG_* constants and
  BuildMiSample's featureBarOffset; MiShiftPad no longer pads by
  m_historyBars.

Compile: 0 errors, 0 warnings (stage).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 21:01:08 -04:00
AnimateDread
8f2164698b 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
AnimateDread
0cd20a5749 diag(geometry): the sweep must not present "least negative" as a recommendation
First 35 eras across both charts, this run:

  shipped 1.21/2.43 (SP500) and 1.26/2.52 (USDJPY): mean -0.0525R,
      positive in 6 of 35 eras
  best plateau after the neighbourhood guard: mean +0.0292R,
      positive in only 17 of 35
  most-recommended pair: 20.00/0.50, seven times - a ~40:1 lottery that is
      simply the least negative cell in an all-negative grid

The recommendation jumps between opposite corners of the ladder between
consecutive eras, which is a grid fitting noise rather than a geometry worth
adopting. Two changes so the line cannot be misread:

- GEOSWEEP_MAX_TIMEOUT_SHARE (0.70): a cell where most trades never touch
  EITHER barrier is not a geometry being tested, it is the horizon close being
  measured. 20.00/20.00 timed out on 100% of trades and was still selected.
  Excluded from SELECTION only; the cell stays filled and readable.
- When the winning plateau is <= 0 the line now says so in those words:
  "NOTHING ON THE LADDER PAYS ... the pair below is the LEAST NEGATIVE cell,
  not an edge."

Still measurement only - nothing reads the recommendation and no geometry moves.

Compile-verified in the staging copy: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 17:59:49 -04:00
AnimateDread
1882f87451 feat(geometry): price every stop/target pair on the trades the model actually called
Step 1 of decoupling SL/TP from training. The geometry is currently chosen
BEFORE the model exists - excursions -> stop at a quantile -> target at the
policy minimum ratio -> labels -> the net learns those labels - so it has never
been asked which pair maximises expectancy GIVEN WHAT THE MODEL CAN PREDICT.
The scan meant to answer that reports "0 ELIGIBLE candidates" on this config
(every rung disqualified by the close-all clamp), so nothing has ever compared
the shipped pair to an alternative.

This needs no retrain and no backtest. CFirstPassageLadder already stores the
first-touch AGE of every rung on both sides and OutcomeR() resolves ANY pair
exactly with the spread charged the way the fill charges it - so 14x14 pairs
over one era's OOS calls is a few thousand array reads.

- Expert/Training/GeometrySweep.mqh: CGeometrySweep accumulates (n, sumR,
  sumR^2, timeouts) per rung pair from the model's own directional OOS calls.
  Reads no chart, holds no net, opens no file - exercisable against a
  hand-built ladder, same doctrine as SDeployVerdict.
- Best() ranks on the 3x3 NEIGHBOURHOOD mean, not the cell itself. A 14x14 grid
  read at its single highest cell is a best-of-196 maximum, biased upward by
  construction - the same selection problem the deploy gate corrects across
  eras. A pair whose neighbours also pay is a plateau; a lone spike is a lucky
  run of trades and does not survive the next window. GEOSWEEP_MIN_TRADES (30)
  keeps thin cells out of the selection entirely.
- Wired into pass 3 where the call and the bar index are both in hand, reset per
  era, reported at pass-3 completion beside ReportCandidateGeometry. ONE line,
  and only when the recommendation CHANGES - it prints the shipped pair's
  expectancy and the best pair's on the SAME trades, so "better" is a difference
  rather than two numbers from two populations.

Measurement only: nothing reads the recommendation yet and no geometry moves.

Compile-verified in the staging copy: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 17:35:12 -04:00
AnimateDread
d12b742a40 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
AnimateDread
9883b209c7 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
AnimateDread
7075747f4a 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
AnimateDread
c14ffc84e2 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
AnimateDread
cbd077c679 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
AnimateDread
9c31625aae 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
AnimateDread
906c60e227 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
AnimateDread
78a070eb5c 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
AnimateDread
3278ea4ae2 refactor(excursion): ExcursionHead is a real collaborator, not a raw-include partial (S4)
Expert/AIBase/Excursion.mqh was 13 method bodies of CExpertSignalAIBase,
#include'd after its declaration - same "not a module" problem already
fixed for ChartUI (S2) and Persistence (S3). Extracted to
Expert/Excursion/CExcursionHead behind CExcursionHeadView/
CAIBaseExcursionHeadView, same view+adapter shape.

STATEFUL, unlike Persistence (0 exclusive fields): grep-verified 26
fields (m_excNet and every accumulator/trailing-ring field) touched
nowhere else in Expert\ except Lifecycle.mqh's old ctor-init-list
defaults and destructor deletes (now moved onto CExcursionHead's own
ctor/dtor). m_geo (SGeometryScan) and m_ladder (CFirstPassageLadder)
stay on the signal - both are genuinely shared with Labels.mqh/
Training.mqh at era boundaries - and are reached only through 15 new
Exc*() view wrappers, including one consolidated
ExcGeometryScanAccumulate() call (same doctrine as Persistence's
RunCpuInferenceSelfCheck) rather than field-by-field pokes.

All 13 original public methods stay at their same declaration point as
one-line forwards to m_excursionHead. Training.mqh's 2 raw m_excUs
reads now go through the new ExcursionMicroseconds() forward. Every
method body is a pure relocation, verified statement-by-statement
against the original (git show HEAD~1:Expert/AIBase/Excursion.mqh).

Compile-verified: 0 errors, 0 warnings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 22:28:31 -04:00
AnimateDread
053d704a84 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
AnimateDread
8c64ec7018 refactor(meta): the signal tree owns its gate - no global
g_warriorMetaGate was a file-scope mutable pointer, and it did not need to
be. The root CExpertSignalCustom - the one CExpert actually calls
CheckOpenLong/Short on - now holds the gate as a member, and children reach
it through a parent back-pointer AddFilter sets on adoption.

That was the last piece of the meta veto that behaved like ambient state:

- CheckOpenPosition reads MetaGate() instead of a global.
- EnsembleEraVerdict's replay reads the same MetaGate(). It sits deep in
  the training code inside an AI filter and had no route up the tree; a
  global WAS that route. m_parentSignal is now, and a back-pointer is safe
  for the same reason the gate adapter's owner pointer is - m_filters and
  m_gates free their children, so a parent always outlives them.
- The stale-pointer hazard is gone by construction. The global had to be
  hand-cleared at every re-init because an input change re-enters OnInit in
  the same program instance and frees the old head; the root signal is
  new'd fresh each time, so nothing survives one. That reset line is
  deleted, not moved.

Note what did NOT need doing: the tree already owned the meta head itself.
AddFilter routes non-voters into m_gates, so it has been a gate child of
the root since the S3 wiring - it was only the VETO that lived outside.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 15:58:16 -04:00
AnimateDread
303c9bf412 refactor(meta): the era's candidates are an object, not eight base members
Five parallel arrays, a count and a two-array intrusive chain sat on
CExpertSignalAIBase - inherited by every direction model, filled and read
by exactly one subclass. CMetaCandidateStore takes all eight.

What that fixes beyond the clutter:

- THE CHAIN WAS LINKED BY HAND. MetaPrepareEra wrote next[id] = head[bar]
  then head[bar] = id itself, after six ArrayResize calls it also wrote out
  itself. Add() does the linking, Reset() does the sizing, and a bar off
  the grid now cannot be stored at all rather than stored unreachable.

- THE BOUNDS TEST HAD FOUR SITES AND THREE IMPLEMENTATIONS.
  MetaCandidateWon indexed side[] with no test at all and answered
  "short" for any id out of range - the same shape as the ladder's
  negative-index read (2c351a0). Side() is three-state here, IsLong() and
  SideIndex() are the safe ways to ask, and the per-side era tally in
  RunOosPass is now guarded exactly like the per-family one beside it,
  which always was.

Like the ladder and the OOS tally, none of it needs a chart, a net or a
broker: hand it bars and rows and every answer is a function of those.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 15:36:31 -04:00
AnimateDread
3d2ee517ca refactor(meta): the veto is a gate, not a virtual every signal carries
Since S3 (f64e0f8) the meta head casts no vote - it scores an entry the
consensus already cleared and vetoes the ones under the cost-adjusted
break-even. The code still said otherwise. LiveMetaGate() was a virtual on
CExpertSignalCustom, so MA, RSI, MACD, Ichimoku, the four direction nets,
the session and news filters and the risk guard each carried a meta-gate
method they had no business having; one class implemented it and a dozen
inherited it. The trading pipeline held the gate as a CExpertSignalCustom*
- a signal pointer, with a signal's two hundred other methods reachable
from the entry path.

Expert\Trading\MetaGate.mqh now owns the abstraction:

  CMetaGate            one pure virtual, Evaluate(), and the two static
                       readings of a verdict (Blocks / Scored)
  META_GATE_*          names for the four codes the three call sites used
                       to spell as bare 0/1/2 and test three different ways
                       (`< 0` here, `== 2` there, `else` for the rest).
                       Codes unchanged; only ONE of them blocks, and that
                       asymmetry is now stated where it lives.
  SMetaGateTelemetry   the five m_metaGate* members that were on the AI
                       signal base - inherited by every direction model,
                       meaningful for none of them. One lifetime, one
                       writer, one object; the arm latch and the two
                       counters are a set that clears together.

g_warriorMetaGate is a CMetaGate*. MQL5's single inheritance means the head
cannot also BE one (it already extends the AI base for the net, the era
loop, the feature windows, the label caches and persistence), so it owns a
bound CMetaGateAdapter and hands that out - the same shape CTrainingDataView
uses for the same reason. LiveMetaGate() is gone from the signal base.

Behaviour unchanged: same codes, same thresholds, same fail-open doctrine,
same live-only telemetry rule. The adapter fails open when unbound, on that
same doctrine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 15:33:47 -04:00
AnimateDread
11006a8e38 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
AnimateDread
4e508460ac 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
AnimateDread
d7469c69a1 fix(gate): a field renamed on the definition side left one reader behind
SDeployVerdict's bothSidesLive became twoSided when the member and ensemble
gates were unified, and the member call site kept reading gate.bothSidesLive.

SECOND TIME THIS EXACT SHAPE HAS BILLED THIS SESSION - the first was `s == 0`
surviving the deletion of the loop that declared `s` (1d7ebbd). Renaming a
declaration does not find its readers, and the compiler only finds them when
no other binding happens to fit.

So this is now checked rather than reviewed: every `instance.field` read
against the six value objects is resolved against what the struct actually
declares. Six structs, zero unresolved reads.

Also renames the ensemble local `vote` to `voteGate`. SVoteAccumulator is
already called `vote` in the base class, and two different `vote`s one
inheritance step apart is a reader trap even where the scopes do not clash.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 14:40:29 -04:00
AnimateDread
3ea2bbc015 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
AnimateDread
93d7bbe677 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
AnimateDread
7452bd1ba9 fix(geometry): a shutdown abort cleared one tally of ten
Found by grouping, not by looking for it.

The candidate-geometry scan kept ten accumulators as ten separate
members. Era start cleared all ten in a ten-line block. The
shutdown-abort path inside the exit-policy simulation cleared
m_geoTrades and nothing else, so nine partial sums - diffSum,
diffSumSq, incSum, candSum, candSl, candTp, incOpen, candOpen,
startTick - survived the abort with the aborted era's values.

The next era then accumulated onto those sums while counting from
zero, so the paired mean is sum/trades with a numerator carrying an
extra era's worth of difference. The paired sigma is worse: diffSumSq
inherits the same contamination, so the scan reports a tighter or wider
spread than it measured depending on what the abort happened to be
holding.

That is the SAME arithmetic that failed its own acceptance test in
b5e22a1, where the reported gain turned out to be monotone in timeout
share. This is not that bug - it needs a shutdown mid-era to fire - but
it lands on the same number, and any geometry reading taken from a
session that was stopped and restarted is suspect.

SGeometryScan now owns all ten with one Reset(). Both sites call it.
A partial reset is no longer something that can be written: there is
one door, and it clears everything behind it.

The struct initialises itself, so the ten constructor-initialiser
entries in Lifecycle.mqh are gone too - MQL5 cannot list struct fields
there, which is a second reason ten loose members was the wrong shape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 13:49:09 -04:00
AnimateDread
a699bd597d refactor(meta): one owner for the pattern taxonomy and its table names
MetaCorpus was already a class, so the raw-include problem was not the
one here. The problem was that the rule for naming a signal-DB pattern
table

    MetaFamilyName(f) + "_Pattern_" + p + ("_Buy" | "_Sell")

was written out FOUR times, each wrapped in its own identical
family/pattern/side triple loop: the corpus loader, the stale-DB guard,
META's row counter and META's exporter. Four chances for a rename to
leave three of them querying an absent table and reporting it as
"family disabled" - which is what that code says when a table is
missing, so the failure would have looked like normal operation.

CMetaFamilies now owns the taxonomy and that rule. Callers walk ONE
flat index over all 52 tables and never spell a name:

    for(int ti = 0; CMetaFamilies::TableAt(ti, table, f, p, isBuy); ti++)

Enumeration order is unchanged - Buy then Sell within a pattern,
families in order - so the corpus is assembled in exactly the same
sequence as before.

META's OneHotSlot had a second hardcoded 0/4/8/14 ladder with a comment
reading "matches MetaFamilyPatterns' 4/4/6/12" - a note asking a reader
to keep two constants in step by hand. The ladder is now summed from
the pattern counts, so they agree by construction. The bound against
META_ONE_HOT_SLOTS stays in META: the head's input width is that
class's business, and a taxonomy grown past it must be caught rather
than silently truncated.

Caught while re-reading the rewritten loop: my first counter was `t`,
and the body declares `MqlDateTime t`. Renamed to `ti` at all four
sites before it reached a compile.

MetaCorpus.mqh moves to Expert\Training\ with the other real classes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 12:26:33 -04:00
AnimateDread
a0f9cfa3e0 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
AnimateDread
8a9aaa4800 fix(training): the wall-clock budget was left behind in Train()
Compile error, four sites: TRAIN_TIME_BUDGET_MS was a const local to
Train(), and the four passes that yield on it are now defined above
Train(). I checked every pass for the eight locals STrainEra replaced
and missed the ninth, because it was a const rather than a variable.

It is per-call state like everything else in STrainEra, so it moves
there as budgetMs. All four sites asked the same question the same way

  GetTickCount() - era.chunkStartTick >= TRAIN_TIME_BUDGET_MS

so the struct answers it once as BudgetSpent().

Swept for the same class of mistake rather than waiting for another
compile: Train() now has exactly two top-level locals left
(STABILITY_WINDOW, STABILITY_TOLERANCE) and neither appears in any
extracted body.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 11:40:15 -04:00
AnimateDread
77482b23a9 style(training): the era's second half was indented as if nested
673 lines after the OOS pass sat at indent 6 while being at function
level. It read as a block inside something, which is how I initially
mis-measured pass 3 as 1,178 lines when its closing brace is 504 lines
in - the indentation, not the braces, was telling the story.

Whitespace only. Verified by comparing every non-blank line of both
revisions with leading and internal whitespace collapsed, COMMENTS
INCLUDED: zero lines added, zero removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 11:31:04 -04:00
AnimateDread
08c2cecd70 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
AnimateDread
0a1337edb2 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
AnimateDread
445470baf8 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
AnimateDread
788115970c 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
AnimateDread
05f1a539c4 feat(geometry): measure per-candidate barriers against the one global pair
Stage 2a of the candidate-conditional geometry the record has named as next and
never built. MEASUREMENT ONLY - no order uses it yet.

WHY THIS AND NOT META-LABELING. Meta-labeling asks take-it-or-skip-it at fixed
geometry, and its verdict stands: real skill, 0 operating points clearing
break-even, and the 4070c5c retraction only moved that bar 1.4pp. The excursion
head, by contrast, just cleared at 3.5-4.6 sigma on LSTM across four eras and
beat the trailing-quantile incumbent. What is learnable here is MAGNITUDE, so
the lever is the geometry, not the veto. A per-candidate rung means a
per-candidate break-even, which a binary gate cannot express.

HOW IT IS SCORED. At each OOS call the same bar is resolved under the incumbent
pair AND under the pair this bar's excursion head would choose, and the paired
difference is accumulated in R with a 2-sigma test. Both legs come from the SAME
first-passage ladder - four array reads, no re-walk, exact even on the ~28% of
bars where both barriers were touched. Mixing the ladder with the price walk
here would measure the discrepancy between two of our own evaluators rather than
the effect of the geometry, which is precisely what f8ac10c had to unpick one
layer over.

The candidate pair applies the GLOBAL derivation's own rule per bar: stop at
BARRIER_SL_QUANTILE of adverse travel, target at the median of favourable.
Neither creates expectancy; what moves is the break-even, which is why the
report quotes R and never a win rate.

FREE VALIDATION. The sigma gate predicts LSTM helps and CONV hurts. If the R
difference reproduces that ordering across members, the head's usefulness is
confirmed by a second, independent measurement. If it does not, something is
wrong and this must not be wired to orders.

Two things caught while writing it, both silent if missed:
- The ladder stores TRAVEL FROM ENTRY, and the scan's mapping is
  risk = ladder + spread but reward = ladder - spread, so the two legs convert
  with OPPOSITE signs. The stop leg had the sign backwards.
- A GEOMETRY_BUDGET_MS wall clock, because this adds a feature-window build and
  a head forward per OOS call to a walk that already runs unchunked at era end
  on a single-threaded EA. That is the shape that got the process force-
  terminated on 2026-08-21. It stops scoring, never the replay, and the report
  prints how many calls it covered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 08:29:59 -04:00
AnimateDread
e372ce60a9 fix(vote): "0 fired" on USDJPY meant the threshold is above the highest vote the ensemble can cast
USDJPY has taken no trades in 66 eras and its highest vote ever seen is 13
against a 25% threshold. Not a bug and not undertrained models - arithmetic.

Direction() divides the summed contributions by the CAPABLE weight, so a
unanimous vote returns the capability-weighted mean of the tier weights, which
is roughly the pooled holdout win rate. USDJPY's members pool at 15.6-19.4%
(its label base rate is 14.0% against SP500's 25.4%, because its derived
geometry resolves far fewer bars directionally: Buy 10.3% Sell 11.2% Neutral
78.6%). So the ensemble's CEILING is ~19 and the threshold is 25. Coverage can
never leave 0, and no amount of training moves it, because the ceiling IS the
win rate.

The report now computes that ceiling - every member voting at its best tier -
and says so when the threshold sits above it, instead of printing "0 fired at
vote>=25%" which reads as "the models are unsure".

Same class as the excursion head's disjoint gate (ee4d459) and the reason
ReportDetectability exists: a configuration that cannot reach its own bar has
to say that, not report a number that looks like evidence.

Also: VerboseMode and Run_Alglib_Baselines back to false. The per-era cadence
was for reading the horizon break-even and the excursion sigmas; both are
settled, and TrainLogDue still prints them every 25 eras. The baselines cost a
45 s single-threaded freeze at every attach and their forest row turned out to
be one deterministic observation that does not survive overlap deflation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 08:16:11 -04:00
AnimateDread
04d70fb748 refactor(dry): one writer for the era-resume context, one for an alt-data row
Two literal duplications the scan found, both of the kind where a divergence is
silent:

- Training.mqh stashed the era-loop resume context at FOUR yield points, seven
  identical assignments each (pass 1 differing only in i-1). A field missed at
  one of them resumes the next chunk against a different era than the one that
  yielded, and nothing reports it until the numbers drift. Now StashEraResume().

- AltDataFetch grew its five parallel arrays inline in three places. They are
  one record split across five buffers, so a resize missed on any one reads out
  of range on the NEXT append, not at the site of the mistake. Now
  AltSeriesAppend(), which returns the new index and zero-fills; callers set
  only the columns their source has.

Braces balance across every in-scope file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 00:36:36 -04:00