forked from animatedread/Warrior_EA
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
053d704a84 |
refactor(chart): ChartUI is a real collaborator, not a raw-include partial (S2)
Expert/AIBase/ChartUI.mqh was 869 lines of method bodies of CExpertSignalAIBase, #included after the class declaration - free to touch any of its ~500 members. First of the eleven AIBase/*.mqh partials to come out (fewest inbound edges - see the SOLID campaign session order), using the same view+adapter shape already proven for CTrainingDataView. CChartView (Expert/Chart/IChartView.mqh) is the abstract read/behaviour surface a chart-rendering collaborator needs - identity, bar/model access, the prediction cache, and the training/vote/meta scalars the panel and HUD line summarise. CAIBaseChartView is the adapter the signal owns and binds to itself (MQL5 gives a class exactly one base, so CExpertSignalAIBase cannot implement the view directly). CChartUI is the real collaborator: it owns the arrow-restore queue, the rescan queue/tally, the last-arrows-saved count and the purge-mismatch latch as its own fields (verified via grep to be touched nowhere else in Expert/), and reaches everything else - including StartChartSignalRescan, moved in from its old inline home in the header since it drives the exact same rescan state machine AdvanceChartSignalRescan drains - through the view. m_arrowSignalCache and m_signalClusterWindow stay on the signal: Training.mqh writes the cache directly every era and the training-data view already reads it, so moving it would mean rewriting Training.mqh's write sites too - out of scope here. CChartUI reaches it through four bounds-checked accessors instead of a raw member poke. All 10 public methods keep their exact signatures and become one-line forwards on the signal, so no other file's call sites change except Training.mqh's one era-end status refresh, which now reads RefreshStatusLabel() rather than reaching into CChartUI's now-private last-displayed-neuron cache directly. Verified structurally, not compiled (never compile - the operator does, in MetaEditor): brace balance checked on every touched/new file against HEAD, and the view/adapter/impl method lists cross-diffed to confirm all 59 accessors match 1:1 across the interface, the adapter declaration and the adapter body. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
8c64ec7018 |
refactor(meta): the signal tree owns its gate - no global
g_warriorMetaGate was a file-scope mutable pointer, and it did not need to be. The root CExpertSignalCustom - the one CExpert actually calls CheckOpenLong/Short on - now holds the gate as a member, and children reach it through a parent back-pointer AddFilter sets on adoption. That was the last piece of the meta veto that behaved like ambient state: - CheckOpenPosition reads MetaGate() instead of a global. - EnsembleEraVerdict's replay reads the same MetaGate(). It sits deep in the training code inside an AI filter and had no route up the tree; a global WAS that route. m_parentSignal is now, and a back-pointer is safe for the same reason the gate adapter's owner pointer is - m_filters and m_gates free their children, so a parent always outlives them. - The stale-pointer hazard is gone by construction. The global had to be hand-cleared at every re-init because an input change re-enters OnInit in the same program instance and frees the old head; the root signal is new'd fresh each time, so nothing survives one. That reset line is deleted, not moved. Note what did NOT need doing: the tree already owned the meta head itself. AddFilter routes non-voters into m_gates, so it has been a gate child of the root since the S3 wiring - it was only the VETO that lived outside. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
303c9bf412 |
refactor(meta): the era's candidates are an object, not eight base members
Five parallel arrays, a count and a two-array intrusive chain sat on
CExpertSignalAIBase - inherited by every direction model, filled and read
by exactly one subclass. CMetaCandidateStore takes all eight.
What that fixes beyond the clutter:
- THE CHAIN WAS LINKED BY HAND. MetaPrepareEra wrote next[id] = head[bar]
then head[bar] = id itself, after six ArrayResize calls it also wrote out
itself. Add() does the linking, Reset() does the sizing, and a bar off
the grid now cannot be stored at all rather than stored unreachable.
- THE BOUNDS TEST HAD FOUR SITES AND THREE IMPLEMENTATIONS.
MetaCandidateWon indexed side[] with no test at all and answered
"short" for any id out of range - the same shape as the ladder's
negative-index read (
|
||
|
|
3d2ee517ca |
refactor(meta): the veto is a gate, not a virtual every signal carries
Since S3 (
|
||
|
|
11006a8e38 |
fix(train): BeginTrainRun read Train()'s parameter from a scope it no longer had
The run-start block calls TrainWindowStart(StartTrainBar), and StartTrainBar is Train()'s parameter. Moving the block into its own method left the read behind. Now passed explicitly. THIRD TIME THIS FAMILY HAS BILLED THIS SESSION, and the third distinct sub-shape: |
||
|
|
4e508460ac |
refactor(train): Train() is the era lifecycle again, not the whole of it
Train() was 1,273 lines. It is now 79, of which about 35 are statements, and they read as what the function is: preempt, begin run, begin era, four passes, advance, complete, report, finalize. Seven methods carry what left it: TrainCallPreempted 107 six ways this call is not a training call at all BeginTrainRun 130 once per run - history sync, window, one-shot walks BeginEra 232 once per era, or resume a chunk that yielded ReportPass1Outcome 105 what pass 1 found, said out loud AdvanceEra 68 count the era, decide whether the RUN ends CompleteEra 590 calibrate, gate, rank, checkpoint, ladders, persist ReportBarrierHold 62 why this member is idle at the era barrier ClaimCallForWalk 15 the preamble the three exclusive walks shared TWO DRY FIXES fell out rather than being looked for. The three exclusive walks each had to tell TWO watchdogs the same thing - the stall reporter which branch is running, the era-barrier watchdog that this member is BUSY rather than stuck - written out three times, so a fourth walk was three chances to be added with only one of them. And the barrier-hold reporting was 44 lines inline in a branch whose only other statement was resetting a tick. CompleteEra is lifted WHOLE and stays that way for now. Its parts share thirty-odd locals - the recalls, the gate verdict, the better/worse flags - and threading those through three signatures would recreate exactly the eight-locals-across-four-passes problem STrainEra was built to end. Splitting it needs an era-outcome object first, not more parameters. VERIFIED AS A PURE MOVE: statement multisets, old file vs new, differ only by the 14 `return;` that became 17 `return true;` plus 3 new returns at the call sites, the 3 collapsed walk preambles, the 8 new signatures and their braces. Nothing else moved, and every function closes at depth 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d7469c69a1 |
fix(gate): a field renamed on the definition side left one reader behind
SDeployVerdict's bothSidesLive became twoSided when the member and ensemble
gates were unified, and the member call site kept reading gate.bothSidesLive.
SECOND TIME THIS EXACT SHAPE HAS BILLED THIS SESSION - the first was `s == 0`
surviving the deletion of the loop that declared `s` (
|
||
|
|
3ea2bbc015 |
refactor(gate): the member gate and the ensemble gate were one rule written twice
SDeployVerdict::EvaluateRates() is now the deploy arithmetic - coverage
floor, chance + EDGE_MIN_SIGMAS x SE, tradeability, and the coverage-
discounted ranking score - and both gates call it.
The duplicate was self-documenting. The ensemble copy carried three comments
asking a reader to keep it in step with the member copy by hand: "same
intent as the member gate's coverage floor + bothSidesLive", "the two gates
have to apply the identical correction or the ensemble becomes the easier
one to clear", "same lexicographic ordering as isBetterEra". They had
already fallen out of step once -
|
||
|
|
93d7bbe677 |
refactor(oos): twenty-one counters with one lifetime become one object
SOosTally holds this era's OOS confusion counts and the rates they imply.
The signal keeps one member where it kept twenty-one, and the era-reset
block loses twenty of its twenty-one clearing lines.
THE SHAPE THIS ENDS is the one that produced
|
||
|
|
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> |