Commit graph Warrior_EA/Expert/AIBase/Training.mqh
Author SHA1 Message Date
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
AnimateDread
b91c7b1f7a refactor(comments): box headers to stdlib length
The //| box blocks were excluded from 0b06f8e and 5efdb48 and were what
remained: 160 of them ran to 10+ lines, the longest to 88. Compressed to their
leading topic sentences - 5 lines for a function header, 8 for a file header -
keeping the box format and the standard MQL5 name/author lines verbatim.

Verified at the BYTE level this time, across every in-scope file: the list of
non-comment lines is byte-identical to HEAD and braces balance. The first check
compared a locale-decoded 'git show' against a UTF-8 read and flagged 25 files
that had not changed at all - every BOM and every non-ASCII line mismatched.

47,696 -> 40,665 lines in scope; comment share 38% -> 26%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 00:30:14 -04:00
AnimateDread
5efdb48de4 refactor(comments): stdlib comment style across the remaining in-scope files
Same pass as 0b06f8e, applied file by file: comment runs of 4+ lines compressed
to their leading topic sentences, capped at 4 lines, whole sentences only.
Warning sentences (NEVER / MUST / trap / would-have) survive the budget.

Every file was checked the same way before committing: the list of non-comment
lines is byte-identical to HEAD, and braces balance. No code was touched.

Panel/, Enumerations/ and the already-terse System headers needed little or
nothing - PooledGate, TradeChecks, BinomialStats and Random came through with
no blocks over the threshold at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 00:25:52 -04:00
AnimateDread
026078d635 fix(reset): a fresh model quoted the deleted model's timeout share
Audit of Delete && Reset Weights before pushing. The button is sound - the
21:11:41 wipe deleted all six files for all four members (0 FAILED, four
distinct paths), cleared the arrow sidecars, rebuilt fresh topologies, and
re-derived the barrier geometry from scratch (2.00/6.00 fallback -> 1.21/2.43
from 10610 excursions), so the 2026-08-19 geometry-laundering fix holds. The
per-run block in Train() clears the plateau, best-score and deploy-gate state,
confirmed on the log either side of the reset (PAI 29.2->24.4, HYB 30.4->25.3,
stage 1/3 -> 0/3).

One thing it could not clear, introduced by 4070c5c: m_lastTimeoutShare,
m_lastTimeoutMeanR and m_exitReplayReported. These are sticky across ERAS on
purpose - EmpiricalBreakEvenPct reads era N-1's measurement during era N - and
nothing distinguished that from sticky across MODELS. A reset therefore left
the fresh model reporting the deleted one's horizon-aware break-even on its
first eras, which is exactly the window an operator watches after pressing it.

It never showed up because a reset followed by a recompile constructs new
objects and the constructor initialises them; the failing case is the ordinary
one, reset with no recompile. Cleared with the other per-run state in Train().

Report-only in blast radius - LiveMetaGate and the rung selector still read
CostAdjustedBreakEvenPct - but it is the same class as the geometry bug that
block already had to learn: state correctly sticky for one lifecycle event,
silently inherited by another.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 21:31:42 -04:00
AnimateDread
f8ac10c808 fix(replay): the exit replay held trades through the Friday flat that the label and the live EA both close
The EXIT-POLICY REPLAY line reported an expectancy from SimulateTradeOutcome
beside a win rate read out of the label cache, and called them "the SAME
calls". Same calls, two different walks - and the walks did not agree.

TripleBarrierLabel stops at NextScheduledCloseAll (3e467f9); SimulateTradeOutcome
never called it, so the replay kept holding positions the live EA is flattened
out of and collected targets the label had already scored as cut. On SP500 H4
the simulation's implied win rate ran 2.2-3.4pp above the label's on identical
calls, and the timeout share read 0.8-1.3% because nothing was truncating the
horizon it walked.

That gap, plus 1.4pp of spread charged twice in CostAdjustedBreakEvenPct, is
the whole of the ~5pp the replay looked "off" by. It was not horizon timeouts,
which is what 4070c5c argued and this log disproved: solving E[R] = 3.008w - 1
+ t(1+m) on each row puts the simulation's zero-crossing at an implied 33.3%
against a frictionless 33.24% - it was internally consistent all along.

- SimulateTradeOutcome takes the close-all cutoff, same expression and same
  placement as the label's, falling through to the existing close-at-last-bar
  branch. Expect the timeout share to rise and expectancy to fall: the replay
  was optimistic.
- m_simTpHits counts this walk's own target-before-stop, printed next to the
  label's with the delta, so a future divergence is visible rather than
  inferable.
- The line prints all three break-evens and names the R convention. The
  frictionless figure is the one this expectancy crosses zero at, because both
  walks place the barriers off the spread-shifted fill.
- CostAdjustedBreakEvenPct is left alone: it still feeds the rung selector's
  BarrierMinReachPct, and moving that relabels.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 21:08:26 -04:00
AnimateDread
4070c5c9bd feat(breakeven): the break-even every layer scores against prices a trade that always resolves
CostAdjustedBreakEvenPct is risk/(risk+reward) and has no horizon term. It is the win rate a trade
needs when it is CERTAIN to end at one barrier or the other. SimulateTradeOutcome has an explicit
branch for the case where it does not - runs out of horizon, closes at the last bar seen for
whatever P&L that is - so on this label geometry the figure describes a different trade than the
one being replayed.

The gap is measurable and large. Across 21 exit replays today on SP500 H4 the geometric figure read
34.5% while the EA's own R simulation crossed zero between 27.4% (lowest positive) and 28.9%
(highest non-positive). Independent corroboration: the zero-skill reference, computed empirically
over every scored bar as max(winLong,winShort)/bars, reads 25.4% - add cost and it lands on the
same ~28%. The geometric number is the outlier, and every edge printed against it was ~6.5pp too
pessimistic: LSTM's 30.6%-win era reported -4.0pp while its replay returned +0.075 R on the same
trades.

With a timeout share t paying a mean m R apiece, expectancy is w(1+RR) + t(1+m) - 1, so

    w* = (1 - t(1+m)) / (1 + RR) = CostAdjustedBreakEvenPct x (1 - t(1+m))

which needs no new geometry - the existing figure already carries 1/(1+RR).

This commit MEASURES ONLY. The replay now separates timeout exits from barrier exits and latches
t and m for the next era to read (the accumulators are zeroed at era start and filled at era end,
so a mid-era reader sees zero trades and would fall back forever). Both break-evens print side by
side on the replay line with t and m beside them, and the threshold line's REPORTED edge - which
selects nothing - switches to the horizon-aware figure so the operator stops reading a wrong sign.

DELIBERATELY NOT CHANGED: LiveMetaGate's veto and the rung selector's BarrierMinReachPct still read
the geometric value. Both are decisions - the second re-derives geometry and therefore relabels -
and t and m have so far only been inferred from a zero-crossing, never seen on a log. One era of
this instrumentation settles that.

The file already contained the argument, one branch away, in the vote-exit comment: a vote exit
produces a CONTINUOUS payoff, not a win or a loss, and that is why an exit-aware gate cannot go on
scoring win-rate against a fixed break-even. A horizon timeout is the same thing, and unlike vote
exits it is on by default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 19:26:25 -04:00
AnimateDread
2c19cf8408 fix(calibration): the field reported the raw argmax, which is not what trades
The CALIBRATION field added in 667f2bc counted m_oosBuyPredicted, and that is
ApplyClassificationSoftmax() - the bare argmax, before the fitted operating
point ever sees the bar. The decision is AdjustedSignalFromSoftmax(), which
applies m_dirConfThreshold and is counted separately in m_oosBuyFired.

So the field was describing a layer that never places an order, and the two
layers are nowhere near each other. Era 3 of the 12:18 build, all four
members:

  CALIBRATION (argmax)  Buy 1.6-2.5x  Sell 0.8-1.8x  Neutral 0.2-0.5x
  threshold fit         directional coverage 34.3-38.5% vs a 37.4% target,
                        miss 0.3-3.1pp

Read together those say the argmax leans hard directional and the operating
point corrects it to within about a point. Read alone, the first says the
models have collapsed away from Neutral, which is what I would have concluded
from it - an instrument that misleads exactly where it is being looked at.

The traded layer now leads. The argmax stays as a tail because the GAP is its
own diagnostic: it says how much of the calibration the operating point is
carrying, and a widening gap means the head is drifting while the threshold
absorbs it.

No behaviour change - reporting only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 12:44:03 -04:00
AnimateDread
667f2bcb6b revert(labels): drop the one-sided exit target; measure the calibration drift instead
Reverts a863796 on the operator's call - "unnecessary complexity". It was
right about the mechanism and wrong about the priority: it re-cut the classes
for a case the measured verdict never reaches (SP500 H4 reads "both sides" at
the derived geometry), while the drift that IS happening affects every chart
and every era. Recoverable from a863796 if a one-sided book ever becomes real.

Two pieces of it survive, both independent of the exit idea:

The drift verdict keeps reading m_winLongCache/m_winShortCache rather than the
collapsed label pair. That line reports always-long vs always-short win rates,
which is what the win caches hold - each side scored on its own barriers,
published before the collapse. The label pair carries only the side touched
first, so it undercounted long wins by the both-won-goes-to-short share. There
are zero both-won bars at any geometry with target >= stop, so this changes no
number today; it changes the wrong number to the right one.

And the .cfg gains nothing and loses nothing: the two appended ints go away
again, and they were the last fields, so a .cfg written by yesterday's build
still reads correctly - the loader simply stops before them.

WHAT THE REVERT MAKES ROOM FOR. The operator's actual requirement is that the
model reproduce the label distribution the scan measured, and nothing in the
pipeline ties it to that. The loss trains on a rebalanced sample and the
abstain rate is owned by a margin threshold fitted on EDGE, so the call rate
and the label prior can drift arbitrarily far apart - and did, invisibly:
at era 1350 the models call Buy on 20-28% and Sell on 22-32% of bars against
a scan-measured 2.1% and 4.8%. Roughly a 10x over-call, and not one line in
the journal said so.

The era line now carries it:

  CALIBRATION calls vs true rate Buy 28% vs 2% (14.0x) Sell 32% vs 5% (6.4x)
  Neutral 40% vs 93% (0.4x)

Reported as a ratio because that is the readable number - 1.0x is calibrated.
This is deliberately a measurement and not yet a correction: matching the
label rate would put coverage near 7%, below the ensemble gate's own 12.4%
coverage floor, so calibration and the gate are in direct conflict and which
one yields is the operator's call, not mine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 10:14:26 -04:00
AnimateDread
68ef19797b fix(telemetry): one ensemble member had never printed a single era line, in any run on record
The era-progress rate limit was a function-scope `static`:

    static uint lastProgressLogTick = 0;
    shouldLogProgress = (nowTick - lastProgressLogTick >= 5000);

In MQL5 that is ONE variable for the whole build, not one per object. A 5s
limit meant to keep a single model's console readable was therefore a limit
across the WHOLE ENSEMBLE, and it did not distribute fairly - it starved
whichever member finishes last, every era, deterministically.

Measured on today's run: the era barrier releases the members together and
LSTM landed 2.06s, 2.43s and 2.28s behind ConvLSTM on eras 1-3, against a 5s
window it could never reach. LSTM printed zero era lines. PAI, CONV and HYB
printed all of theirs - 3 each this run, 17 each in the 10:00 run, LSTM 0 in
both, and 0 again in the 08:32 run.

So one model in four has been training with NO per-era telemetry: no
per-class recall, no dW/W ratios, no zero-skill comparison, no deploy-bar
line. It was still doing the work - tier re-ranks, threshold fits and
exit-policy replays all appear on cadence - which is what made the hole look
like a grep that kept missing the line rather than a line that was never
written. It cost me the LSTM half of a gradient check earlier today.

Now a member, so each model rate-limits its own console output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:48:56 -04:00
AnimateDread
ef6d62c604 fix(ensemble): the era barrier read healthy startup work as a dead member
Reported symptom: one member at era 17 while the rest sat at era 2, with
the combined vote never scoring. Two faults compound to produce exactly
that, and neither needs a broken model to trigger.

FIRST - BUSY WAS READ AS STUCK. BarrierEraHeartbeat() decides liveness
from one signal: has m_eraCount changed in the last 12 minutes. But
Train() returns early, before the era loop, for three ONE-TIME phases
that never touch m_eraCount - the label-cache prebuild, the pattern-DB
backfill and the OOS simulation walk - and those are precisely what a
slow topology spends its first many minutes doing. A member grinding
steadily through a prebuild therefore looked identical to a dead one and
was dropped from the barrier at startup, before it had trained a single
era. The constant's own comment states the flawed premise: "comfortably
past the slowest healthy ERA on the deepest chart" - true, and not the
question being asked. Those three branches now call NoteBarrierProgress()
and a chunk of phase work re-arms the watchdog exactly as an era does.

SECOND - EXCLUSION HAD NO BOUND. Once dropped, a member is skipped by
EnsembleMinTrainingEra(). Drop every OTHER member and that loop finds
nothing to take a minimum over, falls through to its `return m_eraCount`
fallback - the CALLER'S own era - and EnsembleEraBarrierHolds() evaluates
`era > era`, false, for everybody. The barrier silently becomes a no-op
and the fastest member runs away unbounded. EnsembleMinEraAnyMember()
now measures against every still-training member, excluded or not, and a
member may lead it by at most ENSEMBLE_MAX_ERA_LEAD eras.

The cap is deliberately a real stop rather than a warning. A
desynchronised ensemble is not a degraded one: the combined-vote score
and the joint checkpoint both require every member on the same era, so
weights trained past the cap can never be certified by any gate. The
hold reports which of the two it is, because the operator's next move
differs - an ordinary barrier hold resolves itself, a lead-cap hold names
a member that needs diagnosing and will not resolve on its own.

Not yet explained: "only one NN listened to the stop command". The panel
now dispatches down the filter tree and reports the count it reached
("training stopped (N model(s))"), so the next run answers that
definitively instead of leaving it to inference.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:35:25 -04:00
AnimateDread
45af5b9c1e 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
AnimateDread
4346dd3c24 refactor(stdlib): the vote thresholds are ints on the library's scale, not "confidence %"
The MECHANISM was already stdlib and is untouched: ThresholdOpen() ->
m_threshold_open, tested as `m_direction >= m_threshold_open` exactly as
CExpertSignal does it. What was wrong was the presentation. Both inputs
were preset ENUMS labelled "Min confidence to open/close (%)", which
names the wrong quantity - m_direction is a WEIGHTED MEAN OF PATTERN
WEIGHTS, not a probability, and nothing in this path is a confidence.

They are now plain ints named the way the MQL5 wizard names them:

  input int Signal_ThresholdOpen  = 25;   // [0...100]
  input int Signal_ThresholdClose = 101;  // [0...100, 101 = never]

Values are exactly what shipped, so behaviour is unchanged. 101 rather
than the library's default of 100 for close: a weighted mean of pattern
weights cannot REACH 101, which is how the shipped config disables the
vote exit, and quietly lowering it to 100 would re-arm a live exit route
as a side effect of a naming change.

VOTE_CLOSE_PRESETS is deleted (its only user is gone). PERCENTAGE_PRESETS
stays - MinRecall genuinely is a percentage.

** ACTION NEEDED ON DEPLOYED CHARTS: the inputs are RENAMED, so saved
.set files no longer match and charts fall back to the defaults above.
Those defaults are the current shipped values, so a chart on 25/Disabled
needs nothing; a tuned one does.

Comment cleanup in the same pass, and this part was not cosmetic - three
blocks documented mechanisms that no longer exist:
- the AI early-exit route (deleted in 38a12a2) described as live and
  still firing every bar;
- the m_lastNonNeutralSignal alternation gate (removed 2026-08-01)
  described as consuming the AI's vote;
- 16 lines of VOTE_CLOSE_PRESETS documentation orphaned by that enum's
  deletion, ending with "see that enum's note directly above" pointing
  at nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 08:39:01 -04:00
AnimateDread
19e3595e20 fix(build): g_eta - the learning rate global no longer shadows a stdlib local
MetaEditor: "declaration of 'eta' hides global variable" (Math.mqh:792
vs Network.mqh:80). The standard library's Math\Stat\Math.mqh declares a
local `double eta` in its incomplete-gamma branch, and our bare global
of the same name is in scope there.

Same fault as the b1/b2/lr/momentum macros retired in ea2552e: a
single-token global name living in a header that library code gets
compiled beside. The library cannot move, so ours does - 112 references
across 13 files, whole-word only.

Named g_eta rather than g_learningRate to stay inside the vocabulary
already around it (ETA_DECAY_FACTOR, ETA_MIN, m_etaCeiling, etaBefore),
all of which are untouched and none of which shadow anything.

One log line said "continuing to explore without decaying eta", where
the word was prose rather than a symbol reference; that reads "the
learning rate" now instead of naming a variable at the trader.

Scanned for the next occurrence rather than waiting for it: the only
other bare lowercase globals in the tree are eaName and tableschema,
both distinctive enough not to collide with a library local.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:00:09 -04:00
AnimateDread
ce2c324527 feat(baselines): Alglib forest + linear on the NN's own matrix
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.

Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.

Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
  a baseline handicapped by a forced zero intercept would flatter the
  net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
  test rather than correcting it here - both sides must face the same
  bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
  being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
  difference cannot be a regime difference. What was dropped is logged.

Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:45:03 -04:00
AnimateDread
29c82ad50b 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
AnimateDread
ea2552efe2 refactor(stdlib): adopt Math\Stat for the deploy gate's normal tail; retire the b1/b2/lr/momentum macros
The gate's NormalUpperTail was a hand-rolled Abramowitz & Stegun 26.2.17
approximation. Its own comment gave the reason - "drags a chain of headers
behind it" - and that turned out to be one file: Math\Stat\Normal.mqh
includes only Math.mqh, which includes nothing. Swapped for Cody's rational
approximation in the library (~18 significant digits vs |error| < 7.5e-8).
No past verdict changes: at the z the gate operates on, the difference is
orders of magnitude below DEPLOY_FAMILY_WISE_ALPHA.

Adopting it needed the four bare macros in AI\Network.mqh gone first.
"#define b1 AdamBeta1" collides with an identifier in Math.mqh, so the
include would have macro-expanded the library's own local and failed to
compile - the same landmine that made the original author rename the
approximation's coefficients to ntB1..ntB5 rather than use the reference's
b1..b5. lr, b2 and momentum are the same class of hazard: single-token
global macros in a 52k-line codebase. All four now resolve to the input
names they always aliased, which is a pure textual identity - verified zero
bare occurrences remain.

Also:
- SelectionSort over the buffered signals was O(n^2) with an O(n^2) count of
  StructToTime calls, because the comparison rebuilt both datetimes from the
  six int date fields every time. Now materialises the keys once and does an
  insertion sort; ArraySort cannot permute a struct array. IsEarlier goes
  with it, MakeDateTime becomes SignalTime.
- Seven FileOpen sites lacked FILE_SHARE_READ|FILE_SHARE_WRITE, including
  AtomicWriteBegin, which stages every model save. All 43 sites now carry
  them - an exclusive open fails outright when another process holds the
  path, which here has meant a silently skipped save.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:31:36 -04:00
AnimateDread
888f32d21c 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
AnimateDread
5e0317f09d 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
AnimateDread
f64e0f8b67 feat(ensemble): per-NN inputs replace the preset selector - the meta head becomes the vote's gate
User design (2026-08-19): 'remove the enum menu that selects neural networks... individual
inputs for every NN just like classic signals... the META NN should be integrated into the
voting decision pipeline when enabled... as a bonus meta labelling is applied to enabled NNs.'

- AI_CHOICE is GONE (tombstoned per the stale-.set doctrine). Use_MLP/Use_CONV/Use_LSTM/
  Use_CONVLSTM are ordinary bools like the classic votes; the ensemble arithmetic adapts to
  any subset because the consensus divisor is the enabled capable weight. Two or more
  enabled = ensemble (|ENS1 token + joint gate, exactly the old AI_HYBRID fingerprints, so
  existing weight files keep loading); one = the old solo preset; none = classic-only.
- Use_MetaLabeling un-couples META from the direction NNs (the old selector made them
  mutually exclusive). S3 ships: CSignalMETA::LiveMetaGate scores each vote-cleared entry
  (shared window at bar 1 + proposal descriptor: side, net vote, live geometry, spread/ATR;
  pattern one-hot ZEROED - ranking, not calibrated probability, documented in the body) and
  vetoes below the cost-adjusted break-even. Entries only; fail-open everywhere, loudly.
- COEXISTENCE HAZARDS closed: VoteCapableWeight()=0 and ProspectiveVote()=false for the
  meta target - solo-only until today, a trained META would otherwise sit in the consensus
  divisor as a permanent abstainer and shrink every vote by its module weight.
- CERTIFIED == TRADED: the ensemble era verdict replays the identical veto through the same
  g_warriorMetaGate pointer over its OOS fired bars (bar re-resolved from the row's own
  time; fail-open counted as fires and reported: 'metaGate: N approved, M vetoed, K
  unscored'). The overlay deliberately does NOT replay it (veto-filter-in-replay class,
  calendar-cliff precedent) - documented at the sweep site. Solo charts' own gate does not
  model the veto - the standing solo-gate caveat, documented at the input.
- DB continuity: the pattern/journal DB fingerprint's first slot was (int)AIType;
  DbLegacyAiSlot() maps every legacy-expressible config to its OLD value (new 2-3 member
  subsets get 100+bitmask, outside the legacy range) so no existing database re-keys.
  filterID becomes the enabled roster via one EnabledNNSummary().
- HUD: the meta line shows the gate (armed/(trn), last P vs BE, ok/veto tally); the
  armed/disarmed announcement fires on state change via one latch (MetaGateArmedNow), not
  only when an entry happens to be proposed.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:01:02 -04:00
AnimateDread
1445f175ce 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
AnimateDread
f30e342a1d 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
AnimateDread
1a900a0a35 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
AnimateDread
e59dc1629f 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
AnimateDread
b28c81eb78 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
AnimateDread
4858507146 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
AnimateDread
2c443ba3ad 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
AnimateDread
525e92ecb2 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
AnimateDread
64b77e4bbe fix(diag): the cache-invalidation stall message could never name the cause
SP500 and XAUUSD LSTM wedged at era 1 from 19:43 to 21:00+ (77 min) while their
three siblings passed era 200 - the 12-minute barrier exclusion correctly kept
the charts alive, so the ensembles ran three-handed. Both printed:

  cache invalidated at era start (era sized 16236 bars, cache holds 16236)

Equal numbers, which reads as "so it wasn't the size". That inference is not
available: EnsureBarCachesCapacity assigns BOTH invalidation keys
(m_labelCacheBars = bars, m_labelCacheAnchorTime = m_Time.GetData(0)) before it
returns true, so a message built afterwards reports the values it just
overwrote. The two counts are equal BY CONSTRUCTION and ReportTrainStall's
anchor= field is always the live one. The line whose stated job is to name which
key tripped was structurally incapable of naming it.

Capture bars/anchor BEFORE the call and say which one moved:
  "SIZE CHANGED 16236 -> 16240" or "size unchanged", and
  "ANCHOR MOVED 2026.08.18 00:00 -> 04:00" or "anchor unchanged".

Not guessing at the cause. An anchor moving every era with the size steady is a
new candle each pass or a Time buffer that is not being refreshed; a moving size
is the era/prebuild disagreement the branch was written for. The next occurrence
will say which, instead of costing another session.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 21:57:12 -04:00
AnimateDread
62a719f04c fix(consistency): one geometry authority, one exit authority, and the MI screen finally gets a veto
Consistency pass before a fresh deployment. Three places where two systems were
choosing the same thing and one of them silently lost.

1. THE GEOMETRY SCAN'S DECISION WAS INERT - measured, not suspected.

USDJPY, 2026-08-17:

  14:24:12.844  adopting barrier geometry 2:8 ... Relabelling and training on it.
  14:24:12.979  triple-barrier labels - stop 1.61*ATR, target 3.21*ATR ... this is what trains

It adopted 2:8 and trained on 1.61:3.21. ReportBarrierGeometryScan wrote only
m_sl_mode/m_tp_mode, and BarrierMultiples ranks the DERIVED pair ABOVE those ints -
so on any model carrying a derived pair (every model with a .cfg, including a fresh
one whose weights are gone but whose sidecar survived) the adoption changed nothing.
Worse, had it changed something it would have been undone immediately: the adoption
sets m_labelCachePrebuilt = false, and that prebuild re-runs DeriveBarrierGeometry at
era 0, which overwrites m_derivedSl/TpMult from the excursion quantiles.

ONE AUTHORITY: the derived pair, because it is what the labels read, what the deploy
gate certifies, what g_Derived*AtrMult places on the live order, and what the .cfg
pins across restarts. The scan now writes THAT (floored by MIN_SL_ATR_MULTIPLIER, the
same floor DeriveBarrierGeometry applies so the live stop can never be wider than the
labelled one), republishes to the bridge immediately rather than at the next era end,
and forces the sidecar to be rewritten. m_geometryAdopted latches it so the derive
pass the adoption itself triggers cannot overwrite it.

The scan outranks the derive for an evidential reason, not an architectural one: its
winner cleared a permutation test against the null of the MAXIMUM over every eligible
pairing, and it scores the incumbent derived pair as a peer in that same field. The
derive is a descriptive quantile read with no significance test attached.

BEHAVIOURAL CHANGE, and the reason to flag it before a fresh test: barrier geometry
will now actually move when the scan says so. Until today it never did.

2. ONE EXIT AUTHORITY, tied to whose certificate the trade was placed under.

CheckClosePosition had two routes. The AI early-exit reads the AI vote undiluted and
is exactly what the new exit replay reproduces. The blended route thresholds
m_direction, the average over EVERY filter including classic ones whose live votes
pass 3 never computes - so it can close a position the certificate never modelled,
and no replay can ever check it.

When g_DerivedSlAtrMult > 0 the AI's measured geometry is on the order, which means
the deploy gate's certificate is the reason the trade exists. In that state the AI now
governs the exit and the blended route is suppressed. Classic-only configurations are
untouched: there the blended route is the only exit opinion and stays exactly as it
was. Nothing moves at the shipped defaults either way (Min_Vote_Close = Disabled).

3. EDGEFINDER, SECOND HALF: THE MEASUREMENT NOW STEERS.

The MI suite has always printed its verdicts and then trained the direction target
regardless of what they said. That gap IS the difference between this and the
EdgeFinder discipline: measure what the market offers, THEN aim.

m_dirEvidence is set when EITHER the feature/label mutual information OR the
normalised excursion asymmetry clears its block-permuted null - an OR, because the two
look for the same thing by different routes and requiring both would reject on the
weaker of two independent measurements. Normalised asymmetry specifically, never the
raw one, which is the volatility confound.

Deploy - solo AND ensemble - now requires it. A run without it still trains, and keeps
its checkpoint: the research value is real and the measurement can be wrong. It simply
may not go live. Reported separately from the statistical gate because the remedy is
different: a failed selection test says train differently, this says look somewhere
else. Excursion SIZE keeps clearing where direction does not, and that is a
risk-control head rather than an entry signal.

For the ensemble the check is per-chart by construction - the MI suite runs once and
shares its outcome across members - which is the honest treatment: four models finding
nothing between them is not four chances at an edge, it is four fits to the same
absent information.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 16:43:20 -04:00
AnimateDread
6069581323 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