- Replaced standard library signal modules with custom implementations to allow for named patterns and improved voting.
- Added new input parameters for module weights, allowing for optimization of individual signal contributions.
- Enhanced the management of trades with new options for breakeven and management cut.
- Introduced a mechanism for dynamic ranking of signal weights based on historical performance.
- Improved initialization logic to ensure proper registration of filters and handling of trading conditions.
- Added detailed logging for trading permissions and account status during initialization.
Everything from tonight, committed so the restructure that follows is
recoverable: the graded stdlib vote, the Wyckoff modules and feed, the
ALGLIB serializer workaround, the restored DB queue, and the Simple/
prototype that is about to be folded into the real filetree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A session window (10:00-12:00) on a D1 chart would silence a setup for good, since every daily bar
opens at the same minute; and the Friday flat is a day-trading rule that would close every swing
before its first weekend. Both are skipped at PERIOD_D1 and above so the same patterns can be tested
on closed daily bars, carrying the swap (operator's request, 2026-09-09).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every reading touches all ~64 market-watch symbols, and under a tick model the tester answers
SymbolInfoTick for a symbol by synchronising that symbol's whole tick history first. One EURUSD
backtest on 2026-09-09 downloaded 4.7 GB of other symbols' ticks, filled the disk, and the tester
ended the test on its first tick without reaching OnDeinit. The census is a report on the live feed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The shadow journal filled a stop entry AT its level, once a minute. Real
stop orders under the tester's 1-minute OHLC ticks filled 0.7-1.2 bp
past it. On the EURUSD decade the same agreement-3 firings priced +0.9
bp virtually and realised -1.8 bp traded, with identical exit rules;
the pairing of real trades with their own virtual twins put the whole
difference in the fill and in a label cost 0.8 bp too low.
- Unfilled firings are checked on every tick; a stop fills at the price
that crossed it, a limit at its own price; risk is measured from the
fill. The ladder still advances once per minute.
- fill_slip_bp and fill_spread_bp are appended to the row's context.
- The meta-label cost per row is the spread at that fill plus the class
commission; the chart's spread now is only the fallback.
- A real trade's notebook line is the OWNER's (the setup that shaped
the order) with the bar's agreement annotation, not whichever setup
published last - 343 of 632 rows named another setup's entry.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Operator: "could we use mql5's alglib random forest and mlp instead of
relying on python? very quick training could reopen the door to selling
the bot."
Database\MetaLabel.mqh. Trained from the Virtual:<setup> rows (every
firing, not the few the gate traded): 14 features parsed from the
notebook line by ONE parser shared with the live gate (setup ordinal,
side, agreement count, opposed, armed count, headroom, minutes to the
forced close, risk in bp, day, hour, order type, valid test, window,
target); label = the firing paid after cost under its own setup's exit,
re-priced from the ladder as research/pricing.py prices it; cost = the
symbol's spread plus the class's commission, printed with every run.
The honest number is walk-forward: for every year from the third, a
forest trained on the years before scores that year, and the take
threshold is the one whose out-of-sample rows paid best - adopted only
if it beats taking everything, else no gate is written and a stale one
is deleted. Final model on every row, ALGLIB CDecisionForest via the
builder (100 trees, 0.66 subsample, Gini importance), serialised to
Adapt\{SYM}_{PERIOD}_meta.rf with the feature contract in the header; a
file whose contract differs is refused.
Gate: in the agreement block, last, as the mean P(pay) over the setups
armed on that side; a refusal is counted as its own entry gate. Trained
at deinit in the tester (fill, then learn, then trade gated - the 2024
loop) and daily when live. Pure MQL5, no DLL, no Python.
Also: virtual firings advance once per minute, not per tick - the
per-tick walk made a decade run four times slower.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Operator: "meta labeling should drastically change the outcome anyways.
unless the journal logs every patterns independently (I think it should
and also voted trades outcomes for comparison)."
Until now the journal recorded POSITIONS - what the agreement gate let
through - so a meta-labeller would have learned from a few dozen trades
per setup. Every arming is now a virtual trade: a pending order at the
setup's own entry and stop, filled when price reaches it inside the
setup's own window, tracked through the same first-passage ladder as a
real position (one shared AdvanceTrack), closed at the ladder's last
horizon, written to the same table as filterID="Virtual:<setup>". The
parent annotates each bar's firings with agree=N, opposed and the armed
combination. Real trades stay filterID="Book", so the two populations
sit on one table. Closed virtual rows are written 200 per commit.
Context now also carries headroom, minutes to the forced close and the
order type. The `journal` global lives in the header so setups and the
signal base reach the one instance the expert feeds.
Also: SQL identifiers are quoted - "2WD_Pattern_0_Sell" starts with a
digit and every Second Wind pattern table failed to create the moment
the database was on in the tester; an index on the natural key so the
replace-on-key scales to a decade of firings; "no closed trades yet" is
verbose again.
Smoke, BTCUSD H1 one month: 52 firings, 26 filled, 26 rows, annotated,
no database errors.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The owner alone named whichever setup sits first in the roster, so an
agreement-4 table read as "PIN" 24 times. The combination is the thing
the research measured; the comment now carries it (31 chars, cut not dropped).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The stock CExpert::OpenLong/OpenShort send no comment, and the tester
keeps no SQLite journal, so a backtest report could say how the EA did
and never which setup did it. One send site now, lot sizing unchanged,
comment = the owning setup's id (or "vote" for a vote-driven entry).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Root cause of "13,209 SP500 trades, every one a buy": not the arming code.
Every tester run since 2026-09-06 echoed `tradingdirection=1` in its
"started with inputs" block - inherited from a stale tester preset
(Profiles\Tester\Warrior_EA.SP500.H4.20190101_20260824.120.ini, 09-01)
that no launch ini overrode. The source default is BOTH, so reading the
source said the EA traded both sides; the trace that names the gate sat
behind TraceMode=false, so its absence was not evidence either.
A one-month BTCUSD H1 trace run said it in one line once the per-side
counters existed: 39 bars armed SHORT, 0 shaped, 39 blocked by the
direction policy. The SP500 decade re-run with tradingdirection=0 pinned:
22,694 trades, 12,383 of them short, net -1,812.74 against -7,871.58
long-only. Still negative at agreement 1, but now the whole strategy.
Hardening so this class of failure announces itself:
- PlanAudit prints "side policy: BOTH" or a !! line at init.
- ENTRY GATES LONG/SHORT at deinit: armed bars per side and the gate each
stopped at (direction, veto, market closed, threshold, cooldown, not
shaped, SHAPED), counted once per armed bar and ungated by any input.
Sweep findings fixed in the same pass, each verified against the source:
- CBookSrRetest could never fire: the level was the extreme of bars 2..61
and the break test asked whether a bar in 2..21 exceeded it. The level
of a candidate break bar is now the extreme of the 60 bars BEFORE it,
as the research reads it (rolling max shifted by one).
- TimeStopReached evaluated the bar-denominated stop in seconds, so a
weekend or session break closed trades with most of their bars unlived.
Now counted with WarriorBarsBetween like every other spacing rule.
- LoadAdaptiveExit installed the file's MINUTES into m_timeStopMin, which
on H1 made every grid value shorter than the 24-bar rule and on H4 made
"15" a quarter of a bar. Converted to whole bars, refused under three.
- PublishSetupContext wrote time_stop_min=0 for every book setup, so the
adaptive writer's never-wider clamp compared against nothing. The
effective minutes are published now.
- WarnIfAgreementUnreachable swept all 1440 minutes; InWindow only ever
sees bar OPENS, so an H4 chart with a 09:30-12:00 window was reported
reachable while dead. It samples bar-open minutes now.
- Order deviation was 3 adjusted points: 3 pips on EURUSD, three cents on
BTCUSD. Floored at 1 bp of price.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Operator: "look at the data without scripts... you can mimick a human
trader much better than mechanical rules, which will allow us to find what
is wrong." Reading the trade list instead of aggregating it, two minutes in:
SP500 decade, agreement 1 : 13,209 round turns, opening side {buy: 13209}
EURUSD decade, agreement 4 : 47 round turns, opening side {buy: 47}
OPPOSED events in 346,188 SP500 bars : 0
THE EA HAS NEVER OPENED A SHORT POSITION. Half the strategy has never run,
and every negative result of this campaign was measured on a long-only EA
while research measured both sides.
It is NOT the direction gate: WarriorEffectiveDirection() returns
tradingdirection, which ships BOTH, and the "blocks short entries" trace
fired zero times in a decade - that gate is never even reached. So
ArmedSetupCount(false) is permanently 0 and no setup sets m_armedShort. The
stdlib calls ShortCondition() as well as LongCondition(), so the failure is
inside ShortCondition or inside every DetectShort. NOT YET DIAGNOSED.
ReportArmingHistogram now counts the sides separately. The combined peak
histogram reports max(long, short) and therefore CANNOT show a one-sided
EA - which is exactly why this survived a full night of screens, sweeps and
significance tests. The aggregate hid a categorical failure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Operator: "port everything from research... there cannot be too much
analysis." Two things, and the first is the cause of the second.
SEVEN SETUPS PORTED - 14 to 21
The journal carries 25 distinct triggers where the EA had 14, and the gap
was not random: EVERYTHING the EA implemented was a BAR SHAPE. An inside bar
is by definition not an engulfing bar, a pin bar is a third shape, and only
ma_cross was shape-independent. Four mutually exclusive patterns cannot
agree, so the agreement count - the only measured edge this project has -
had almost nothing to count. Measured on EURUSD M5 over 74,678 bars, peak
simultaneous arming was 0:68761 1:5333 2:517 3:63 4:4, which is agreement 2
on 0.78% of bars and 3+ on 0.09%.
sr_retest forex p89-91 broken resistance becomes support
tl_bounce forex p92-95 a line through two KNOWN pivots
tl_break forex p92-95 the same line, broken
cci_div forex p237-243 price/oscillator divergence
pin_bar_c forex p206-209 the book's actual 3-bar pin, option 2
cons1234 stocks p185-186 four small bars at the extreme
fvg SMC a three-bar imbalance
NONE OF THESE IS A BAR SHAPE - levels, lines, an oscillator, a gap - so each
can fire on the same bar as an inside bar. On SP500 M5 that took agreement 2
from 2.40% to 4.70% of bars and 3+ from 0.23% to 0.64%, and the plan audit
now reports 19 setups eligible together where it reported 4.
Trend lines read pivots only k bars after they print, which is where that
study usually leaks the future. cons1234 carries no window because the
stocks book explicitly calls it session-agnostic - which also makes it one
of the few that can agree across hours. fvg is labelled book="smc" and is
reachable ONLY through BOOK_ROSTER_ALL: AUTO must never hand a chart a setup
no book endorses, and no result it contributes may be called a book result.
THE CLASSIC MODULES EARN THEIR PLACE - AS A VETO, INVERTED
Their fate, asked properly: given a book setup has fired, does a classic
module agreeing change what it is worth? Measured on ~1.3M journal rows
across 8 instruments, sparse (0-2 of 26 patterns) against crowded (3+):
agreement 1 +1.074 vs +0.407
agreement 2 +2.283 vs +0.696
agreement 3+ +3.922 vs +2.016
Positive in all three periods at every level, and sparse beats crowded on
7 of 7 instruments. ONE binary comparison, not the best of a grid. The
classics are not confirmation - they measure how OBVIOUS the move already
is, and the book setups are largely reversals and breakouts, which do worse
when everything already agrees. That mechanism predicts the sign, which is
why this is trusted where their standalone edge (at chance) is not.
Scale does not transfer: research counted 26 PATTERNS, each module here
reports ONE direction, so the EA counts modules and tops out near 15.
Calibrated on the EA's own distribution instead - 14,338 armed firings, and
vetoing at 6 keeps 19% of them against the journal's 20.75% sparse cell.
AND THE EA DOES NOT REPRODUCE THE SIZE. SP500 M5 2024, agreement 1:
no veto 1780 round turns +0.354 bp t = 1.02
veto at 6 465 round turns +0.414 bp t = 0.63
Right direction, 1.17x where the journal measured 2.6x, on samples too small
to tell either from zero. Two decisive sources disagreeing about magnitude
is what an input is for, so Signal_CrowdVeto is one, defaulting to 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The init audit says whether a quorum COULD form from the declared windows.
EURUSD M5 proved that is not enough: the audit correctly reported "agreement
2 of 4 setup(s) is reachable" and the year traded NOTHING.
Both were right. The four forex setups share sessions and are MUTUALLY
EXCLUSIVE BAR SHAPES - an inside bar is by definition not an engulfing bar,
a pin bar is a third shape, and only ma_cross is shape-independent. They can
be eligible together and never arm together, and no init-time check can see
that. It is the second structural reason within-book agreement cannot work,
after the Stocks book's disjoint windows.
So the run now reports what actually happened. ReportArmingHistogram()
samples the peak simultaneous arming ONCE PER BAR, taken where the counts
already exist so "armed" keeps a single definition, and prints at deinit:
ARMING over 74678 bar(s) - peak simultaneous setups per bar:
0:68761 1:5333 2:517 3:63 4:4 | the threshold of 2 was reached on
584 bar(s) (0.78%).
...with an explicit "NOTHING COULD HAVE TRADED" when the threshold was never
reached. Init asks could it; deinit asks did it; the pair is the diagnosis.
That measurement is also a finding in its own right. Agreement 2+ occurs on
0.78% of bars and 3+ on 0.09% with ALL FOURTEEN setups enabled. Research's
3+ cell held 416,366 rows because its journal carries 36 triggers where the
EA implements 14, so co-firing is far rarer here and the threshold optimal
there is structurally too strict. The default of 2 is right for this EA.
CExpertCustom exposes the report as a BEHAVIOUR rather than widening
GetCustomSignal to public: OnDeinit needs this one line, not the signal
pointer, and handing out the pointer to get it would widen the class for a
single caller.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Operator: "the EA should be dynamic and work on any timeframes and symbols.
it needs its own internal research framework."
Every failure this EA had today was a SCALE failure - a rule meaning one
thing where it was measured and something else on the chart it ran on:
* the opening reversal is a 20-minute construct; on H1 that is ZERO bars
* a 120-minute time stop is 24 bars at M5 and TWO at H1 - a different rule
* the Stocks book's three windows are disjoint, so a quorum of 2 is
unreachable and the run trades nothing
* m_entry was shared by both sides, so an armed setup shaped from a zero
None of those raise anything. The EA trades nothing, or trades a different
strategy, and looks exactly like a quiet market doing it. Three cost a full
test run to find; the fourth had been live since the setups were written.
The fix is disclosure, not cleverness. At init every setup now states what
it will actually do on this symbol and period, and what cannot be true here:
===== TRADING PLAN, resolved for EURUSD PERIOD_M5 =====
IB inside_bar forex win 10:00-12:00,15:00-17:00 entry 1 bar
stop 1.00R target none trail 1.00/1.00R be 1.00R timestop 24 bars
...
agreement 2 of 4 setup(s) is reachable - up to 4 eligible together
===== 4 setup(s), no scale problems =====
DESIGN, and it is the reason this cannot rot. Each filter writes its OWN
line (PlanLine) and answers for its OWN coherence (ScaleIssues); PlanAudit
only collects and orders them. Adding a setup, or a rule to one, cannot
leave the audit behind because the audit knows nothing about either.
ValidationSettings now asks ScaleIssues rather than carrying its own copy of
the timeframe test, so a rule is checked in exactly one place.
ScaleIssues catches, among others, the case that cost today's run: a
wall-clock time stop that resolves to under three bars on this chart is a
value measured on a faster one, and says so.
It warns, never refuses - one roster can hold setups from books of different
granularity, and aborting because one cannot fire here is a worse answer
than running the rest and saying so. WarnIfAgreementUnreachable is called
from the audit so there is a single init entry point.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The operator asked whether everything from research was ported. It was not,
and the gap was systematic: every TIME-BASED finding was missing.
THE FOREX BOOK'S THREE ENTRY WINDOWS
BookForex.mqh contained no SessionWindow call at all, so all four forex
setups fired around the clock - while every research number that measured
them ran only inside the book's own windows. The book names them (p145):
London 03:00-05:00 ET, New York 08:00-10:00 ET, Tokyo 19:00-21:00 ET, "the
first one or two hours after a session open".
This is the same mismatch that made research/add_session.py apply the
STOCKS cash session to forex. Correcting it there TRIPLED gross edge on M5;
every pair went positive gross on both timeframes, EURUSD M60 net
-0.856 -> -0.271 bp against a 0.80 round turn. Raw hourly range confirms it
independently of any trade definition: a 3.5x spread across the day, and
the dead hours are exactly the ones an all-day EA was trading.
The three windows are DISJOINT, so CBookSetupSignal now holds a SET of
windows rather than one pair. That is also why the forex windows could not
simply have been added before.
NO NEW ENTRY INSIDE THE LAST 60 MINUTES
The most robust result of the campaign, and it was absent entirely - nothing
in the EA knew how much session was left when it placed a trade. On 951,919
session trades from 2005: enter with 30 minutes left and 87.4% never resolve
at all, so the forced close decides them, and that close is worth +2.8 bp
early against -1.0 bp late. Late setups are not worse patterns; they are
never given room.
cutoff kept net bp vs all years improved
15 96.2% -0.961 +0.039 22/22
30 92.1% -0.936 +0.064 22/22
60 83.4% -0.895 +0.105 22/22
120 67.7% -0.867 +0.134 22/22
Monotone, saturating near 90-120, improving EVERY one of 22 years. Trusted
where mined rules are not because the mechanism predicted it before it was
measured. 60 rather than the 120 that measured best: 60 carries 78% of the
improvement while keeping 83.4% of trades against 67.7%, and trade COUNT is
the binding constraint right now. One #define to raise it.
Measured against WarriorScheduledFlatMinuteOfDay - the SAME forced close the
expert actually applies, now extracted so both callers share one owner
rather than each deriving a schedule that can drift from the other.
Context - BTCUSD H1 2024, equal weight, risk guard off, 0.10 lots:
agreement 1, no clock 391 RT -5.65 bp
agreement 1, clock 113 RT -3.15 bp
agreement 2, no clock 116 RT -2.11 bp
agreement 2, clock 31 RT +18.93 bp <- first positive, but n=31
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two fixes for the same class of failure: a configuration that trades
nothing while looking exactly like a quiet market.
THE CRYPTO CLOCK - OURS, AGAINST THE BOOK, AND MEASURED
The Bitcoin book says crypto has no session and no hour effect: "as active
at 3AM on a Sunday morning as it is at 9AM on Monday" (p38, p77). The
second half is FALSE, on 868,560 BTCUSD M5 bars from 2017-05 to 2026-09:
quiet 05:00-14:00 server 51.0 - 60.7 bp
middle 00:00-04:00 63.4 - 71.8 bp
ACTIVE 15:00-23:00 66.3 - 91.8 bp peak 17:00 = 91.8
quietest 07:00 = 51.0, busiest 17:00 = 91.8, ratio 1.80x
Sat 0.59x, Sun 0.63x against weekdays 0.99-1.02x
WHY IT IS A COST RULE AND NOT A SESSION CLAIM. The book is right that there
is no open, no close and no meaningful "day", and nothing is gated on one.
But cost in R is cost_bp / dist_bp, so at 51 bp an hour instead of 92 the
same pattern gives a stop about half as wide and pays close to TWICE the
relative cost for the identical trade - on the class that already pays the
highest commission we trade.
HOW THE WINDOW WAS CHOSEN, so it is not a fitted parameter: the median of
the 24 hourly medians is 64.25 bp, and 15:00-23:00 is the ONE CONTIGUOUS
BLOCK entirely above it. It lands on the US cash session plus the hour
into it. One statistic, one threshold, one contiguous run, no search.
The weekend skip is cruder and needs no threshold at all: 0.59x and 0.63x.
AN UNREACHABLE AGREEMENT THRESHOLD NOW SAYS SO AT INIT
SP500 M10 under AUTO ran a full year and traded nothing at agreement 2.
Nothing was broken. The Stocks book's three setups are SESSION-DISJOINT BY
DESIGN - opening reversal 16:30-19:00 server, ERBO 17:00-19:00, PDH/PDL
21:00-23:00 - so PDH/PDL can never be armed on the same bar as either
morning setup, at most two of three are ever eligible at once, and a quorum
of two needs a gap fill and a range break on one bar and one side.
WarnIfAgreementUnreachable() walks all 1,440 minutes, counts how many book
setups are ELIGIBLE at each, and takes the maximum. Firing is rarer than
eligibility and can only be rarer, so that maximum is a hard ceiling: a
threshold above it is unreachable with certainty, not merely unlikely. It
warns rather than refuses, so a sweep across rosters still scores the pass.
Measured today on BTCUSD H1 2024, equal weight, risk guard off:
AUTO agreement 1 391 RT -5.65 bp (research: -6.47 for one alone)
AUTO agreement 2 116 RT -2.11 bp
ALL agreement 2 269 RT -2.18 bp
ALL agreement 3 64 RT -5.13 bp
The 1 -> 2 improvement is the confluence mechanism reaching the EA for the
first time. It does not continue to 3, and nothing is positive yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Operator: "commission is automatically charged on mt5 during backtest, so
make sure to not include anything related to commissions in the EA. have as
little inputs variables as possible for a clean menu."
COMMISSION. Expert\CostModel.mqh deleted, the six Cost_Comm* inputs with
it, along with the per-bar spread sampler in OnTick and the init COST MODEL
printout. This removes a DUPLICATE, not the cost: MetaTrader applies the
broker's own schedule to every deal in the tester and on the account, so a
hand-typed second copy inside the EA could only ever disagree with it - and
a schedule that drifts from the broker's is worse than none, because it
looks authoritative. Nothing consumed the numbers any more in any case; the
gate that read them went with the era verdict.
Only the asset-class detector survived, moved to Expert\AssetClass.mqh. It
answers a question MT5 does not: which book applies.
THE ROSTER IS ONE INPUT NOW. Fourteen per-setup constants stood in
ClassicSignals.mqh, all false, none reachable without a recompile. They are
replaced by Book_Roster, chosen a BOOK at a time - which is the only
grouping that respects the accountability rule (one book per asset class, a
setup is only accountable on the class its own book covers) and the only
one the agreement count can use, since a roster of one cannot express it.
AUTO reads the class off the instrument. Eight tester values instead of
16,384 mostly meaningless combinations.
INPUTS 63 -> 22 (17 parameters and 5 section headers). Cut or made const:
everything dead after the training deletion, the two API keys (a credential
is not a strategy parameter and must not travel in a .set), and knobs that
cannot change an outcome - Signal_ThresholdOpen above all, since Direction()
now returns exactly 0 or +/-100, so every threshold in (0,100] behaves
identically.
TWO OF THOSE ARE FIXES, NOT TIDYING.
- The 30-bar signal cooldown was still live. Its floor came from the
leg-ride label ("a trade is held 5 + the median ZigZag leg = 18-19 bars").
That label is deleted, and the thing being spaced now is a book setup that
owns its trade end to end. Worse, it would have silently thinned the very
population the agreement count counts. Off, and const.
- Signal_MinAgreement defaults to 2 rather than 1. One trigger alone is the
policy measured NEGATIVE on every asset class.
The session filter is const off for the same class of reason: every setup
carries its own window from its own book, and a global one layered on top
applies one book's clock to another book's market.
SweepGuard's table was rebuilt - it was almost entirely names from the
deleted training layer. It now refuses Asset_Class (a statement about the
instrument, not a strategy choice) and pins the log-volume switches.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An armed setup used to be decisive on its own. That is exactly the policy the research measured,
and it is negative: across 942,918 journal trades, one trigger alone is -2.15 bp out of sample
and up on 2 of 8 instruments, two agreeing +5.65, three or more +7.59 at 240 trades a year and
8 of 8, monotone in all three periods. Gross moves as much as net, so it is not a cost artefact.
Direction() now counts DISTINCT setups armed on the same bar and side, over the same population
ArmedSetupOwner() picks the order-shaper from - so the count and the shaper can never disagree
about who fired - and takes the trade only at Signal_MinAgreement or more.
OPPOSED IS TESTED FIRST, because it is a veto and not a tie. Setups firing both ways on one bar
is the worst cell measured anywhere in this project, -5.80 bp pooled and -21.31 on BTCUSD,
negative gross as well, and worth more than the agreement bonus is. It stands aside and says so
once per bar.
Firings that arm but fall short of the threshold are traced rather than dropped: those declined
signals are the population the offline meta-label net has to learn take-or-skip from, so they
belong in the journal.
Signal_MinAgreement is an INPUT because choosing it is a Strategy Tester question - 2 buys far
more trades, 3 a better rate, and the walk-forward decides. It defaults to 1, which is the
measured-negative policy, only because that is also what a chart with a single setup enabled must
do; a silent no-trade would be worse than an honest bad default.
All three Walsh books state the principle in words, so this is the books' own rule rather than an
overlay on them.
Compiles 0 errors / 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The base class the operator asked for. A book setup now writes only its pattern; arming, the
entry window, order shaping, the time stop, breakeven, the ratcheted trail, the journal context
line and the per-setup adaptive-exit file are all shared.
PER-SIGNAL MANAGEMENT IS THE NEW PART, and it is why one global trailing object was never
enough: the books state management per setup and they disagree. Walsh's stocks book moves to
breakeven at 10-15 cents on a 5-10 cent stop (1.0-3.0R, centred 1.25R - the constant that was
inverted in research for a whole campaign); the Bitcoin book uses a RATCHETED trail and says so
in those words; the forex book trails at ten pips on a 5-10 pip risk. Added
CExpertSignalCustom::ManageTrailing() and wired CExpertCustom::CheckTrailingStop() to ask the
OWNING setup first, falling through to the global object only for positions no setup owns.
A setup returning false from ManageTrailing is an ANSWER, not an abstention - it wants the stop
left where the book put it - so the position is not then handed to CExpertTrailing to have a
different book's rule applied to it.
The ratchet is enforced by only ever returning a stop better than the one the position already
carries, rather than by remembering a peak in the EA. Same rule, and it cannot drift out of step
with the broker's copy across a restart or a re-init. The risk unit is recovered from the
position's own open price and stop, not recomputed from the pattern, because the bars that
produced the pattern may be long gone.
FIRST FOUR SETUPS, all from the forex book with its own qualifiers applied rather than emitted
raw: the inside bar (angled-and-not-flat 10-SMA, inside bar smaller than its mother, at the
correct extreme - only 14.2% of inside bars meet all of it), the pin bar, the engulfing bar (with
the one-or-two-bar pullback and the with-trend requirement that this project computed for months
and applied nowhere), and the MA cross.
ShouldTraceTradeRejections() moved private -> protected so a setup can log its own VOID.
Compiles 0 errors / 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Operator, 2026-09-07: "we don't want to train on the legs anymore." That removes the reason
the whole training stack existed, so it goes.
77 files deleted, 68,078 -> 29,113 lines: 57% of the codebase. A full build drops from 66s to
24s. Compiles 0 errors / 0 warnings against a 0/0 baseline taken before the first cut.
THE SEAM. The four AI signal modules (PAI/CONV/LSTM/HYBRID) were the only consumers of
ExpertSignalAIBase -> AI/Network -> AI/Impl/*, AIBase/*, Training/*, Persistence/*, Topology/*,
Labeling/*, Features/*, OnlineLearning/*, ConfigLock/* and the training half of Chart/*. Cutting
those four dropped all of it. Nothing else reached in.
WHAT SURVIVES, and it is the part that matters: System\NNFilter.mqh - 225 self-contained lines
with their own forward pass, reading a plain ASCII model written by research/export_nn_filter.py,
with the feature-name contract that REFUSES a file whose feature list does not match rather than
approximating it. The offline meta-label net's entire runtime already existed; it never needed
any of what was deleted.
THE LEG-RIDE LABEL AND ITS EXIT. LiveLegDirection() replicated the stock ZigZag so the exit could
fire on the same event the label's ride ended on. No label, nothing to agree with - the replica,
the exit and Expert\Labeling\LegState.mqh are gone, and the take-profit is unconditional again
(it was suppressed only to avoid capping the tail the leg label selected for).
ONE THING THIS NEARLY DID SILENTLY. ClassicVotesMoveMoney() returned "no while an AI member is
present, yes when none is registered". Deleting the networks made the second clause true
everywhere - which would have reinstated the worst defect this codebase has had: fifteen classic
modules at weight 1.0 as the live money vote, which is what "the EA is not profitable" turned out
to mean. It now returns false unconditionally. The only thing that can open a trade is an ARMED
BOOK SETUP through the +/-100 override in Direction(). The classics stay wired because they are
silent and free, and because they are the raw material for the agreement count.
Also extracted System\ChartObjects.mqh - the chart-object namespace list and its sweep, which had
lived inside ExpertSignalAIBase.mqh and were never about training.
NOTE THE CONSEQUENCE, PLAINLY: the only book setup that exists in MQL5 is CSignalInsideBarGap and
it is still disabled, so this EA now trades nothing until the book triggers are wired. That is
slice 3 in REFACTOR_PLAN.md and it is a deliberate state, not an oversight.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The operator's complaint that "the logs get filled" is measurable, so it was measured. The
2026-09-06 tester log is 1,570,535 lines and FOUR print statements are 86.6% of it:
556,732 35.4% "Starting direction calculation with total filters: N"
556,731 35.5% "Final directional result: N"
247,146 15.7% the two "open rejected" traces
None of the four is a decision. Direction() runs on every tick and recurses into every filter -
about eight calls a tick on this fleet - and emits two lines carrying a filter count and a
number. The rejection traces fire on every tick a side is blocked, which on a one-sided chart
is forever.
They were all on the same switch as everything else, so turning VerboseMode on to diagnose one
thing produced a journal too large to search. Added a second level, TraceMode, off by default,
and moved exactly those four sites to it. PrintVerbose() keeps its meaning and none of its ~40
call sites changed.
TCLog's throttle was near-inert where it mattered most. It measured its window with
TimeCurrent(), which in the Strategy Tester is SIMULATED time: a pass over years of history
crosses sixty simulated seconds many times a second, so the throttle admitted nearly every call.
That is why 247,146 lines got through a function whose whole purpose is collapsing them. Now
GetTickCount64(), which is real elapsed milliseconds and behaves identically in both worlds -
unchanged in live trading, genuinely one line per key per minute of run time in a tester pass.
Suppressed calls are counted and reported on the next line through, so nothing is hidden.
Compiled clean in the stage copy: 0 errors, 0 warnings, against a 0/0 baseline taken first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The single most frequent line in a 180 MB agent log this session was
WarriorExpandFleetIfDue: ChartSaveTemplate failed (error 4003) - cannot expand the fleet
printed once per bar, for the whole pass. A tester run has one chart and cannot open another, so
ChartSaveTemplate fails every time and the sweep had nothing to do even when it succeeded. It now
returns immediately under the tester, optimiser and forward pass.
The print is also latched to once per run for the live case. The condition behind it does not
change between sweeps, so repeating it says nothing new and buries the lines that do - the same
one-shot latch the calendar probe and the pooled-gate writer already use.
This is the measured half of the operator's "the logs get filled"; the rest of that is the
per-era training diagnostics, which go with the training layer in the planned refactor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The second half of the sweep. Everything the first pass reported and left.
THE ENSEMBLE GATE JUDGED A POPULATION THE ACCOUNT NEVER SEES. It scored every OOS bar whose
vote cleared the rung; live, a vote that clears the threshold still has to pass
VoteCooldownAccept, and a suppressed bar produces no arrow, no order and no position. On this
fleet that window is 30 bars against a mean ride of ~34, so the certificate counted roughly an
order of magnitude more trades than the account could take, each overlapping its neighbours.
The member gate was fixed for exactly this on 2026-09-03 and both sites carried a comment
saying the ensemble still had the defect. The sweep now replays the vote cooldown per rung:
rows arrive in time order, so one kept-timestamp per rung reproduces it exactly. sweepTraded[]
carries precision, the book, the cost and the by-side counts; sweepFired[] stays the signal
population and only coverage reads it, because a cooldown caps traded coverage by construction.
The effN uses the declustered helper - the cooldown has already spaced that stream.
BOTH FAMILY-WISE SELECTION GATES FORMED THEIR SE ON RAW CALL COUNTS, the last SEs in the
project still undeflated for label overlap, which made the correction guarding the deploy
decision the most permissive test here. Now effective. This TIGHTENS both bars, which is why
it was left standing until asked for; the ensemble gate's own note already recorded that every
chart clears it by 6.5-12 sigma on effective calls, so the measured cost is nothing.
THE RIDE WAS PRICED AT BAR CLOSES NO ORDER CAN FILL AT. Entry was the close of the bar the
vote was formed on. Direction() runs on the first tick of the NEXT bar and the market order
fills there, which is that bar's open; the exit is read from closed bars and acted on one bar
after the flip. So the book credited every ride with two bar gaps and called it the trade the
EA places. Entry is now the next bar's open and the exit the open after the flip - the same
event, at the price the account gets.
RIDES LONGER THAN THE 200-BAR CAP WERE DROPPED, not capped: one-way survivorship against the
longest winners a trend-riding label has. A ride whose bars existed and simply had not flipped
is now marked to market at the cap. Only one that ran off the leading edge of loaded history
stays unresolved, which is the one case genuinely not knowable.
Both label changes re-key: TGT:LEG1 -> TGT:LEG2. Every model retrains from era 0.
Also: the minimum-stop floor wrote an un-normalised price (TCAdjustStops normalises only what
it widens, so a legal floored stop reached the trade layer off-tick); and the journal's context
window was 180 seconds, sized for the dead sixty-second order expiry, while an entry window is
counted in BARS - so any fill later than three minutes silently lost the context column the
per-pattern adaptation is built from.
Compiles 0 errors, 0 warnings. Smoke-tested on EURUSD H1 over 2026-08-24..09-01: runs clean,
no runtime errors, fingerprint reads TGT:LEG2:10:D12 in situ, and nothing trades - which is
the re-keyed label refusing the stale models, as intended. Not deployed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sweep after "still not profitable" found the EA was not trading the strategy it certifies.
THE VOTE. Direction() aggregated all children in one accumulator: 15 classic modules at the
stdlib weight of 1.0 each, the four AI members at trust weights of 0.17-0.22, and the derived
threshold (1% on most charts) had been certified on the AI members' vote alone. Four unanimous
networks netted 1.01; two classic modules confirming a state (10 + 10) netted 1.27 and opened
the trade with the networks silent. Proven in the tester on the old build: EURUSD H1 from
2025-09-01, threshold 1% published at 00:05, market buy 3.5 lots on the first bar, then vote
magnitudes of 4-6 that the networks cannot produce. The book that traded was the classic
consensus this project measured at chance; the certified book could barely open.
- ClassicVotesMoveMoney(): classic modules stay in pass 1 (journal, raw arrows, the vote vector
the networks read) and leave the money sum and the overlay whenever an AI member exists. With
no AI member they remain the book. Announced once.
- CExpertSignalAIBase::LiveVote(): the parent sums the AI member's certified contribution
(module weight x (tier - chance), clamped) instead of its raw tier weight.
- VoteCapableWeight() uses LongCondition's readiness test, m_deployedLive included: a deployed
member had numerator and no divisor share, which with the classics gone would have been a
division by zero live while the inference-only tester looked fine.
THE BOOK. The ride book the gate judged carried no stop; every live position carries one at the
published mean adverse excursion. The verdict now gates the ride under that stop (sideG*: a ride
whose adverse excursion reached it pays -stop) and prints both. Sweep line: [book|stopped|cost].
THE REST OF THE SWEEP.
- Scheduled close-all: a +-1 minute window with no catch-up, and Processing() ran on the same
tick with the cached vote, so it could re-open five minutes before the weekend. Now a per-day
latch from target-1 min, retried every tick and timer, and OpenPosition refuses while latched.
- News filter: an empty CalendarCountries() answer (the base still synchronising) was cached for
the session, leaving the filter inert with EnableNewsFilter true. Not cached any more.
- NF_MinImpact default HOLIDAYS vetoed 50% of EURUSD weekday hours (36% GBPUSD, 38% USDJPY,
27% USD-only), measured on the terminal's own calendar export; HIGH vetoes 14/12/11/9%.
Default -> HIGH. Charts attached before keep their stored value.
- One m_tradeOwner for both books delegated the short book's exit to a long-only setup, whose
default CheckCloseShort fell into the base vote exit on the child (m_direction EMPTY_VALUE =
DBL_MAX >= any threshold): every short died the tick after the inside bar armed. Per-side
owners, and the stdlib's EMPTY_VALUE guard restored in CheckClosePosition.
- One expiry clock for two books -> per-book m_bookExpiration[2].
- The inside bar's time stop selected the lowest-ticket position on the symbol -> by book magic.
Build tag inputs-not-voters-1. Compiles 0 errors, 0 warnings. Not deployed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Operator's report, and it is correct: ImpactWeightedProximity weighted by
MqlCalendarValue.impact_type, which is MT5's ACTUAL-versus-FORECAST verdict
(ENUM_CALENDAR_EVENT_IMPACT: 0 NA, 1 POSITIVE, 2 NEGATIVE) and is only
knowable after the release. With searchForward=true on a historical bar it
read the post-hoc outcome of an event that had not yet happened - the exact
thing the comment above it claimed was "deliberately not exposed anywhere
here". The comment stated the right principle and the code contradicted it.
Verified against the terminal's own 200,510-release export rather than
argued from the docs:
- impact_type is 0/1/2 and NEVER 3, so the /3.0 divisor capped the feature
at 0.667 and 1.0 was unreachable. Exactly as reported.
- of 16,103 rows where nothing had been released (no actual value), 16,079
- 99.85% - carry impact_type 0. It is a function of the outcome.
- it is near-orthogonal to importance: 8,783 of the 13,662 HIGH-importance
releases (64%) carry impact_type 0. The feature scored ZERO on two thirds
of the biggest events on the calendar, while scoring its maximum on a
trivial event that happened to surprise.
THE SAME FIELD WAS ALSO DRIVING THE LIVE NEWS FILTER, and there it is worse.
NF_IMPACT_PRESETS is plainly the importance ladder - HOLIDAYS=0, LOW=1,
MEDIUM=2, HIGH=3, which is ENUM_CALENDAR_EVENT_IMPORTANCE exactly - and it
was being compared against impact_type. Since impact_type never reaches 3,
selecting "High Impact News", the obvious choice for anyone wanting to avoid
major news, made the test unsatisfiable and SILENTLY DISABLED THE FILTER: the
EA would trade straight through NFP with the news filter on and set to its
strictest setting.
Both now read the event's scheduled importance via CalendarEventById, which
is published in advance, takes the full 0..3 the divisor was written for, and
makes the forward-looking half honest - a training bar may know NFP is due in
twenty minutes, because everyone did. Lookups are cached in a sorted array;
a failed lookup is NOT cached, because an unsynchronised calendar base fails
transiently and would otherwise pin an event to 0 for the session.
Topology gains |NEWSV:2 so a model trained on the old leaky feature can never
load against the new one - UseNews and NewsFeatureWindowMinutes could not
catch it, because neither of them changed. Conditional append, per the rule
the MACD/Ichimoku block states, so a fingerprint with news OFF stays
byte-identical.
BEHAVIOUR UNDER THE SHIPPED DEFAULTS IS UNCHANGED, which is why no retrain is
triggered: EnableNews is false, so the feature is off and NEWSV appends
nothing; and NF_MinImpact defaults to HOLIDAYS=0, where ">= 0" vetoed every
relevant event before and does so now. What changes is that every other
preset now means what its label says.
Compiled clean in the _claude_s2build scratch copy: Result: 0 errors, 0
warnings. NOT DEPLOYED - feedback_no_compiling authorises compiling in a
scratch copy and forbids touching the deployed build; _claude_stage's .ex5 is
untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
NEWS_REFRESH_SEC documents an hourly refresh, but g_newsExport.Update()
was called only from WarmExternalData() in OnInit. The exporter would
therefore have run once per attach and never again, and the calendar is
exactly the file that keeps changing after startup - a release lands and
the terminal's base gets its actual. OnTimer's alt-data upkeep block was
already calling the alt-data fetcher under the same tester guards, so the
exporter joins it there. Its own hourly throttle makes the call cheap and
means the two cadences do not have to agree.
Also lands the per-tick order expiry clock (CExpertCustom::ExpirePendingOrders)
that was still sitting uncommitted, with its reasoning corrected: it argued
from a "sixty-second order", and the entry window became a count of BARS in
eb2a443. The sixty seconds came from a fast-fill result that turned out to be
87% session-gap fills booked at a price never offered, so the deadline's
length carries no edge - only the honouring of it does, which is what this
function exists to guarantee. The defect it fixes is unchanged and is about
the CLOCK, not the length: with Expert_EveryTick off, Refresh() declines every
intrabar tick, so the earliest any deadline could be enforced was the next bar
and a one-bar window silently became a two-bar one.
Build tag three-1 -> books-1 so the OnInit line identifies the binary.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There is one day-trading book PER ASSET CLASS: the Stocks book (applied to indices), the Bitcoin
book for crypto, and the Forex book. A pattern is only accountable on the class its book was
written for. If it also works on another class, keep it there; if it does not, that is not a
failure of the pattern and must not count against it - it simply is not traded there.
DECLARED, NOT INFERRED. WarriorAssetClass() reads SYMBOL_PATH and falls back to SYMBOL_CALC_MODE,
which is a good guess and still a guess: a broker filing US500 under "CFD" would hand the chart
the wrong book silently. ASSET_AUTO keeps the detection and any other value overrides it, set
when the EA is attached. WarriorAssetClassResolved() is the single place that resolution happens,
so the cost model, the logs and any future book gate cannot disagree about what this symbol is.
OnInit now states it either way - auto-detected (with a note to set the input if the broker files
the symbol oddly) or declared, printing what the detector would have said.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TESTER-FOUND, NOT YET TESTER-CLEAN. Compile-verified (0 errors) in _claude_stage; run 7
(SP500 H1, 2022-2026) shows buy stops shaped 1.25R/8R/60s, the 15-minute time stop
closing them, and the network gate active. Two defects remain open - see the handoff
memory project_handoff_20260906_opus.
WHAT WAS WRONG. The parent CExpertSignalCustom never called a child's OpenLongParams
or CheckCloseLong: once the inside-bar setup voted, the parent shaped a MARKET order
from its own ATR rule (2 ATR stop, no target) and closed it on the vote 9-35 hours
later, or on the very next tick through the protective path. And with the fifteen
classic voters on, the setup's 100 netted to ~11 against a threshold of 15.
Expert/ExpertSignalCustom.mqh
OwnsTrade() / SetupArmed(isLong) virtuals; ArmedSetupOwner() scans m_filters.
OpenParams() asks the armed owner for price/sl/tp/expiration first and records
m_tradeOwner + m_shapedExpiration; the ATR path clears them.
CheckClosePosition() with an owner returns the owner's CheckCloseLong/Short only.
Direction(): an armed setup is decisive (+/-100), never diluted by the classics.
Expert/ExpertCustom.mqh
OpenPosition(): on TRADE_RETCODE_INVALID_EXPIRATION resend GTC and set
CExpert::m_expiration to the requested expiry. KNOWN DEFECT: that clock is only
read on the new-bar path (Expert_EveryTick=false) so orders lived 30 min, not 60 s.
Signals/SignalInsideBarGap.mqh
OwnsTrade/SetupArmed overrides, m_armedBar; CheckCloseLong no longer chains to the
vote exit; CNNFilter + CInsideBarFeatures wired as the confluence gate (file
Common\Files\Warrior_EA\Adapt\_ALL__inside_bar_u.nn, feature names checked one by
one); nn_gate/nn_a/nn_b/nn_agree in the journal context; verbose-mode feature dump
to Adapt\features_{SYMBOL}_{PERIOD}.csv for research/check_feature_parity.py.
Features/InsideBarFeatures.mqh (new)
The 48 network inputs computed on a chart from closed bars, in the file's order.
First parity pass on 13 bars: 30/48 agree, 18 drift - NOT trusted yet.
System/NNFilter.mqh (new) two groups of dense nets, agreement = both >= threshold.
System/AltData*.mqh
cot_am_idx3y / cot_lm_idx3y / vix_rank500 appended to the catalog (export v3),
raw COT cache widened to asset-manager long/short (old caches refetch), HasColumn().
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
I introduced two duplications in 3a925fa and this removes them before they set.
THE SAVE/EVAL/RESTORE BRACKET WAS WRITTEN TWICE. HistoricalNetVote() had it inline and
ClassicVoteAt() got a second copy. It is six statements that must stay together - SaveVoteState,
EvalShift(idx), Direction(), EvalShift(0), read the ladder weights, RestoreVoteState - and the
consequence of getting one wrong is not a compile error: Direction() writes the state the signal
database journals for a real fire, so an unbracketed replay of a three-week-old bar overwrites what
the live tick just decided and records history as though it happened now. Two copies is two chances
to drop a line. Now ReplayClassicWeightAt(), used by both.
It returns the RAW ladder difference and leaves ModuleWeight() to the caller, which is the honest
seam rather than a convenience: HistoricalNetVote applies it because it is reconstructing a VOTE
where a module opinion is weighted by trust, and the feature path must not because a model input
cannot move for reasons unrelated to the bar it describes. Encoding that choice inside the helper
would have forced one caller to undo it.
THE CLASSIC-VOTER PREDICATE WAS WRITTEN THREE TIMES. "not an AI filter and has patterns" appeared in
ClassicVoteWidth, ClassicVoteAt and again in HistoricalNetVote. Width and values MUST agree on which
children count or the feature block silently misaligns against its own column names - a fault that
produces plausible numbers rather than an error. Now IsClassicVoter(), four callers.
A MISTAKE WORTH RECORDING: my first attempt used line-mode `perl -pi` with embedded newlines against
a 3,000-line file and expanded it to 268,141 lines. Caught by the match count reading 262,029
instead of 2, restored with git checkout, redone with precise edits. The deployed build was the good
commit throughout, so nothing reached the running fleet. Structured multi-line edits do not belong
in line-oriented regex.
No behaviour change: same brackets, same predicate, same arithmetic, one copy each.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The fifteen classic modules now feed the neural nets as features instead of only voting beside
them. One signed column per pattern-bearing module, replayed AT EACH HISTORICAL BAR so the network
sees what the rule-based primaries said there, not what they say now.
WHY FIFTEEN COLUMNS AND NOT SIXTY. Every module OVERWRITES rather than sums (verified across all
fifteen), so exactly one pattern weight is returned per call - and the weights are DISTINCT within
every module (verified: no collisions anywhere). The signed net vote is therefore a lossless code
for which pattern fired and in which direction: RSI +10 rising, -10 falling, +20 oversold reversal,
-20 overbought, +-60 divergence, +-90 double divergence, 0 nothing.
And it works as a SCALAR, which a categorical code normally does not. Feeding codes to a network as
a number line is usually a modelling error - it interpolates between values that have no order - and
the fix is one-hot at ~60 columns of width. This escapes that because the ladder was built ordered
BY CONVICTION: 10 is a weak confirming state, 95 is rare and strong. Magnitude carries meaning
independently of identity, so the net can read signed strength before it ever learns the codebook.
Both properties exist ONLY while the weights are frozen priors. If the database is ever allowed to
re-score them this column becomes an unreadable moving quantity.
SCALED /100 on the way in. The raw ladder runs to +-95 while the rest of the vector sits within a
few units, and one column a hundred times wider than its neighbours owns the first layers gradients
and the covariance matrix with it - the exact failure that cost a session when a Wyckoff sentinel
read -580.
THE REPLAY IS THE EXISTING ONE. HistoricalNetVote() already brackets a filter evaluation in a
six-field SaveVoteState/EvalShift/Direction/RestoreVoteState sandwich, because Direction() writes
the live journaling state and replaying a three-week-old bar without it would overwrite what the
live tick just decided and journal history as if it had happened now. Same bracket, new consumer.
CACHED ON THE PARENT, not per member. The four AI models on a chart share the same filter objects,
so a naive implementation replays fifteen ladders over 179k bars four times - and worse, could let
members see different vectors. One computation per bar, served to all four: an ensemble voting on
different evidence is not an ensemble.
RSI, MACD AND TIME FEATURES ARE BACK ON. They measured 0/1, 0/3 and 0/6 unanimous under the
leg-ride label and I cut them last night on that evidence. Under this architecture they are CONTEXT
rather than predictors - an RSI-oversold MACD cross is a different trade from a bare one, and
day-of-week conditioning is exactly the conjunction a meta-labeler is meant to find. Standalone
predictiveness and conditioning value are different questions and the width cut answered only the
first.
Width is now retrain-forcing through m_neuronsCount as always, so the models re-key themselves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
WHAT AND WHY. ed91919 removed the four classic votes because all 26 patterns measured AT CHANCE as
standalone entries - pre-registered, nothing fitted, and the +4 sigma that had once appeared was two
bars of lookahead. That result stands. It is also not the claim being made here.
That test measured UNCONDITIONAL edge: fired blind, does this pattern beat a coin on average. No -
after the lookahead fix MACD_p4 read -0.02pp at -0.02 sigma, flat rather than weak. A signal that is
zero on average can still be strongly positive on a SUBSET, and finding that subset is exactly what
meta-labelling is for. The primary supplies direction and an entry BAR and is judged on RECALL; the
net decides which firings to take.
THE MEASURED PROBLEM THIS ATTACKS. Under the leg-ride label the primary was "the ZigZag leg in
progress", so entry landed on an ARBITRARY bar inside a move already underway. Measured 2026-09-05
on the converged fleet: the ORACLE ride is +4.2 to +4.6 ATR per leg on every chart, and the models
captured +0.08 to +0.38 over a 14-17 bar hold - under 10%, because most of the leg was gone before
entry. Always-ride scores -0.14 to +0.10, i.e. an arbitrary entry inside a leg is worth nothing. A
pattern fires at a CHOSEN bar with the move ahead of it.
RESTORED: SignalMA (4 patterns), SignalRSI (4), SignalMACD (6), SignalIchimoku (12),
OscillatorDivergence, plus Bill Williams SignalAO (4) and SignalAC (3) recovered from 1073262.
33 patterns across six voting modules. The only edit needed was dropping SweepPrepareIndicator(),
whose base-class method no longer exists.
THE WEIGHTS ARE FIXED PRIORS (Variables\ClassicSignals.mqh), set from structural strength and rarity
in the spirit of the standard library's own ladder, NOT fitted to returns:
10 confirming state ("price is on the right side") - every module's pattern 0
15-30 simple state or weak-grade event (overbought reversal, cross in a poor location)
30-50 a crossing or completed pull-back
50-70 structural: divergence, cloud breakout, strong-grade cross
80-100 rare confluence: double divergence (90), Sanyaku Kouten (95)
33 numbers tuned against measured returns would be 33 free parameters and would hand back the
family-wise problem this project keeps rediscovering. They are priors and must stay priors.
AND THE FEEDBACK LOOP IS CUT. DB_RankingFeedsWeights=false splits the signal database in two:
ProcessBufferedSignals() still RECORDS every firing, direction, entry, exit and outcome - that
corpus is the input to the meta-labelling work - while UpdateSignalsWeights() no longer writes win
rates back into the pattern weights. That loop is why the classic votes were never evaluable: the
same setup contributed a different amount at different times, so the vote drifted era to era under
the model and nothing could be measured against it. UseDatabaseRanking gates BOTH halves, so turning
the master switch off would have stopped the collection too.
RARITY CUTS BOTH WAYS, recorded rather than glossed: double divergence earns 90 BECAUSE it is rare,
which also means the net will see very few examples and can say little about it. The prior carries
those patterns; the model will learn mostly about the common, low-weight ones.
Compiles clean. Not yet deployed - the Bill Williams suite (Alligator, Fractals, Gator, BWMFI) and
the stdlib ports land next, and one restart should carry all of it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THE PREMISE NEEDED CORRECTING FIRST, and the correction is the useful part of this commit.
The intent was to stop "the database system that changes patterns and signals weights". The signal
database does not change the NN's weights. `ApplyPatternWeight()` opens with
if(m_tiersSelfRanked) return;
and every AI model sets that flag the first time it ranks itself, so the hourly DB win-rate pass
(UpdateSignalsWeights -> ApplyPatternWeight) weights CLASSIC FILTERS and nothing else. Its own
comment says so: "THE SIGNAL DB DOES NOT OUTRANK THE HOLDOUT... without this the ranking pass would
silently undo every era's self-ranking within the hour."
WHAT ACTUALLY MOVES AN AI SIGNAL'S WEIGHTS is RankTiersFromOos() (Lifecycle.mqh), at EVERY pass-3
completion:
w = ShrunkRatePct(effHits, effN, trustPct, TIER_PRIOR_EFF_N); // per tier
ApplyTierWeight(t, wi); // four tier weights
Weight(trustPct / 100.0); // ensemble trust weight
Both are recomputed from the model's own purged held-out fires, era after era. So the same feature
vector maps to a different signal strength depending on WHEN it was evaluated, and a dataset
collected across eras mixes several mappings - any pattern statistics taken from it measure the
ladder as much as the market. That is the thing to freeze for stable collection, and turning off
UseDatabaseRanking would not have touched it.
IT RANKS ONCE, THEN HOLDS - not "never ranks". A member that has never self-ranked is not admitted
to the vote at all (the m_tiersSelfRanked guard at Lifecycle.mqh:566, and the "deployed but WITHOUT A
TIER LADDER (so it cannot vote)" path), so a freeze applied before the first ranking would silence
the ensemble rather than stabilise it. The overlay snapshot still runs unconditionally, so the chart
view keeps refreshing; only the WEIGHTS are held, and one throttled line says so per model.
ALSO WORTH RECORDING: two comments in the tree claim UseDatabaseRanking "ships FALSE"
(TradeJournalManager.mqh:333, Warrior_EA.mq5:1210). It ships `input bool UseDatabaseRanking = true`.
Not changed here - it is a separate question from this freeze, and it also gates the RECORDING half
(table creation and Direction()'s per-tick buffering, Warrior_EA.mq5:2532), so flipping it would
stop collecting the data rather than stop weighting it.
Not in any fingerprint: models resume from disk, nothing retrains.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THE PROBLEM THIS FIXES IS IN MY OWN EVIDENCE. The width cuts (95adc79, 4092a94) chose which feature
blocks to keep from a keep-screen measured on EURUSD, USDJPY, NAS100, XAUUSD and XTIUSD - and were
then judged on the books of those same five charts. The screen selects on MI between column and
label while the book is mean ATR per call, so it is not circular; but it is not INDEPENDENT either.
A column set chosen to be informative on five instruments should be expected to look informative on
those five. That is precisely the shape of result this project keeps promoting and failing to
replicate, and it does not stop being that shape because the numbers moved the right way:
chart 552 inputs -> 372 -> 294 net of ~0.035 ATR cost at 294
XAUUSD +0.538 +0.343 +0.364 +0.329 (29/29 eras positive)
EURUSD -0.009 +0.141 +0.133 +0.098
USDJPY +0.064 +0.023 +0.073 +0.038
NAS100 -0.208 -0.086 +0.036 +0.001 (breakeven, from -0.25)
XTIUSD -0.152 -0.047 -0.161 -0.196 (the one that regressed)
Three of five clear cost and a fourth is at breakeven, up from two at the start of the night. But
every one of those five helped choose the features being tested.
GBPUSD, USDCAD and DAX40 took no part in that selection. They contribute nothing to the screen that
produced the current 49 columns, so their books answer the question the other five cannot: does a
feature set chosen on one panel of instruments pay on instruments it never saw? If it does not, the
cut fitted the panel rather than the market.
GBPUSD was originally dropped from the fleet for being the most correlated with EURUSD - which made
it a poor POOLING candidate, since it adds few independent observations. That is a different role
from the one it has now: as a HELD-OUT test its correlation with the selection panel is a weakness
only if it makes the test too easy, and USDCAD and DAX40 are there to cover that.
NO WIPE AND NO RE-KEY: FLEET_EXPANSION_SYMBOLS is not part of BuildModelFingerprint, so the five
existing charts resume from disk (USDJPY came back at era 25) and only the three new ones start at
era 0. Verified in situ: eight charts, 294 inputs (6 bars x 49 features), zero casting errors, zero
out-of-range, 27 GB headroom.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
IT HAS BEEN INERT SINCE BEFORE THIS SESSION. `EnableCrossAsset` is `const bool ... = false`, the only
thing that ever calls `UseCrossAsset(...)` is Warrior_EA.mq5:819 passing exactly that constant, and
`BuildCrossAssetPanel()` returned at its first line. Its 6 features were never added to the 92, and
its fingerprint tag `|XA:` was never appended - so REMOVING IT CHANGES NO FINGERPRINT AND NO FEATURE
VECTOR. Nothing retrains because of this commit.
WHAT WENT:
System\CrossAsset.mqh 564 lines, deleted outright
the panel build, the per-bar emit block, the feature-table entry (FeatureBuilder)
the member, the accessors, the persistence fields (ExpertSignalAIBase)
three view interfaces + their two implementations
the topology feature-count contribution and the fingerprint tag
the blocking warm-up in OnInit, the sweep-guard rule, the input constant
THE PERSISTENCE FORMAT LOSES A FIELD, and that is safe ONLY because every model was wiped an hour
ago. The .cfg carried a length-prefixed cross-asset pin between the direction-confidence threshold
and the alt-data name list. Both the writer and the two readers (the loader and the cfg walker) drop
it together, so the remaining fields stay aligned - but a .cfg written by the PREVIOUS build would
now have its alt-data pin read from the cross-asset slot. There are no such files.
WHY DELETE RATHER THAN LEAVE IT SWITCHED OFF: it was 117 references across 22 files that every future
reader had to understand before concluding it did nothing - I spent part of tonight doing exactly
that, twice, and got the reasoning wrong the first time (I claimed UseCrossAsset(true) was never
called anywhere; it is called, with a const false). Dead code that takes two passes to prove dead is
not free.
NOT A MEMORY FIX. It was proposed as one and it is not: the panel allocated nothing at runtime. The
27 GB was the indicator tuner (b31f462).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
REMOVED, NOT DISABLED. TuneIndicatorsByFilter() and ScoreCurrentParamsByMI() are gone, along with
the block in TuneIndicatorsAndTrain() that invoked them. AutoTune.mqh 701 -> 473 lines.
WHY REMOVAL AND NOT A FIX. The leak was the cheap half of the problem. The expensive half is that
the search CANNOT WIN:
* It scored candidates by per-column MUTUAL INFORMATION against an overlapping label. That
statistic sits at the noise floor on this data, so 324 candidates per model moved 0.00236 ->
0.00236 on all three AD configs and 0.00370 -> 0.00370 on PAI, and the family-wise gate
correctly refused every winner it ever produced.
* It searched PER CHART. A per-chart winner is a per-chart overfit, and this project ships one
parameter set for every instrument.
* It leaked one terminal indicator instance per candidate (CIndicator::Create overwrites the
handle, it does not release). Measured 2026-09-04: 20,076 live full-history buffers, 27.4 GB
committed and never touched, 202,125 "VirtualAlloc failed in large allocator" lines in a day.
Fixing only the leak would have bought a cheaper way to lose.
KEPT DELIBERATELY, and this is the part a wholesale delete would have broken:
* FeatureColumnMI / BuildMiSample / ScoreMiSample stay. They are the sampling layer under the MI
DIAGNOSTIC, and FeatureScreen.mqh calls all three - that screen produced the per-column table
showing cumdelta clears 4/6 columns on all five charts while wyckoffEvent clears 0/16. The
sampling was never the problem; using it to SELECT was.
* CADIndicatorTuner stays. It still holds the ACTIVE indicator settings ReInitADIndicators builds
handles from. It is now a settings holder, not a search.
* The .nnw indicator-parameter field stays. Dropping it would change the persistence format and
force a second retrain on top of the one the const AutoTuneIndicators already triggered.
WHAT REPLACES IT: Scripts\ResearchADParams.mq5, run once, offline, output edited into the _DEF
constants by hand. It changes the three things that were actually wrong:
1. BLOCK-LEVEL, NOT PER-COLUMN. Fits a block's k buffers jointly against the label, which is what
the network does with them and has far more power than k univariate tests.
2. ATR PER CALL, NOT NATS. The metric is the mean forward RIDE RETURN of the bars the fit ranks
highest - the same currency as the book the deploy gate judges. MI was never convertible to it.
3. SELECT IN-SAMPLE, REPORT OUT-OF-SAMPLE, on a PURGED split. Max-of-N is biased upward by
construction, so the IS number is printed but explicitly labelled as not evidence.
Ranking is MAXIMIN ACROSS CHARTS - a candidate is only as good as its WORST instrument - because a
mean lets one strong chart carry a set that is useless on the other four, which is precisely how
this project has previously promoted results that then failed to replicate.
Every iCustom handle is released in the iteration that created it.
A FLAW IN ITS OWN VERDICT LINE, recorded rather than quietly patched: the script prints "clears
every chart OOS" when maximin OOS > 0. That test is nearly free, because every chart's no-model
baseline is already POSITIVE (+0.09 to +0.74 ATR - the forward ride pays a drift). The meaningful
quantity is the EDGE over that baseline, which the per-chart lines do print. Read the edges, not
the verdict.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MEASURED, NOT INFERRED. The commit charge was 43,221 MB against a 44,498 MB system limit - 97% of
everything this 32 GB box can commit - while the WORKING SET was 1,625 MB. A 26x gap means memory
that is committed and never touched, which is a reservation, not a leak in the usual sense.
HOW IT WAS FOUND, after four wrong guesses (MaxBars, OpenCL, our own arrays, the history cache -
each refuted by measurement, none of them it):
1. Region enumeration of the live process (VirtualQueryEx) instead of more theorising:
20,076 private allocations of EXACTLY 1,433,600 bytes = 179,200 doubles = 27,448 MB
10,038 private allocations of EXACTLY 319,488 bytes = 39,936 doubles = 3,058 MB
179,200 doubles is one full-history buffer at this bar count, rounded to MQL5's 1024-element
allocation granularity. So: twenty thousand live full-history buffers.
2. Same terminal, same 5 charts, same history, EA renamed so it could not load: 154 MB.
That is the whole terminal-side cost. Every one of the missing 33 GB is ours.
3. Commit did NOT scale with chart count (3 charts 33,349 MB, 5 charts 32,189 MB), which killed
the standing "super-linear in charts" theory. It is a fixed cost paid per tuner run.
4. The TERMINAL JOURNAL - logs\, not MQL5\Logs\, which is where I had been looking all day and
the reason this took as long as it did:
202,125 x "VirtualAlloc failed in large allocator" in one day
~155,000 AD custom indicator instances torn down: 33,309 ADCumulativeDelta, 30,424
ADWyckoffEventStream, 30,424 ADShorteningOfThrust, 30,406 ADWyckoffFailedStructure,
30,289 ADWyckoffSignificantBarInversion
Removals arrive in bursts of 6,000-12,600 inside a single minute. A burst that size means that
many were LIVE AT ONCE - churn would be spread out. That is the 20,076 buffers.
[[project_indicator_handle_leak]] diagnosed this exact mechanism on 2026-08-08, wrote down the
diagnostic recipe that found it again, and concluded "AutoTuneIndicators=false is both the unblock
and the right default". It was set to true. This makes it stick.
WHAT THE TUNER BOUGHT FOR 27 GB: nothing, ever, on this data. It has never returned an accepted
winner - 0.00236 -> 0.00236 on all three AD configs, 0.00370 -> 0.00370 on PAI. Its objective sits
at the noise floor ([[project_mi_noise_floor_verdict]]), so the family-wise gate correctly refuses
every candidate. It is a search that cannot win by construction.
const, NOT input bool ... = false. MT5 stores inputs PER CHART, so an attached EA keeps its saved
value and ignores a changed default - the trap that cost a deploy cycle in 2679b02. As a const it
also drops out of the .chr and .tpl input lists, so the fleet template cannot carry the old true.
MEASURED AFTER: 2,850 MB commit / 1,875 MB working set, flat over 5 minutes, 33.8 GB headroom.
Commit now TRACKS the working set instead of standing 20x above it, which is the real tell that the
untouched reservation is gone.
TWO FATAL CHART-KILLERS FIXED ALONGSIDE, both the same shape: an unchecked allocation under the
memory pressure this was causing, then an out-of-range read, which in MQL5 is fatal and REMOVES THE
EXPERT while the terminal keeps running and logging - the silent-dead-chart mode of
[[project_fleet_selfheal_dead_charts]]. Both were observed killing charts today.
* Labels.mqh (666,32), killed XTIUSD at 21:31:51. The prebuild's two tail loops bounded their
reads by m_labelPrebuildBars - the bar count the pass STARTED with - rather than by the array
they were about to index. The identical guard already existed on the loops at 574 and 605; these
two were missed. Also: m_labelCacheBars is the length every reader trusts, and it was set to the
REQUESTED bars even when the resize came back short. It is now set to what was actually
allocated, so a short allocation shortens the window instead of corrupting memory.
* BaselineComparator.mqh, killed XAUUSD at 21:08:45. ALGLIB allocates without checking and then
indexes what it did not get: CDForest asserted "rows(xy)<npoints", the forest then reported a
PERFECT 0.000 OOB error (a failed build, not a good one), and MLPCreateC1 died on "index out of
range in dataanalysis.mqh (8779,41)". These baselines are a diagnostic that nothing downstream
depends on, so they now refuse to start unless TERMINAL_MEMORY_AVAILABLE covers 4x the matrix,
and verify the matrices' actual dimensions before writing a single row.
NOT THE CAUSE, checked because it was proposed: CrossAsset. UseCrossAsset(true) is never called
anywhere - the only writer is the setter itself - so m_useCrossAsset is permanently false,
BuildCrossAssetPanel returns at its first line, Warm() is flag-guarded, and its 6 features are never
added to the 92. It costs nothing at runtime. It is dead code and worth removing for that reason,
but removing it would have recovered zero bytes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TWO OPERATOR REQUESTS.
1) THE OPTIMIZER IS NO LONGER AN INPUT. One input applied one optimizer to all four ensemble
members, which is the opposite of what a voting ensemble wants - members that fail in correlated
ways average to nothing. Each model class now names its own via the new virtual
PreferredOptimizer(), so the choice lives with the architecture instead of in a global switch.
It also had to stop being an input on this project's own rule: it is retrain-forcing (a field of
the model filename), and retrain-forcing values are not inputs, because MT5 stores inputs PER
CHART and an already-attached EA ignores a changed default - the exact trap that cost a deploy
cycle in 2679b02.
ASSIGNED WITHIN WHAT IS ACTUALLY IMPLEMENTED, and no further: only SGD (heavy-ball momentum) and
ADAM exist here, and ADAM already carries DECOUPLED weight decay - it IS AdamW. RMSprop, Nesterov
and amsgrad are NOT implemented and are not pretended to be.
Perceptron / MLP -> SGD, the deliberate odd one out. No structural prior, so it overfits
fastest and adaptive steps make that worse; SGD is an implicit
regulariser and makes it the conservative ANCHOR vote.
Conv / LSTM / ConvLSTM -> ADAM (AdamW).
The learning rate follows the optimizer: InitialEtaForOptimizer() now TAKES it instead of reading
the global, which would have handed every member the same LR and re-created exactly the
uniformity this change removes.
THIS IS ALSO THE CHEAPEST TEST OF THE DIVERSITY HYPOTHESIS. Member correlation is already printed
every era - currently r 0.19-0.36 across the fleet, i.e. 4 models worth ~2.2 independent ones. If
varying the optimizer moves that number, implementing RMSprop/Nesterov/amsgrad is justified by
measurement; if it does not, the hypothesis is refuted for the price of one config line. I expect
a small effect: the members already differ by ARCHITECTURE and still correlate at 0.29 on
average, so the shared input vector and shared label look like the real cause.
2) ERA CAP 100 -> 10000. 100 was arbitrary and far too low - an era here is a full pass over ~179k
bars, and networks of this shape are normally given thousands of them. It no longer terminates
anything either (that was removed the same day), so all a low cap did was fire a redundant deploy
and a log line every 100 eras while charts sat past era 260.
DB key note: the chart-level config fingerprint now hashes TrainingOptimizerDefault rather than any
per-member value, which is correct - per-member differences live in each model FILENAME - and leaves
every existing database key byte-identical.
COST: PAI re-keys to era 0 on every chart, and the ensemble lead cap will hold its siblings until it
catches up. Done now deliberately: the terminal is already restarting for an unrelated MT5 live
update, so nothing converged is being discarded that was not restarting anyway.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit split m_trainingComplete into "may trade" and "has stopped learning", but the
.nnw carries a SINGLE boolean written before that split. PersistLoadNetOnce read it straight into
m_trainingComplete, so any model that deployed under the old build was resurrected frozen.
Observed directly after the restart: the three charts that had deployed earlier (NAS100, XAUUSD,
XTIUSD) completed ZERO eras while the three that never deployed (EURUSD 29, GBPUSD 28, USDJPY 4)
trained normally. Same build, same terminal, same minute - the only difference was what their file
said.
Load now translates a persisted "complete" into m_deployedLive with m_trainingComplete FALSE: the
model comes back TRADING and CARRIES ON LEARNING. Save writes (m_deployedLive || m_trainingComplete)
into the same slot, because writing only m_trainingComplete would silently lose deployed status
across a restart now that deployment no longer sets it - the model would return unable to trade.
Old files migrate on first load. Nothing needs deleting and no era count is lost.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
StartLabelCachePrebuild refused to arm on `m_zigZag.GetData(0, 0) == EMPTY_VALUE`, reported as "the
handle is COLD or DEAD". But a HEALTHY ZigZag is empty at almost every bar - it carries a value only
AT a pivot - so the test fired whenever the newest bar simply was not one. While ticks arrive, bar 0
keeps moving and the condition clears by luck. The moment the market CLOSES, bar 0 freezes on a
non-pivot and the prebuild can NEVER arm again.
MEASURED, same evening, same build otherwise:
* 16:47 restart (broker ~20:47, market open): 0 such messages, first era completed in 50 seconds.
* 19:26 restart (broker ~23:26, week closing): 150 such messages and NOT ONE chart completed a
single era in 15 minutes.
The handle was never cold. The terminal journal reported "custom indicator ZigZag loaded succesfully"
on all six charts, and the LEG STATE REPLICA verified against that same buffer across 1299 pivots
while the prebuild was calling it dead.
BarsCalculated() <= 0 is the actual readiness signal - it means the indicator has produced nothing,
which is what "cold or dead" was meant to catch, and it does not depend on whether any particular bar
happens to be a pivot.
WHY IT MATTERS BEYOND TONIGHT: it is Friday. Without this the fleet sits idle for the entire weekend
while appearing healthy - the terminal alive, all six charts logging, no errors, no rejections. The
only symptom is the absence of eras, which is exactly the class of silent failure this project keeps
getting caught by.
I first suspected my own lifecycle change from the preceding commit. The diff was clean and the
market-open/market-closed comparison above is what separated the two.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Operator instruction, given more than once: "let the nn learn, there should not be a gate. we only
filter inputs by correlation."
m_trainingComplete meant TWO things at once - "this model may trade" (LongCondition/ShortCondition
return 0 without it) and "this model has stopped learning" (trainingPending gates on it). So
deploying froze the chart. Measured today: THREE of six charts had stopped training, and two of them
froze on books the gate itself had refused - XTIUSD deployed after -0.198/-0.013/+0.026/-0.121, all
four NOT DEPLOYABLE. A frozen chart also stops publishing rows to the shared training pool, which is
the one lever that ADDS independent observations, so freezing quietly undid the reason for running
six instruments.
Split into m_deployedLive (may trade) and m_trainingComplete (has stopped learning). Deployment now
sets the first and leaves the second alone, so a later era can still promote a better checkpoint.
Four sites changed:
* ENSEMBLE DEPLOY: goes live, no FinalizeTrainRun, and the once-only guard is now keyed to
m_deployedLive - keying it to m_trainingComplete would have re-fired the branch every call and
returned before training ran, a HARDER freeze than the one being removed.
* era-end deploy: no era.stop, no completion; announces once.
* LongCondition/ShortCondition: m_deployedLive also permits trading.
* the lifecycle if/else became TWO independent tests, so a deployed model refreshes its live signal
AND keeps training. A merely-complete model still takes only the first branch, as before.
THE ERA CAP IS GONE AS A STOP, and it also stopped asking. PromptContinuePastEraCap opens a
MessageBox in a LIVE terminal - on an unattended chart that blocks the EA thread waiting for a click -
and on "no" it set m_trainingComplete and ended the run for good. Both halves are gates on learning.
The cap now does the only useful half of its old job: it takes the best checkpoint live and resets
the cap window so training continues.
No m_trainingComplete = true remains anywhere in Training.mqh.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MEASURED, and the estimate that justified the expansion was wrong. I sized the step 3 -> 6 at ~1 GB
per chart from the 3-chart steady state. The real figure is ~5.8 GB per chart COMMITTED: six charts
reached 34.8 GB and left 4.2 GB of system headroom, against the 2.6 GB at which the operator VS Code
was dying this morning.
THE COST IS ADDRESS SPACE, NOT RAM. Working set was 7.4 GB against 34.8 GB committed - about 1.2 GB
touched per chart against 5.8 GB reserved. Windows charges the reservation against the commit limit
(RAM + pagefile), so the unlock is a LARGER PAGEFILE, not fewer charts. That needs admin this session
does not have.
AND THE LIMIT ITSELF MOVES: 49,650 -> 44,498 -> 48,113 MB across today, because the pagefile is
auto-managed. Headroom can therefore fall by several GB without anything being allocated, which is
why 4.2 GB is not a safe margin to leave unattended even though it was stable while watched.
GBPUSD is the one dropped: most correlated with EURUSD in the set, so it adds the fewest INDEPENDENT
observations, which is the entire point of pooling. Remaining set spans an FX major, JPY, an index,
a metal and an energy.
NOTE: this only stops GBPUSD being RE-ADDED. FleetExpansion has no close path - it opens and revives
only - so the already-open chart stays until closed by hand or by a deliberate contraction feature.
I did not add self-closing charts unattended; a bug in that path closes the fleet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Everything that removes PARAMETERS was tried and measured this session and none of it reaches the
budget:
* correlation pruning: 92 -> 54 columns even at |r| >= 0.60, which already deletes genuinely
different features
* STANDARDISED PCA: 45-46 of 92 components carry 95% of the variance. The "8 components" figure the
PCA plan was built on came from RAW-variance PCA and was almost entirely a scale artefact - on
the correlation matrix the data genuinely spans ~45 dimensions. k=8 would discard real
information and k=45 does not cut enough.
So there is no redundancy left to reduce away, and the constraint is the SAMPLE, not the encoding.
Adding an instrument is the only lever that ADDS independent observations instead of removing
parameters - the capacity line has said so all along.
Pooling is already working (each chart adopts 8-16k rows from its two peers), so this multiplies
something that is already plumbed rather than starting it.
WHY 6 AND NOT BACK TO 10. The fleet was cut to 3 this morning for an OOM whose root cause is now
fixed and measured - eleven masked-out feature blocks were still loading and computing, commit
27.8 GB -> 3.3 GB, headroom 2.6 GB -> 32.7 GB. The operator instruction was "max 3 charts IF it is an
OOM crash"; the premise has changed but the caution has not, so this steps 3 -> 6 (~1 GB per chart,
leaving ~27 GB headroom) and measures before going further.
CHOSEN FOR INDEPENDENCE. Pooling correlated instruments adds rows without adding much information,
so the set spans FX majors, JPY, an index, a metal and an energy. XAUUSD and XTIUSD are also the two
the leg-ride label actually paid on in its first-run verdict (+0.467 on gold, positive long book on
oil) while FX was ~zero and the index negative.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The "8 of 92 columns carry 95% of the variance" figure that the PCA plan is about to be built on
came from PCABuildBasis run on RAW columns, so it ranks directions by RAW variance - and these
columns are not on a common scale. FEATURE HEALTH measures widest/median at ~21x today, and before
the Wyckoff zone sentinel was fixed it was 1137x, at which point a SINGLE COLUMN was the entire
first component and the report said "1 of 92 carry 95%, top component 100%". A raw-variance
component count therefore partly measures our unit choices rather than the information.
Standardising each column to unit variance first makes PCA operate on the CORRELATION matrix, which
is the scale-free version and the only defensible basis for choosing k. It matters more now than it
would have a week ago: the supervised keep-screen is gone, so PCA runs on unscreened columns and
nothing else is left to stop scale deciding the answer.
Both counts are printed, because the GAP is the diagnostic - if standardising moves the number a
lot, the raw figure was measuring units. Constant columns are left at 0 rather than divided by a
~0 sd, which keeps them out of the basis instead of inventing a direction for them.
The report also states the rule the transform will have to follow: k comes from the STANDARDISED
number, and the transform must standardise with the SAME per-column mean and sd it was fitted with -
fitting on one scaling and applying another is the silent version of this same bug.
Measurement only. No feature vector changes, no fingerprint change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Shipped an hour ago asserting "the encoder is at its minimum hidden size, so the only term left to
cut is FEATURES PER BAR". The live readings are H=16 and H=32 against a floor of 8, so that is
simply false, and it made the line understate the options: at H=32 it reported F<=0 for alpha=2,
which reads as impossible, when dropping H to the floor affords F<=48 on the same budget.
An LSTM costs 4*H*(H+F+1). H and F trade against each other and BOTH are levers. The line now prints
the affordable F at the CURRENT H and at the floor, and says plainly that cutting H costs
representational depth while cutting F costs information - so which gives way is a judgement, not an
arithmetic result. What the arithmetic does say is that at alpha=0.40 both have to move, and no
choice of H alone reaches the band while F stays at 92.
I wrote the false claim from the earlier chart where H was 8 and generalised it without checking the
others.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The capacity line now reports a real whole-network weight count, and the first honest readings are
11018 weights = 3.03 per independent observation on the widest model and 1738 = 0.48 on the
narrowest. The old line said 0.2, because it counted the first dense layer only.
"3.03 weights per observation" only means something against a target, so the standard bound is
stated with it: total parameters <= N_eff / alpha, alpha in [2,10]. At N_eff ~3763 that is 1882
weights at alpha=2, 753 at alpha=5, 376 at alpha=10 - and the widest model implies alpha=0.33, i.e.
six to thirty times outside the band.
THE LINE ALSO NAMES THE ONLY REMAINING LEVER, because the ratio alone does not. Both stages are
ALREADY budgeted against effN individually - ComputeFirstLayerWidth solves H1 = N_eff/(N_in+1) and
ComputeLstmHiddenSize solves 4*H*(H+inputs+1) <= N_eff - so this is not a missing constraint, it is
two constraints applied SEPARATELY whose sum is 2-3x either one. And the encoder is already at
LSTM_HIDDEN_MIN on these charts, so hidden units cannot come down further.
That leaves FEATURES PER BAR. An LSTM costs 4*H*(H + F + 1), so at the minimum H the affordable F
follows directly: F<=50 at alpha=2, F<=14.5 at alpha=5, against the 92 emitted now. The line prints
those numbers per chart so the feature set can be designed against a budget instead of an opinion.
That also gives PCA a target rather than a guess: 8 components carry 95% of the variance (measured
after the Wyckoff sentinel fix), and 8 features per bar sits inside the alpha=5 budget of 14.5.
Required IFeaturesView::LstmHiddenSize, since the budget arithmetic needs the encoder width.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TWO FIXES, THE FIRST OF WHICH IS AN OUTAGE I CAUSED.
1. FATAL CAST. The weight walk did `CNeuronBase *n = layer.At(i)` - an implicit downcast off
CLayer::At(), which returns CObject*. A layer can hold CNeuronBaseOCL, a SEPARATE hierarchy that
does not derive from CNeuronBase, so the cast raised "incorrect casting of pointers in
Network.mqh (683,22)". That is FATAL in MQL5 and REMOVES THE EXPERT FROM THE CHART: all three
charts stopped training at 14:56 and stayed down for 30 minutes, with the terminal alive and the
log still writing, which is exactly the signature project_fleet_selfheal_dead_charts describes.
Every cast in the walk is now a dynamic_cast, which returns NULL on a mismatch.
2. THE COUNT THEN RETURNED 0, because these models run on the DLL/OpenCL stack and every
dynamic_cast to the CPU hierarchy legitimately missed. The report printed "0 trainable weights =
0.00 per independent observation" - the most reassuring lie this line could tell, on the exact
question it exists to answer. WarriorNetTotalWeights now handles BOTH hierarchies (CNeuronBase
via WeightCount, CNeuronBaseOCL via getWeights) and lives at the END of Network.mqh because it
must see a class declared after CNet. When it still cannot count, the line says capacity is
UNKNOWN rather than zero.
The lesson is the one this file keeps teaching: a diagnostic that fails silently to a flattering
value is worse than no diagnostic. A zero here is a failure to count, not a measurement, and it must
read as one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It reported "first dense layer is 9 x 16 = 144 weights against ~868 independent observations = 0.2
weights per observation" - a comfortable number - while IS/OOS said the same models were overfitting
13-90x. The two disagreed because the diagnostic was measuring a SLICE: on a sequence model the
first DENSE layer sits AFTER the encoder, so it sees ~9 inputs, not the 552 that enter the network.
A capacity figure that ignores the encoder is not a capacity figure, and this one was quietly
reassuring on the exact question it existed to answer.
Added a virtual CNeuronBase::WeightCount(), overridden where the weights do not live in Connections:
CNeuronPool counts its OutputLayer, CNeuronLSTM counts its FOUR GATE LAYERS plus the output layer.
CNet::TotalTrainableWeights() walks the stack. The CAPACITY line now prints both - the first-layer
figure it always printed, and the whole-network total with its own weights-per-observation - and
says plainly which one to judge capacity on.
I also have to correct myself: earlier today I asserted the first dense layer was "~17,600 weights
against 3,763 observations". That was wrong for the sequence models - it assumed the raw 552 inputs
fed a dense layer directly. The direction of the concern survives (the encoder holds thousands of
weights nobody was counting) but the number I gave did not, and the fix is to measure it rather than
to argue about it.
Context for why this matters now: correlation pruning cannot rescue the width. Sweeping the prune
threshold gives 92 -> 82/78/69/59/54 columns at |r| >= 0.95/0.90/0.80/0.70/0.60, i.e. 552 -> 324
inputs even at an aggressive 0.60 that starts deleting genuinely different features. Redundancy is
not the whole story, so the real parameter count is the thing that has to be known.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A prune at one threshold answers "how much is duplicated at 0.90". It does not answer the question
that matters: whether correlation pruning ALONE can cut the input width far enough to matter against
~3,763 independent legs. At 0.90 it drops 15 of 92 columns - 552 -> 462 inputs, a 16% cut - which is
nowhere near enough for an IS/OOS gap of 13-90x.
So the report now sweeps |r| in {0.95, 0.90, 0.80, 0.70, 0.60} and prints columns and resulting input
width at each. If even the aggressive end leaves the width large, redundancy is NOT the whole story
and the rest has to come off by ROTATION (PCA) or by shortening the lag window - pruning can only
remove information that is DUPLICATED, never information that is genuinely there.
What the 0.90 pass already found is worth recording: two pairs at |r| = 1.000.
* cumdelta[1]~cumdelta[0] - not our bug. ADCumulativeDelta defines
pressure = normalizedCumulativeDelta + 0.25*initiative - 0.15*absorption, so buffer 0 is a
near-copy of buffer 1 and the adjustments are negligible against the variance.
* wyckoffEvent[6]~wyckoffEvent[0] - event direction and struct direction share a sign exactly.
Feeding both of each pair is pure waste, and the prune found it without being told what to look for,
which is the point of filtering on the INPUTS rather than on the label.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The redundancy report counted redundant PAIRS and named the worst by INDEX ("worst is columns
54/60"), which nobody can act on. It now names them and runs the prune the operator asked for:
filter on CORRELATION - a question about the INPUTS, needing no label - rather than on marginal
significance, which is the network job and which structurally cannot see a feature that is useless
alone and useful in combination.
GREEDY, LOWEST-INDEX-WINS: walk columns in emitted order, drop any that correlates above the
threshold with one already kept. Deterministic, no fitted basis to persist, no IS/OOS split, no
fingerprint of its own - unlike PCA, which rotates the space and must carry a basis with the model.
Start here; reach for PCA only if this does not cut enough. Reports what a prune WOULD leave
(columns, lags, resulting input width) and the dropped columns with the twin each duplicates.
MEASURED, NOT APPLIED.
WHY NOW, and it is the most urgent number on the page: IS/OOS has blown out to 13-90x. EURUSD books
+2.1..+3.4 ATR per call in-sample against +0.06..+0.09 out; USDJPY +1.5..+2.2 against +0.03..+0.16;
NAS100 is NEGATIVE out-of-sample on 4 of 5 recent eras. Memory records this gap as 3-5x, and the
input width tripled (180 -> 552) in between. That is textbook over-parameterisation against ~3,763
independent legs, and redundancy reduction is the only lever that cuts width WITHOUT discarding
information.
Required a small interface addition: ITrainingData::FeatureSlotName, so a report living in the
comparator can name a column owned by the feature builder.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>