forked from animatedread/Warrior_EA
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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 (
|
||
|
|
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> |
||
|
|
b91c7b1f7a |
refactor(comments): box headers to stdlib length
The //| box blocks were excluded from |
||
|
|
5efdb48de4 |
refactor(comments): stdlib comment style across the remaining in-scope files
Same pass as
|
||
|
|
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
|
||
|
|
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 ( |
||
|
|
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>
|
||
|
|
2c19cf8408 |
fix(calibration): the field reported the raw argmax, which is not what trades
The CALIBRATION field added in
|
||
|
|
667f2bcb6b |
revert(labels): drop the one-sided exit target; measure the calibration drift instead
Reverts |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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
|
||
|
|
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
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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 |
||
|
|
b28c81eb78 |
feat(rank): AI models rank their own confidence tiers from held-out outcomes
Closes the caveat
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
94019f363e |
feat(gate): grade OOS calls on the exit policy actually in force, and move vote combining out of the members and into the orchestrator
Option (a) from the exit-policy question: the certified number must be the traded number. Plus the modularity correction the user called for on |
||
|
|
778b6c09c6 |
feat(gates): derive the recall floor instead of configuring it, publish what the configuration can PROVE, and stop an arbitrary member driving live exits
Three changes, all from the same principle: measure what is there before aiming
at it, and never certify a number you do not trade.
1. THE RECALL FLOOR IS DERIVED, AND IT MOVES BELOW CHANCE.
MinRecall=40 was a constant doing a statistical job. Its reference point is the
33.3% recall a zero-skill 3-class model gets on EVERY class, and against that the
constant was accidentally calibrated for exactly one sample size: on USDJPY CONV
(n_eff 195) 40% is chance + 2.0 SE; on SP500 PAI (n_eff 42) the same 40% is chance
+ 0.9 SE. One chart was being held to a bar twice as strict as the other, for no
reason anyone chose.
CollapseRecallFloorPct() computes it per class from that class's own effective
sample - EffectiveSampleSize(), so the overlap deflation the rest of the gates use
applies here too - as chance - EDGE_MIN_SIGMAS x SE. 26.5% at n_eff 195, 18.8% at
n_eff 42.
BELOW chance, deliberately, and this is the substantive change rather than the
arithmetic. This gate's only job is refusing to call a COLLAPSED model converged.
It is not a quality bar; the deploy gate is the quality bar and it is already
rigorous (chance + 2 SE on the deflated sample, Sidak over candidate eras, then the
cross-instrument pooled certificate). A convergence gate that ALSO demands
provably-above-chance recall on all three classes double-counts that job, and it
has failed that way twice here: MinRecall=60 blocked every SP500 H1 run in 2026-07,
and the 40 that replaced it made Neutral structurally unreachable once first-touch
resolution cut Neutral to a 0.65% residue. A floor nothing can reach does not make
a funded account safer, it stops the run converging at all.
Testing significantly BELOW chance instead catches what a fixed 40 was actually
catching - a model that has stopped emitting a class - and cannot become
unreachable by construction. It also fixes the direction the old constant scaled:
it now widens on a thin OOS window, where low recall genuinely cannot be told from
noise, and tightens on a rich one. Today's SP500 PAI (Buy 51 / Sell 18 / Neutral 30)
is still correctly blocked on Sell.
This also resolves a standing contradiction the code half-admitted at the
isBetterEra comment: selection ranks on coverage-weighted PRECISION while
convergence gated on RECALL, so a sparse high-precision abstainer - precisely the
model that could clear the deploy bar - was blocked by the floor.
The era line now PRINTS the derived floor. Anyone comparing these recalls against a
remembered "40" is reading the wrong bar.
2. DETECTABILITY: WHAT THIS CONFIGURATION COULD PROVE, BEFORE IT TRAINS.
The DEPLOY BAR line states the bar. It never said what reaching it would take, and
that is the actionable direction. ReportDetectability() inverts the same identity -
the gate passes when edge >= z x sqrt(p(1-p)/n_eff), so certifying an edge d needs
n_eff >= z^2 p(1-p)/d^2 independent calls, hence L times as many raw ones - and
prints a +2 / +5 / +10pp ladder as required independent calls, raw calls, and share
of the OOS window, marking any rung that needs more than the window holds IMPOSSIBLE.
Every term is a property of the CONFIGURATION - geometry via break-even, horizon via
mean label lifespan, window via oosCutoff - so no amount of training moves any of
them. It fires once, at the first healthy sweep, beside ReportFeatureHealth, for the
same reason: that is the first moment the bar grid, the measured geometry and the
lifespan are real numbers rather than defaults. It gates nothing.
This is the EdgeFinder discipline applied to our own gate: establish what the market
and the measurement design have to offer, then point the net at it - rather than
spending a thousand eras chasing something this OOS window could never certify.
3. AN ARBITRARY MEMBER WAS DRIVING LIVE EXITS AND TRAILING (user-identified).
Every ensemble member ran
g_LiveAISignedConfidence = SignedAIConfidence();
unconditionally, every tick. Last writer wins. Its consumers are the AI early-exit
route (CExpertSignalCustom::LiveSignedConfidence) and TrailingIntelligent - so on a
four-model chart an LSTM entry could be closed, and its stop moved, on the
Perceptron's opinion alone, decided by scheduling order. Not the vote, not a
weighted blend.
Now the mean across registered members, matching how the ensemble actually trades:
the open decision is the weighted-average vote, and an abstaining member contributes
0 and dilutes exactly as it does there. Members still training read 0, so a
half-trained ensemble reads WEAKER rather than louder - the safe direction for an
exit trigger. Deployed and paused members are included, which is the opposite of the
era barrier's exemption rule and correct for the opposite reason: that one asks who
must be waited for, this asks who has an opinion.
Latent today and staying that way for now by choice - Min_Vote_Close ships Disabled
(101, unreachable on both scales it drives) and TrailingStrategy is off, so live
exits are SL/TP only and the certified hold-to-barrier win rate is what actually
gets traded. Fixed now precisely because the plan is to enable vote exits once the
models are accurate, at which point a scheduling-order exit would be both harmful
and very hard to see.
STILL OPEN, and needs a decision before vote exits go on: the member gate and the
ensemble vote gate both grade hold-to-barrier, so enabling vote exits makes the
certified number stop describing the traded one. Warrior_EA.mq5 currently argues
barrier models may keep vote exits because "their label IS the vote's own horizon" -
that does not hold, since a vote flip at bar 5 of a 64-bar horizon is not the
target-before-stop outcome the gate measured. Either grade the OOS call on the real
exit rule (first of SL / TP / vote-flip / horizon) through the fill engine, or set
HoldToBarrier for ensemble members so the policy cannot drift from the certificate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
be396749fc |
fix(ensemble+depth): the barrier had no liveness escape, and the depth gate could not report the one state the evidence pointed at
Two charts (USDJPY 50,179 bars / XAUUSD 33,982) sat at era 0 for 38 minutes with
four of their eight members completely silent. Nothing in this commit guesses at
why the sweep fails - the last five guesses were all wrong. It makes the failure
say what it is, and stops one broken member taking its whole chart down with it.
WHAT THE LOG ACTUALLY SAYS, before any of this.
- The running build IS
|
||
|
|
45c9e211b3 |
feat(depth): prime -> settle -> sweep, and name which handle is short
"Max bars in chart" is set to Unlimited, so the static-terminal-limit reading in |
||
|
|
7e63a8be01 |
fix(depth): route EVERY ResizeBuffers call site through one indicator-depth gate
|
||
|
|
1dda479261 |
fix(train): clamp the sweep to indicator-servable depth - the scan wall was CopyBuffer, not a cold indicator
Symptom: on a 3-chart run with contention ruled out (SP500 sitting at era 2552),
USDJPY and XAUUSD produced 0 usable windows out of 50,163 and 33,966 - forever,
re-sweeping on every discard, which is the panel oscillating 0->100%.
Bars() is the PRICE series depth. A CUSTOM indicator's is not: MT5 calculates it
in its own context bounded by "Max bars in chart" (TERMINAL_MAXBARS), and
CopyBuffer past that limit does not short-read, it FAILS - so CDoubleBuffer keeps
nothing and EVERY index answers EMPTY_VALUE. ADMovingAverage is the only custom
indicator whose feature block REJECTS on EMPTY_VALUE (ADZigZag, also CiCustom,
neutral-fills; RSI/MACD/Ichimoku/ATR are built-ins served at any depth), so the
sweep died on feature 25 of every bar while the 24 price features under it were
fine. That is exactly the "window had 24 of 832 values" the stall report named.
Perfectly depth-correlated, measured 2026-08-17:
SP500 16,234 bars -> era 2552 XAUUSD 33,982 -> 0 windows
XTIUSD 16,611 bars -> era 71 USDJPY 50,179 -> 0 windows
This RETIRES the 2026-08-17 cold-indicator reading of the same stall.
|
||
|
|
1cf4c57d57 |
fix(altdata): median-fill instead of zero-fill, and a one-shot feature-vector autopsy
ALT-DATA AUDIT. The files themselves are healthy - all six symbols, 6,073 daily
rows, 2010-01-01 to 2026-08-17, no constant or degenerate columns, sane tails
(mac_cpi/mac_unemp flat ~47d is monthly data behaving correctly). The problem is
not the data, it is what happens where the data ISN'T.
CAltDataPanel::Features() returned an all-ZERO vector for any bar older than the
file's first row, and left blank cells at 0 too. Both were deliberate ('the block
is additive context and must degrade, never reject the bar') and that reasoning
holds for the CHANGE columns - but half these features are LEVELS: vix, ivol,
mac_y10, mac_cpi, mac_unemp, eia_util. For a level, 0 is not a missing reading,
it is an impossible one far outside the series' range. VIX does not visit zero.
And the spike lands in exactly the wrong place. Every alt file starts 2010-01-01
while the charts run far deeper - USDJPY H4 reaches ~1994, roughly HALF its
history - so 'alt block is all zeros' is precisely the predicate 'this bar is
older than 2010'. The IS/OOS split is chronological, so that predicate covers
~half of IS and none of OOS: an in-sample feature guaranteed to be useless
out-of-sample, and a bimodal input for the first BatchNorm to normalise. Not a
lookahead leak - a distribution corruption, which is quieter and was never
reported anywhere.
Now filled with the column MEDIAN over the covered range. A constant cannot leak
whatever its source - it takes the same value on every pre-coverage bar, so it
carries no information about which of those bars won - which is what makes a
median computed over later data legitimate here. Median not mean because the
series are skewed. Blank cells get the same treatment (eia_stk_idx1y alone has
181 blanks in 6,073 rows) and the count is now logged at load.
THE BACKOFF WAS ALREADY THERE AND WAS DEAD. Training.mqh arms m_coldSweepTick on
m_featureFailTransient, but only the open/ATR guards ever set that flag, so
f0cf659's cold ADMovingAverage looked PERMANENT and the sweep re-ran at full
speed forever. Setting the flag in the indicator guards revives the mechanism
that was already designed for this; no second backoff was needed and the one I
first wrote has been removed in favour of it.
SELF-HEALING, as asked. ReportFeatureHealth() runs once, the first time pass 1
produces usable windows, samples ~400 bars spread across the whole training range
and names every feature slot that is CONSTANT or mostly-zero, tagging alt-block
slots as alt[i]. Both of today's failures were the same shape - a block silently
produces nothing while every downstream number stays plausible - and neither an
accuracy figure nor a model can tell 'this feature is always 0' from 'this
feature is genuinely 0 here'. Evenly spaced sampling so a block that dies only in
deep history is caught as surely as one dead everywhere. A report, not a gate:
a rare-flag feature can be legitimately constant, and refusing to train would
turn a diagnostic into an outage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
f0cf659945 |
fix(features): a cold indicator is TRANSIENT, not a permanent miss - and back off instead of re-sweeping
Six fresh instances on USDJPY and XAUUSD swept 33,965-50,162 bars and produced
ZERO usable feature windows, repeatedly, for 40 minutes and 239 stall reports,
without ever completing era 0. The four instances already warmed up before those
charts were attached trained normally throughout.
THE STALL REPORT NAMED THE SPOT EXACTLY: 'lookback slot 0 REJECTED the bar
(window had 24 of 832 values)', and 24 is the core block to the value - 4 price +
5 swing + 4 range + 4 volume + 6 time + 1 ATR. So feature 25 was the wall, and
feature 25 is the first value of the MA block. The same 24 appeared on XAUUSD
against a 816-value window (51 features/bar vs 52), which is what ruled out any
symbol-specific data gap: the wall sits at a fixed feature index, not a date.
ADMovingAverage is a CUSTOM indicator, so MT5 fills its buffer asynchronously and
returns EMPTY_VALUE for EVERY index until it has calculated - not just the
warm-up tail. That guard did not set m_featureFailTransient, so every bar of the
sweep was cached as a PERMANENT miss. This is precisely the failure the ATR guard
twenty lines above it was fixed for on 2026-08-10; the fix was never propagated
to the indicator blocks that follow. RSI, MACD and Ichimoku had the same defect
and are fixed too. (The Donchian high/low guard is a break into a
degraded-but-usable path, not a rejection, and is deliberately left alone.)
IT ALSO SELF-SUSTAINED, which is why it never recovered. The ok=0 self-heal drops
the feature cache and re-sweeps immediately, so each stuck instance spent every
millisecond re-reading 30-50k bars - six of them at once, on a six-core box,
competing for CPU with the very indicator calculation they were all waiting on.
The recovery was preventing the recovery. A transient total failure now re-arms
m_warmupPassesRemaining, yielding the CPU for a few separately-scheduled Train()
calls - the same mechanism a fresh model already uses to let history sync finish,
pointed at indicator warm-up instead.
Verified in the terminal journal first: indicators load and unload in matched
counts and there is no OOM, so this is NOT the
|
||
|
|
d87f7d88ff |
feat(gate): cross-instrument pooled certification
The deploy bottleneck is CERTIFICATION, not training. A 4,738-bar OOS window at L=75.6 holds ~63 independent observations; certifying a 3pp edge at 2 sigma needs ~1,036. More bars of the same symbol barely help - they overlap. Other symbols do not. WHAT POOLS. Not win rates: symbols have different derived geometries, different break-evens and different drifts, so averaging raw rates across them is meaningless. What pools is each symbol's EXCESS OVER ITS OWN CHANCE RATE, combined by inverse-variance weighting (fixed-effects meta-analysis). Each symbol keeps its own model, geometry and chance rate; only the evidence is combined. THE CORRELATION PROBLEM, bracketed rather than assumed away. SP500 and NAS100 are ~0.9 correlated and pooling them as independent inflates the evidence. Nothing here can measure that without sharing return series, so instead of guessing a correction the gate reports both ends: SE_INDEP = sqrt(1/SUM(1/var_i)) all members independent SE_CORR = SUM(w_i * sqrt(var_i)) all members perfectly correlated The truth is always between. THE GATE USES SE_CORR, so a pass cannot be an artifact of correlated instruments - that bound already assumes the worst. The ratio is logged as the diversification credit the gate declines to claim, so the cost of that conservatism is visible instead of hidden. SCOPE, deliberately limited: the pooled result is REPORTED, never folded into tradeableOK. The local gate certifies the model that actually trades this symbol; the pool answers the different question of whether the strategy has an edge at all. Letting a cross-symbol result license a local deploy would ship a model that never cleared its own bar - so it cannot. Mechanics: one file per instrument (no concurrent-write path to get wrong), every FileOpen carrying FILE_SHARE_READ|FILE_SHARE_WRITE, records skipped rather than reinterpreted on a version mismatch, 12h staleness cutoff so a stopped chart cannot vote, and pooling refused below 3 instruments. Poolability requires matching timeframe and ratio; differing SYMBOL is the entire point. Publishing is unconditional - a pool that only hears from winners is a selection effect, not a meta-analysis. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |