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>
The diagnostic shipped in de382bb came back off both live charts and
confirmed the arithmetic exactly:
CLOSE-ALL BUDGET - flattens every position every 29 bars ... an entry
landing anywhere in the cycle gets 15 bars on average. The horizon
ladder just granted 128.
So the ceiling the ladder was rejecting rungs against - BARRIER_HORIZON_MAX,
384 - never bound anything, while the one that does bind was invisible to
it. SnapHorizonToLadder and the scale ladder's fitsH test now both read
EffectiveHorizonMax(), which is the measured close-all cycle. One
function, so the ceiling cannot be lowered in the snap and left high in
the rejection test.
The CYCLE, not the 15-bar mean: a Monday entry really does get the whole
cycle, and rejecting on the mean would invent a second criterion where
the design deliberately has one ceiling and reports the milder snap-down
truncation instead of rejecting on it.
Expect the ladder to pick a NARROWER pair, which is what the MEASURE
objective already asks for - min provable EV grows as width squared, and
USDJPY's 6.00*ATR target was being asked of a trade that lives ~11 bars.
"Schedule off" is cached; "not enough bars loaded yet" is not. Caching
the latter would restore the 384-bar ceiling for the whole process
because one early call landed before history arrived.
RE-KEYS EVERY FINGERPRINT - the horizon is a label parameter, so this is
a full retrain on both charts. Done now because both are at era 0 after
a fresh deploy, which is the cheapest this change will ever be.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every label timeout on both live charts was the scheduled close-all and
none was the horizon. Not "mostly" - all of them:
USDJPY 14417 of 14417 timeouts ended by the close-all
SP500 2434 of 2434
targetDayOfWeek is CLOSE_FRIDAY, so every position is flattened weekly.
A trading week is ~30 H4 bars and an entry lands uniformly inside it, so
the average bar is labelled under ~15 bars of runway. The horizon ladder
granted USDJPY 96 and SP500 32, and the SCALE ladder rejects rungs
against BARRIER_HORIZON_MAX (384) - a ceiling that never binds while the
one that does is invisible to it. USDJPY's chosen target is 6.00*ATR,
asked of a trade that lives ~11 bars: 78.6% of labels come back Neutral,
the base rate collapses to 14.0%, and no model can clear a 33.4%
break-even against a label that mostly cannot resolve.
The close-all itself is correct and must stay - it is what the account
actually does, and 3e467f9 put it into the labels for that reason. What
is wrong is that the geometry deriver has never been told about it.
This commit only MEASURES it. MeasureCloseAllBudget() walks the real bar
series (session- and DST-correct, not arithmetic on a nominal week) and
returns the cycle length plus the mean an entry gets; a CLOSE-ALL BUDGET
line prints both next to what the ladder granted. No geometry changes:
the horizon is a label parameter, so capping it re-keys every
fingerprint and costs a full retrain on both charts. That is the
operator's call, and it should be made against this line rather than
against my arithmetic.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The CANDIDATE GEOMETRY line shipped in 05f1a53 said per-candidate
geometry beats the global pair on every SP500 member at 2-3 sigma. It
does not. It said so because a bar that reached neither barrier scored
0 R, and the incumbent's mean is NEGATIVE (-0.07 to -0.21 R). Against a
losing baseline a free zero is a win, so the widest candidate always
came out ahead - and the reported gain ordered itself by timeout share,
not by skill:
PAI 95.1% timed out -> +0.189 R (head measured -2.42 sigma, HARMFUL)
HYB 73.8% -> +0.182 R (head at chance, +0.68 sigma)
CONV 61.8% -> +0.163 R (head measured -2.47 sigma, HARMFUL)
LSTM 27.1% -> +0.158 R (head +1.67 sigma)
Monotone in the timeout share and inverted against the sigma gate. The
acceptance test written when this was built - "the sigma gate predicts
LSTM helps and CONV hurts; if the R difference does not reproduce that
ordering, something is wrong" - is what caught it.
A trade that reaches neither barrier is not worth zero. It is closed at
the horizon, which is what the scheduled close-all does live and what
SimulateTradeOutcome's timeout path already charges. So mark it there:
TripleBarrierLabel now publishes the signed close-to-close travel at the
last bar it actually visited (m_termTravelCache, same validity flag as
the excursion and ladder caches), and LadderOutcomeR prices a timeout
off it instead of returning false. A bar that cannot be evaluated under
BOTH pairs is now dropped whole - scoring one leg and defaulting the
other is the same bug in a smaller costume.
Second defect, same function: CandidateGeometryFor applied neither of
the floors the global derivation applies, so on USDJPY it chose stop
2.00 / target 1.00 - a 67% break-even, forbidden by the 1:2 policy
floor. c3daded in miniature: a selector optimising its own criterion
with no reference to the decision criterion. Both floors now apply, and
the ratio is re-checked AFTER the per-leg rung snap, which can lose it.
Also: the module weight was an unshrunk pooled win rate. USDJPY ConvLSTM
fired 19 times (2.0 effective), won 36.8%, and took module weight 0.37 -
41% of the ensemble's capable weight and the loudest voice on the chart,
off two effective observations. It also lifted the computed vote ceiling
to 26.3 against a 25 threshold, which is why THRESHOLD UNREACHABLE never
printed on a chart whose peak vote is 14 and whose practical ceiling
without that member is 18.8. The pooled rate is now shrunk toward the
coin-flip rate on the era's own OOS bars over 30 prior-equivalent calls,
and the tiers shrink toward the shrunk value rather than the raw one. A
member with ~300 effective calls moves by ~0.4pp; the 19-fire member
goes 0.37 -> ~0.15.
MEASUREMENT ONLY still - no order reads any of this.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
4373 -> 3325 lines. 236 comment blocks compressed to their leading topic
sentences; no code line changed. The archaeology - dates, observed symptoms,
the narrative of each past bug - lives in git and in the project memory, and
repeating it beside every declaration was crowding out the declarations.
Kept: the rule a comment exists to enforce. Any sentence carrying a NEVER /
MUST / trap / would-have warning is preserved even when it falls outside the
budget, because those are the ones that stop a regression.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
EXCURSION_MIN_DISJOINT was 200, sized as "~16k scored bars over a 64-bar
horizon leaves ~250 independent ones". The head scores the OOS SLICE, not the
history. At a 30% split and a 32-bar horizon the ceiling is 4691/32 = 147, so
200 was unreachable and every era printed "[disjoint sample too small]", which
an operator reads as "wait longer". Unpassable by construction - the identical
failure this file already documents one gate down, one layer up.
The trail gate inherited it: m_excTrailScored is a subset of the disjoint bars,
so it failed the same 200 for the same reason, at 130.
Raising OOSSplit or shortening the horizon would clear it and would be fitting
the experiment to the answer. Instead, ask the question the count was standing
in for - is the skill bigger than its own noise:
- The scorer banks one paired Brier difference per DISJOINT window over the
decision rungs (base-head, and trail-head). Disjoint by construction, so no
EffectiveSampleSize deflation applies - striding by the horizon is what buys
that - and paired on identical bars, so the correlation between the two
predictors cancels instead of needing to be estimated.
- passDj and passTrail now require skill >= EXCURSION_SKILL_USEFUL_PCT AND
>= 2 sigma, with the count reduced to a sanity floor of 30.
- Both sigmas print on the verdict line.
This is not a lowered bar. The 2% skill requirement, the oracle control and the
monotone test are untouched, and the sigma test can fail where the count test
never spoke: if +8.5% is noise across 147 windows, it will now say so.
DecisionRungMask() is the single definition of "rung the decision depends on",
called by both the scorer and the report, so the standard error is computed
over exactly the rungs the skill score is. The report's inline copy of the
bracketing test is gone.
Also prints whether the disjoint count is BELOW ITS CEILING or at it, so
"not enough yet" and "not in this configuration" stop reading the same.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The compact panel leads with "learning (era N, pass X%)". The verbose panel led
with "Study -> Era N" and then progressLine, which counts BARS inside the
running pass - it changes wording between passes and reads "Era complete" for
as long as a member sits at the era barrier. So turning VerboseMode ON, which
20b7d99 did as a test-run default, took the one continuously-moving readout
away in exchange for detail. Verbose is meant to be a superset of the simple
view; this was a swap.
Both output-shape branches now carry the same "(pass X%)" beside the era, built
from the same m_passLabel/m_passProgressPct the simple branch uses so the two
cannot disagree.
Also: buy marks are clrDodgerBlue rather than clrLime, per the user - blue
against red reads at a glance where green against red does not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
clrDarkGreen/clrDarkRed were hard to pick out against the candles. clrLime and
clrRed are the brightest pure pair MQL5 names, and they match what the vote
readout already uses for "this would trade".
The colour is the direction ENCODING, not decoration - a signal line carries no
arrow code, so SaveChartSignals recovers buy-vs-sell by comparing against
WARRIOR_SIG_BUY_COLOR, and marks left by an older build now decode as SELL. No
legacy fallback is kept, per the user: weights and arrows are wiped on every
push. The constraint is written next to the defines instead, for whoever changes
them on a chart that is not being wiped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
Two diagnostics on, neither in the fingerprint, so no model is re-keyed and the run resumes:
VerboseMode false -> true per-era journal instead of every 25th
Run_Alglib_Baselines false -> true verifies 3d81fed and finally reaches the OOS scoring
And the fix that makes the run worth doing. ReportExitPolicyDivergence printed ONCE per run while
vote exits are off, justified by "then it is arithmetically guaranteed to agree with the
certificate". That is the claim 4070c5c disproved: the certificate scores win rate against a
GEOMETRIC break-even while this line replays the real payoff including horizon timeouts, and the
two disagree by ~6.5pp. The timeout share is a MEASURED quantity that moves with geometry and
volatility - one print per run is the wrong cadence for it. Now on TrainLogDue(), the same density
as every other era line, with m_exitReplayReported still guaranteeing at least one.
What to read: BREAK-EVEN geometric X% vs horizon-aware Y%, and the timeout share and mean R beside
it. Those two numbers decide whether LiveMetaGate and BarrierMinReachPct get repointed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Yesterday's fix predicted the cross-validation's cost and declined it before starting, then left the
linear fit to a plain boundary check. The 17:01 run says exactly what that was worth:
ALGLIB MLP trained in 12.1 s ... NOTE: 6435 weights against 995 rows
cross-validation SKIPPED - 3 folds x 12.1 s = ~36.3 s predicted against 31.9 s left
Alglib baselines stopping before the OOS scoring - 548.8 s spent of a 45 s budget
Everything up to the linear fit obeyed the budget. LRBuild then held the thread for ~535 s in one
uninterruptible call and the check fired afterwards, on a decision that was already made. The same
block shows up in the training timer as 'SLOW ERA heartbeat - net fwd/back 3.9s | everything else
566.2s', and in three members re-arming a study event that never arrived.
LRBuild solves a (width+1)^2 normal-equation system: the cost grows as width^3 and ignores the row
count entirely, so the row cap that fixed the forest and the MLP does nothing here. Predicted from
that measurement and declined before it starts, like the CV.
Second, independent reason to decline it: at 800 columns against 1000 rows the normal equations are
singular, so any coefficients returned are one arbitrary solution of infinitely many. That fit would
have been printed as a baseline while carrying no information. Both grounds are checked, and the log
says which one applied.
The OOS scoring - the phase that answers the question the suite exists for - has now failed to run
twice for two different reasons. It is the first thing to check on the next attempt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Run_Alglib_Baselines was described as bounded because it 'cannot outrun the bar it runs on'. That
bound is four hours on H4, it was never enforced, and it is the wrong target: the EA is single
threaded, so while this pass runs there is no training, no panel, no tick handling, and a removal
request only queues.
Measured today on SP500 H4, 800 inputs x 4000 rows: forest 5 s, MLP 52 s, then the 3-fold CV -
three more full retrains. At 15:18:16, 83 s into it, MetaTrader force-terminated the EA. OnDeinit
never ran, so every arrow, panel object and label was orphaned on the chart. The init purge cleans
that up on re-attach; nothing in the pass writes to a cache, saves a model or feeds a decision, so
the damage was the freeze and the dirty chart, not corrupted state.
Three changes:
- BASELINE_MAX_CELLS. Every fit costs O(rows x width) and the row caps carried no width term.
Rows now shrink as the window widens - 1000 instead of 4000 at 800 columns - and the log says
when the cap bit.
- BASELINE_BUDGET_MS with BaselineBudgetSpent(), replacing the bare ShutdownRequested() calls at
each phase boundary. A stop and a spent budget need the same answer and only one was asked.
- The cross-validation is now declined BEFORE it starts, from its predicted cost - the measured MLP
time times the fold count. A boundary check cannot help once a phase is in flight, which is
exactly the phase that was in flight when the process died.
Also: the MLP now reports its weight count against its row count. At 800 inputs it carries ~6,400
weights, and today's run duly printed a training class error of 0.000. That is knowable from the
shape before fitting, so it is stated rather than left to a cross-validation that may not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two same-config charts collide on the config lock. AcquireConfigLock prints a precise REFUSED
line, but CExpert::InitIndicators then returns a bare false, RetryInitStep retries it five times
- a lock held by a live chart gives the same answer every time - and the last line on screen is
"Failed to initialize Indicators after retries", which names neither the lock nor the owner.
The explanation is six lines further up, under identical-looking retry noise.
A refusal that cannot change on a retry now says so and stops: AcquireConfigLock records the
reason, RetryInitStep repeats it and returns immediately.
Found because a second SP500 H4 chart was started to run Run_Alglib_Baselines. That input is a
diagnostic and is deliberately not in the fingerprint, so both charts resolved to the same
filename - the guard was correct and the message was not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The report has flagged f30 as mostly-zero (78%) on every member of every run
for days. Establishing what f30 actually IS took reconstructing the emission
order across three files, and I got it wrong on the first attempt - guessed
RSI, then MACD, both wrong because those feature blocks ship disabled.
It is spread[1], the spread CHANGE ratio, and 78% exact zeros is exactly what
that should read: the broker quotes the same spread on consecutive bars most
of the time, so the change is exactly 0. Benign, and it cost two wrong
answers to say so.
The report now names the block - "spread[1] (78%)" instead of "f30 (78%)".
The walk lists every block in the order BufferTempDataCompute emits them with
the widths Topology.mqh's m_neuronsCount sum declares, which makes this a
third place that has to stay in step with those two. So it does not stay in
step silently: the widths must total m_neuronsCount, and when they do not the
layout has drifted and every name past the drift point is wrong - so it
returns "f<slot>?" and names nothing rather than naming confidently and
incorrectly. A wrong name is worse than a bare index.
This is the same lesson as cb30360 (print the resource's IDENTITY, not just
its state), applied to the feature vector. The alt-block hint in the header
goes away with it - it existed to disambiguate one block, and every block is
disambiguated now.
Reporting only, no behaviour change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Neutral has been excluded from the logit adjustment since 2026-08-16 - offset
pinned to zero. That fix was right for its moment and it rested on two
justifications, one of which is now measured false.
The sound one: Neutral was then the RAREST class (10.61% on SP500 H4), so
including it SUBSIDISED abstention by 1.20 logits, and with no directional
edge to overcome that the model took the free lunch - OOS recall Buy:1%
Sell:0% Neutral:100%. The anti-collapse mechanism was the collapse.
The other: "abstention is already owned by m_dirConfThreshold". Era 1 of
2026-08-21 measured that directly, now that the operating point is fitted on
calibration and reports what it reaches. Three of four members drove the
threshold to 0.00 - no abstention filter at all - and STILL called a direction
on only 12.9-14.5% of bars against a 37.5% label rate. At 0.00 the threshold
owns nothing; the abstention is coming from the head's own argmax. So nothing
was correcting the Neutral rate, and the new CALIBRATION field shows the
result: all four members over-call Neutral 1.3-1.8x while under-calling Sell
0.0x-0.5x. CONV calls Sell on 1% of bars against a true rate of 25% - a flat
refusal to trade one whole side.
The sign has also flipped since that failure. Neutral is the DOMINANT class
now (62.5%), so including it PENALISES abstention rather than paying for it.
Rather than depend on that staying true, the invariant the old comment
STATED is now implemented literally instead of by proxy: Neutral's offset is
clamped at >= 0. It can be penalised when over-represented and can never be
boosted when rare. Pinning it to zero blocked both directions; this blocks
only the half that was ever harmful, and makes a return to the 2026-08-16
regime structurally safe rather than newly dangerous.
The cap is now sized on the full three-class spread so it bounds the real
offset range, and the log line says which way abstention is being pushed -
that being the question this correction has now got wrong in both directions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The margin threshold now sits where the model calls a direction as often as a
direction actually occurs. Nothing else.
WHY THE OLD OBJECTIVE HAD TO GO. It maximised `coverage x (precision -
breakEven)`, and this function's own comments were already the case against
it: over 98 consecutive fits of the shipped SP500 H4 model, correlation
between the chosen threshold and the win rate at it was -0.056, while the
era-to-era spread of that win rate (1.32pp) matched its own binomial SE
(1.25pp) to within 0.07pp. The margin does not rank trades. So the argmax
returned whichever of ~37 bins drew the luckiest sample, and the threshold
teleported 0.42 -> 0.04 -> 0.74 in three eras.
The response at the time was to build a null-of-the-maximum gate, an
effective-sample SE and a parsimony fallback to hold the noise down. All of
that is gone now, because fitting on calibration removes the problem instead
of bounding it: coverage is a ratio against a fixed denominator so it is well
determined at every bin, the target is a measured label rate rather than an
outcome, and nothing is maximised over a noisy curve so there is no best-of-N
to correct for. Net 174 lines out, 62 in.
It deliberately does not chase edge. It cannot - at ~0 measured edge no
operating point has more of it, and pretending otherwise is what produced a
threshold of 0.96 that still passed 60% of bars while the model called a
direction ~10x too often. The edge at the chosen point is still REPORTED,
just no longer what chooses it.
THREE READINGS OF ONE QUANTITY, AND THEY DISAGREE. "How often does a direction
occur" is measured in three places and gives ~7% (the scan's own tally), ~41%
(the era loop's counters, via this function's old coverage floor) and ~50%
(the ensemble gate's OOS base rate). They cannot all be right. Rather than
pick one silently, ScanDirectionalRatePct() and EraDirectionalRatePct() are
now named accessors, the fitter targets the SCAN - that is the tally the
operator reads, and the one "predict the labels as measured during the scan
phase" names - and the threshold line PRINTS BOTH every time it moves, so the
disagreement is on the record instead of buried in a derived floor.
The ensemble gate's own floor is deliberately NOT changed in this commit. If
the scan is right, a calibrated member covering ~7% of bars cannot clear a
12.4% floor and every model would fail the gate by construction; if the gate
is right, the scan tally is wrong. The CALIBRATION field added in 667f2bc
reports the OOS true class rates directly and settles it in one era - that
measurement comes first, and the floor follows it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
Found while costing whether to turn Run_Alglib_Baselines on. The suite builds
its design from BuildFeatureWindow() over DeriveHistoryBars() bars, and
neither reads any per-member state - symbol, period and feature toggles only.
So every member of an ensemble builds a byte-identical matrix and fits an
identical forest, MLP and OLS to it: four un-chunked fits over ~1000 columns
x 4000 rows, to print one answer four times. The MI suite has had a
once-per-chart gate for this exact shape since the ensemble landed; this
never got one.
Gated the same way, and here without the MI gate's caveat: that one has to
warn that the geometry scan at the end of its chain makes a DECISION, so a
donor-only run would leave the other members on a different target. This
suite decides nothing - nothing trades on it, no model is saved, and
ReportGeometryDrift assigns to nothing - so the donor's run is the complete
answer. Solo charts are unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
User request: "when an asymmetry is noticed in a market (like sp500 upward
drift) ... it does not need to predict shorts, but exit points. a sell signal
needs to be preceded by a buy so that it can say I predict we must close that
long."
Until now a LONG_ONLY verdict only BLOCKED short entries. The network went on
being trained to predict them - a third of its output capacity spent learning
an answer the direction policy guarantees it can never act on, while the
question the book actually faces (when to get out of the long) was never
asked. The two are not the same event: "a short pays" needs price to travel
the SHORT's target before the SHORT's stop, and at any geometry where reward
!= risk that is a different bar from "this long hits its stop first". The exit
is the second one.
So on a one-sided book TripleBarrierLabel re-cuts all three classes around the
only position the book can hold: Buy = it reaches its target, Sell = it
reaches its STOP first, Neutral = the horizon expired with it still open. Both
come off the allowed side's own barriers, which the walk already computed -
this reads longLost where it used to read shortWon, so it costs nothing.
Label lifespan and the timeout flag follow the allowed side too, so the
overlap correction is sized on the window this label actually spans.
DECIDED ONCE, AT ERA 0, AND PINNED. m_exitTargetSide goes in the .cfg beside
the derived geometry under the same doctrine and for the same reason: it
decides what Buy and Sell MEAN, and a target that moved mid-run would retrain
a fitted model against something it never saw. A .cfg from before this ends
early and reads 0/0 - "not decided, symmetric" - which is exactly what every
existing model was trained as, so nothing needs migrating. The weights
fingerprint keys on the INPUT only (explicit Long only / Short only); under
Intelligent the measured verdict must never reach a filename, or the model is
orphaned the moment more history downloads.
THE DRIFT VERDICT HAD TO MOVE OFF THE LABELS FIRST, and it turns out it was
measuring the wrong thing anyway. It counted m_labelCacheBuy/Sell and called
them "always-long vs always-short win rate", but the label pair is the
COLLAPSED first-touch verdict: a bar where both sides reached their target
carries only the side touched first, so long wins were undercounted by the
both-won-goes-to-short share. m_winLongCache/m_winShortCache are the actual
per-side win rates, published before the collapse, and that is what it reads
now. Necessary as well as more correct - deriving the verdict from labels the
verdict shapes is a feedback loop, since Sell-as-exit is near complementary
to Buy and would close the very gap that produced it. The gap's SE now leans
conservative rather than anti-conservative for the same reason.
LIVE. The retargeted class is wired to close the position, or training it
would be pointless: CheckClosePosition's "never vote-exit a certified
position" rule keeps governing symmetric books and gains a one-sided
exception, and the replay reads the identical rule through one
LiveVoteExitThreshold() so certified and traded cannot describe different
policies. Armed only when the operator picks a close threshold
(Signal_ThresholdClose ships Disabled) AND the model's own pin says its
blocked-side class means "close" - a model trained symmetric never fires it,
whatever the verdict has since become. This does trade a different game from
the one the win-rate certificate grades; the era's EXIT-POLICY REPLAY line
already reports expectancy in R for exactly this case and says so in words.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two UX changes the operator asked for.
THRESHOLDS. Signal_ThresholdOpen/Close were raw ints with the legal range
written in the label ("[0...100, 101 = never]") - the one input style this
codebase converted away from everywhere else. Open now takes the existing
PERCENTAGE_PRESETS, whose comment already declared itself to be "Signal_
ThresholdOpen's scale" but was never wired to it; Close takes a new
SIGNAL_CLOSE_PRESETS carrying the same rungs plus CLOSE_DISABLED = 101, which
is why it cannot just reuse the other enum. Member names are prefixed because
MQL5 enum members share ONE flat namespace - a bare PCT_25 in the second enum
would silently resolve to the first one's, warning only. Values are unchanged,
so existing .set files keep their settings. Both call sites now cast
explicitly at the CExpertSignal boundary rather than leaning on an implicit
enum-to-int conversion that only warns.
ARROWS. 2026-08-19 replaced the low/high arrows WITH trigger-price lines; that
was a swap where it should have been an addition, and it cost the zoomed-out
view. A mark is now both objects: the line is the precise entry/exit level,
the arrow off the candle's extreme is the finder that says there is something
here to zoom into. The arrow's name is the line's plus a suffix, so it stays
inside SIG_ARROW_PREFIX and every prefix-scoped purge already reaches it.
The two type-filtered sweeps had to widen or they would clear one half and
leave the other: the Hide/Show visibility loop and the pre-rescan scoped
delete both walked OBJ_TREND only. Both are typed-blind and prefix-scoped now
- the same widening this file's 2026-08-09 note describes, for the same reason
it gives. Deletes go through one WarriorDeleteSignalMark() so an arrow cannot
outlive the line it belongs to, and the sidecar deliberately still records one
row per mark off the line (the half carrying the price), with the restore
redrawing the pair.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The detector claimed "-1 means an INVALID HANDLE, 0 means created-but-never-
calculated". This run disproved it with our own instrumentation: the repair
line prints only when Create() RETURNED TRUE, and the depth it read
microseconds later was
BEFORE: MA=-1(h13) | AFTER: MA=-1(h13)
A freshly created, valid handle read -1 - the value the model says is
impossible for one. So BarsCalculated() < 0 does not mean "dead"; it also
covers "valid, not calculated yet", and the trigger cannot separate them.
Consequences, all fixed here:
- The AFTER depth was re-read synchronously, when it can only be -1 or 0, so
every repair looked like a failure and the line was unreadable either way.
It now reports the handle NUMBER across the recreate instead. A changed
number proves a new instance; SAME means MT5 handed back the same
refcounted one, so it was never dead.
- IndicatorDepthReport printed the handle number for MA alone. Every tunable
gets one now, through a single IndicatorDepthField() - nine near-identical
StringFormat calls collapse to one.
- The comment justifying "never release before re-creating" rested on the
claim just disproved. The decision stands, the reason is restated: given
the ambiguity, releasing is the dangerous half - a recycled number would
decrement whatever owns it now and CAUSE this outage - while re-creating a
live handle only leaks a reference on a path that fires a few times a
session.
The before-handle is captured on its own line, never as a sibling argument to
the Init* call: MQL5 does not define argument evaluation order.
Behaviour is otherwise unchanged - same trigger, same cooldown, same
recreate. Only what gets reported changed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ExpertSignalAIBase.mqh 4388 -> 4256 lines. Every edit verified the same
way: non-comment lines extracted before and after and diffed, so the
change is provably comments only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Box header per function/class, section banners, and 1-3 line notes for
variables - not the paragraphs that had accumulated. Both files verified
mechanically rather than by eye: every declaration in Inputs.mqh (159 of
them) and every non-comment line in ExpertSignalAIBase.mqh is byte-
identical before and after.
Variables/Inputs.mqh 881 -> 355 lines (704 -> 179 comment lines)
Expert/ExpertSignalAIBase.mqh 4795 -> 4388, first ~800 lines converted
What was cut is narration - the history of what a constant used to be,
paragraphs restating the line below them, and commentary about earlier
versions of the comment itself. What was kept is every constant, every
measured number, and the traps worth a warning.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
The control panel drove training by looping g_aiSignals[] - a
hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had
already dropped an ensemble member on the floor once (609be10). A model
missing from it still trains and still votes, it just cannot be paused,
stopped, deployed or reset, and every button label is computed from the
same short list, so the panel described one set of models while acting
on another. Classic signals could not respond to a panel action at all.
Commands now walk the signal tree CExpert already owns:
Expert.DispatchSignalCommand(cmd) -> root signal -> every filter,
recursively, returning how many actually acted.
CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait,
both no-ops by default), so a classic signal opts in by overriding two
methods and needs no registration and no cap. CExpertSignalAIBase
implements the training commands over its existing Pause/Stop/Deploy/
Reset methods - the behaviour is unchanged, only its reach reported.
Button labels ask the same tree via CountSignalTrait, with
SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is
meaningless without knowing how many could be paused. Pause/Stop resolve
their toggle direction ONCE in the EA and hand every model the same
plain command, instead of each re-deriving the direction from its own
local state - which is how a mixed set ends up half paused. The alerts
now report the count acted on rather than assuming it.
Two dispatch bugs found on the way, both from a database guard copied
onto event delivery: CExpertSignalCustom::OnTickHandler and
::OnChartEventHandler each skipped any filter whose GetFilterID() is
"NULL". That id is a DB folder name, and CSignalNewsFilter,
CSignalSessionFilter and CSignalRiskGuard never set one - so all three
were silently receiving neither ticks nor chart events. The guard stays
where it belongs, on the paths that write pattern tables.
ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include-
guarded) because the Expert bases have to name it and the panel is
included long after them.
The AI-only lifecycle loops - PollTraining, the weight autosave,
AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and
are untouched here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every AI signal repeated the same five-line InitIndicators override that
did nothing but call InitNeuralNetwork. The cause was an access mismatch,
not a design: CExpertSignalCustom declares InitIndicators public, the AI
base redeclared it PROTECTED, and each subclass had to redeclare it
public to be reachable by CExpert. Worse, the base's own override does a
different job entirely - it creates the OHLC/ZigZag feature indicators -
and InitNeuralNetwork called it back scope-qualified to stop the virtual
dispatch landing in the subclass. Two jobs, one virtual name, and a
recursion trap held off by a scope qualifier.
The feature-indicator step is now InitFeatureIndicators() (protected,
non-virtual, named for what it does) and the AI base carries the single
public InitIndicators override. CONV/HYBRID/LSTM/PAI/META drop their
copies and are now purely identity plus topology, which is the classic
signal file's shape.
Comment pass on ExpertSignalAIBase.mqh, -100 lines with every constant
and every measured number kept. Three claims in the tier block were
stale and inverted - it named CalibratedConfidenceMagnitude() as the
tiering input where the code deliberately uses the RAW magnitude, and it
described the signal DB as re-ranking each tier when ApplyPatternWeight
declines the DB from the end of era 1. Also dropped a paragraph whose
subject was a previous version of the comment, and moved two notes down
onto the constants they document (CONV_COMPRESSION_DIVISOR was 16 lines
and three unrelated defines away from its own text).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Three ALGLIB additions, all measurement-only and all under the existing
Run_Alglib_Baselines switch.
MinBLEIC COMBINING WEIGHTS. The live ensemble weights each member by its
own pooled holdout win rate - a defensible prior, but not a fit, and
nothing has ever asked what mixture minimises error on the bars the
members disagreed about. Two individually-mediocre members wrong in
different places can beat one individually better, and a per-member win
rate cannot express that because it never looks at them jointly.
Solved on the simplex (w >= 0, sum w = 1), which is exactly what
MinBLEIC is for. Non-negative because a negative weight asserts "trade
the opposite of this member", a claim ~60 effective observations cannot
support. Least squares on the signed outcome rather than precision:
precision is a STEP function of the threshold that no gradient method
can walk, and optimising a smooth proxy for a step decision is how
c3daded put every operating point 14pp underwater - so the result is
reported in BOTH currencies, the SSE it minimised and the directional
hit rate the mixture would actually have scored against the equal mix.
If the second does not improve, the first is noise.
This needed data that did not exist: g_ensVoteSum accumulates member
contributions and the sum destroys the decomposition, while
g_ensVoteVoterMask records only WHETHER a member voted, never what.
g_ensVoteMember[] keeps them unsummed. The live arithmetic is untouched.
ENS_MAX_MEMBERS is 8 and deliberately larger than MAX_AI_SIGNALS (5):
independent caps, over-allocating is free, and matching them would make
this array silently short the day the registry grows - a cap that has
already dropped a member once without saying so.
MLPKFoldCVLBFGS. Every baseline row carries a binomial SE, which is the
sampling error of SCORING a fixed model and says nothing about how much
the FIT moves. One LBFGS run from one random start can land anywhere,
and a baseline that cleared or missed the bar on luck of initialisation
reads exactly like one that did it on merit. 3 folds, because each is a
full retrain. LBFGS not LM - LM builds a Hessian over ~7,700 weights.
CCorr ALL-LAGS PROFILE. Added BESIDE the MI lag profile, not instead:
MI catches nonlinear dependence and is the stronger negative, which is
why it settled the verdict - but its per-lag permutation null limits it
to ~20 lags. FFT correlation gets every lag in one O(n log n) pass, so
linear structure parked at lag 300 would surface for free. Different
question, not a replacement. Walks CONTIGUOUS bars, unlike everything
else in this file, because a lag index is meaningless otherwise; both
series are mean-centred first since CorrR1D is a raw sum of products;
and the max over columns x lags is judged against a Sidak family of
exactly that size, not a bare 2-sigma line.
fasttransforms.mqh needed its own include - verified that none of
ap/optimization/statistics/solvers/linalg reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Asked whether ALGLIB's perceptron should replace ours "only if it is
better". This measures that instead of assuming it, and the measurement
is worth more than the swap would have been.
The forest and the linear fit test whether the MATRIX carries direction.
Neither tests whether OUR CODE is what fails. ALGLIB's MLP does: it is a
long-standing independent implementation of roughly what our own dense
stack computes, trained on the identical rows through the identical
gate. That distinction is not hypothetical here - two silent
implementation faults have already invalidated every model-based
negative behind them (a transposed dense hidden gradient, so backprop
was never backprop; and Adam storing sqrt(v) and feeding it back as
variance, so every "Adam" run was SGD). If alglib-mlp clears a bar our
own net cannot on the same rows, the fault is in our code.
Trained BEFORE the target column is rewritten for the linear fit:
MLPCreateC1 builds a softmax classifier whose trainer wants the class
index, which is what the matrix still holds at that point. Scored by
argmax with Neutral abstaining, exactly as the forest is. Sidak family
is now 3, not 2.
Deliberately NOT a replacement, and the gap is not close: ALGLIB's MLP
is feed-forward only (no conv, no recurrence - so it could not stand in
for three of the four members at all), and it has no batch norm, no
logit-adjusted loss for the class imbalance, no era resumption, no
OpenCL or CPU-DLL backend and no .nnw. Adopting it would trade a
maintained architecture for a control.
Kept small on purpose - 8 hidden units, 1 restart, 100 iterations, decay
at the 0.001 the ALGLIB docs recommend when you have no reason to pick
another. A wide net would spend the six-core box's hours answering a
question about capacity when the question is about implementation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
DeriveBarrierGeometry() reads the stop off a quantile of the adverse
excursions in the IS region ONLY - correctly, since a geometry chosen
with the holdout in view has used the holdout for selection and it stops
being a holdout. The cost of that correct choice is that nothing ever
checked whether the distribution it measured still holds on the bars the
model actually trades.
If adverse excursions run wider in the OOS window than in the IS region,
the derived stop is too tight for the market it is used in, every label
was cut on the wrong geometry, and the deploy gate certified a game the
trade is not playing - the 2026-08-09 geometry mismatch arriving through
drift rather than through a config error.
Reported in TWO currencies deliberately. A rank-test p says whether the
distributions differ; it does not say whether anyone should care. The
stop each half's own quantile would derive says exactly that, in ATR
multiples - the units the order is placed in. A significant p with both
stops on the same ladder rung is a curiosity; half an ATR of movement is
a problem whether or not it clears 0.05.
Declustered first, same as the regime test: overlapping labels are not
independent draws. Harvest guards are copied from the deriver's own so
the two describe the same sample - and the split is verified identical
(totalIter == bars - historyBars, so Train's oosCutoff and the deriver's
are the same number).
Runs before the two model fits and needs only the excursion caches: a
chart too thin to fit a forest can still have drifted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MQL5's MathRand() is the 15-bit MSVC LCG - 32768 distinct values and
the lattice structure that shape of generator has. Two places here
actually lean on randomness and both were hurt by it:
WEIGHT INIT. Six He/LeCun-uniform sites drew
((MathRand()+1)/32768.0 - 0.5) * 2 * scale, so a first dense layer of
~250k weights had only 32768 possible values and thousands of
connections started byte-identical. Breaking that symmetry is the whole
job of random init.
SHUFFLING. ShuffleRandomIndex() already had to splice TWO MathRand()
draws to reach 30 bits, and its own comment documented the residual
modulo bias it still carried. HQRndUniformI() is rejection-sampled and
exactly uniform, so the splice and the bias note both go.
CHighQualityRand is L'Ecuyer's combined multiplicative congruential
generator - two differenced streams, 31-bit output, period ~2.3e18 -
and it ships with the terminal.
AND A BUG THE MIGRATION EXPOSED. The three MathSrand(GetTickCount())
calls sit immediately before "build a fresh topology", once per model.
GetTickCount() steps in ~15.6 ms on Windows and an ensemble builds every
member inside one OnInit, so members could be handed the SAME seed and
draw the SAME weights wherever their shapes coincide - and members that
start identical are not an ensemble. WarriorRandSeed() takes a salt (the
model id) plus a never-reset call counter, so a collision is impossible
rather than merely unlikely, while the tick keeps the run itself
genuinely unrepeatable the way those call sites asked for.
Seeds are masked positive rather than trusted: HQRndSeed computes
s % (M-1) + 1 and MQL5's % keeps the sign, so a negative seed leaves the
generator in a state its own assertions reject. GetTickCount() is a uint
and goes negative as an int after ~24 days of uptime - a fault that
would surface as "training is broken" on a long-running terminal and
nowhere else.
The indicator tuner's 52 draws move across too: its random search is
where sample quality earns its keep.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two ALGLIB diagnostics on the pass that already builds the matrix, under
the same Run_Alglib_Baselines switch, so they cost nothing by default
and share one walk of the feature pipeline.
REDUNDANCY (CBaseStat::PearsonCorrM + CPCAnalysis::PCABuildBasis).
The alt-data block already produced this finding once, by hand: 13
features x 16 window slots = 208 inputs spanning ~11.5 effective
dimensions, and the cost was not wasted parameters but a gradient
weighting bias, since batch norm rescales collinear copies without
decorrelating them. This generalises that measurement to every column
instead of the one block someone thought to check.
Run on the ANCHOR BAR'S ROW, not the full window. The window is
m_historyBars near-copies of the same columns at different lags, so
per-bar redundancy is the question worth asking - the lag structure is
what the conv/LSTM stage exists to exploit - and it keeps the eigensolve
at ~60x60 rather than ~960x960.
REGIME STABILITY (CMannWhitneyU::CMannWhitneyUTest) on the net's own
signed margin: its conviction, signed by whether the conviction paid,
older half of the OOS window against newer.
DECLUSTERED FIRST, which is the whole reason this is not a one-line
call. Triple-barrier labels overlap, so consecutive scored bars share
the price action that resolved them and are not independent draws.
Feeding all of them to a rank test yields a p computed on an n it does
not have - the same error label overlap invalidated everywhere else
here. One sample per label horizon costs most of the rows and buys a p
that means something; when too few survive, it says so and declines.
Both report and neither acts. Wiring the regime flag to position sizing
is the obvious next step and the one to take only after watching the
number: run every era on ~60 effective observations, a 0.05 test crosses
by chance regularly, and an auto-derisk on that is a random position
size generator.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
InitNeuralNetwork() was 673 lines and the fingerprint assembly - the
single most audited block in the file, since its hash decides when a
trained model may be resumed and when it starts again from era 0 - sat
in the middle of it with no name of its own.
BuildModelFingerprint() is now that block, moved line for line. Its
header states the two rules the per-field notes have been repeating one
at a time for months: measured quantities never enter (the .cfg carries
those, adopt-don't-compare), and new fields append conditionally so
shipping one does not re-key models that never use the feature.
The assembly is byte-identical - verified by diffing every `fp =`/`fp +=`
line against HEAD, which differ only by the new call site. So no
existing .nnw/.cfg re-keys and nothing retrains.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Beta-prior arithmetic that turns counts into a ranking weight was
written twice, term for term: WinRateFromCounts() for the classic
pattern ladders and RankTiersFromOos() for the AI confidence tiers.
Same formula, two transcriptions, and the same class of duplication the
binomial SE consolidation removed a few commits ago.
ShrunkRatePct() in System\BinomialStats.mqh is now the only copy. The
two call sites keep what genuinely differs - the classic path passes RAW
trade counts with a prior of MIN_TRADES_FOR_WIN_RATE, the AI path passes
OVERLAP-CORRECTED effective counts with TIER_PRIOR_EFF_N, which is far
smaller precisely because effective counts are - and that contract is
now stated once, in the function, instead of being implied by two
comments that could drift apart.
Also fixes a difference the consolidation exposed: with an empty sample
and a prior present, the posterior mean IS the prior, and returning 0
there would have handed a tier a vote weight of zero on no evidence.
The AI path could reach that (effN can round to 0 when labels overlap
heavily); the classic path cannot, since it returns NO_DATA_WIN_RATE
first.
Corrects a stale note of my own in passing: this ranking was recorded as
a "raw win rate behind a MIN_TRADES cutoff heuristic". It is not, and
has not been for some time - it is already a proper empirical-Bayes
estimator with a per-filter pooled prior. Replacing it with a
significance test, as that note implied, would have swapped the
estimator the weight needs for a gate answering a different question.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
First of the AI vote layers to go. CheckClosePosition had two exit routes:
the stock blended vote, and an AI-only one reading the AI members' sub-vote
undiluted. The second existed because an AI reversal averaged in with the
classic filters could be diluted below the threshold before it could close
a position.
It is gone, and with it m_lastAiVote and the aiResult/aiWeightSum pair
Direction() carried to feed it.
This CLOSES the certified-vs-traded gap rather than widening it. The
deploy gate certifies a win rate measured on hold-to-resolution outcomes,
and CheckClosePosition already gated the blended route off whenever an AI
model's derived geometry was on the order - so the AI route was the only
vote exit an AI-certified trade could take, and the exit replay existed to
reproduce it. With it removed, an AI-certified position holds to its
barrier by construction instead of by reconstruction, so Warrior_EA.mq5
now pushes ExitPolicy(0.0, true) unconditionally. Previously it forwarded
Min_Vote_Close and relied on Disabled arriving as 1.01 to switch the
simulated exit off by arithmetic - correct at the shipped default, and one
input change away from the simulation and the live path describing
different games.
Min_Vote_Close keeps its meaning for the classic route and is now
documented as inert wherever an AI certificate governs, rather than
appearing to drive an exit it can no longer reach.
Comment debt cleared while here: a tombstone block for m_ai_exit_threshold
(a member deleted 2026-08-18) still sat in the header, and four sites still
named LiveSignedConfidence's "two consumers" - it had one, the intelligent
trailing stop, since that same date.
NOT touched, and deliberately: NMS declustering is NOT a quality layer. It
gates the live signal at Inference.mqh:226 (NmsLiveAccept), and the
undeclustered population is ~8x what the EA trades. Removing it would
multiply live position count, not simplify a scoring path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The codebase had THREE conventions for the same statistic. AltData took a
true median; the barrier horizon and the derived input window took the
upper of the two middle values; the MI terciles and the barrier stop
ladder used nearest-rank indexing. All four now go through MathMedian /
MathQuantile, which is R's type 7 and the library's one answer.
System\AltData.mqh column median -> MathMedian (exact, no change)
AIBase\Labels.mqh swing median -> MathMedian
leg-range med -> MathMedian
stop ladder -> MathQuantile, read in one call
AIBase\Topology.mqh window median -> MathMedian
AIBase\AutoTune.mqh MI terciles -> MathQuantile + MathMin/MathMax
Signals\SignalSessionFilter DST last Sunday-> CDateTime::DaysInMonth()
gaps[]/legs[] change from int to double so MathMedian can read them; the
values are bar counts either way.
VALUES MOVE. Even-sample medians shift by half a bin and the quantile
reads interpolate, so the barrier geometry and the derived input window
can land on different rungs - re-keying fingerprints and forcing a
retrain. Accepted deliberately: stdlib consistency was the ask, and three
private conventions for one statistic is what it buys out.
Two YAGNI finds fell out of the ladder rewrite. MathQuantile sorts its own
copy, so DeriveBarrierGeometry no longer sorts up[]/dn[] in place - which
means upUnsorted[], a full array copy kept only to undo that sort, is
gone. ArraySort(up) had no consumer needing order at all; it was pure
work. The library call also gets a failure guard the hand-rolled indexing
never needed but the ladder read does.
Verified while here: Math\Stat\Math.mqh's MathAbs/MathMax/MathSqrt/MathPow
and friends are ARRAY overloads, not scalar redefinitions, so pulling it
into the translation unit shadows no builtin.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>