forked from animatedread/Warrior_EA
942 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c6f70d8ee1 |
refactor(inputs): remove the Neural Networks section - seven dialog switches that did nothing
Follow-up to the training deletion. The whole "Neural Networks" input section survived the file
deletions and every one of its knobs was inert: Use_MLP / Use_CONV / Use_LSTM / Use_CONVLSTM,
Run_Alglib_Baselines, Use_Training_Pool and OOSSplit are inputs the operator sees in the dialog,
and after the cut none of them reached anything. Exit_On_Leg_Flip was the same - a switch for a
label that no longer exists. 132 lines, and Inputs.mqh drops 1,022 -> 891.
An inert switch in the Inputs tab is worse than a deleted one: it invites the operator to change
a setting and conclude the EA ignores them, which is close to what the complaint that started
this refactor actually was.
TWO SLOTS DELIBERATELY KEPT AS LITERALS so no existing database is re-keyed. DbLegacyAiSlot()
encoded which architectures were enabled and now returns the legacy AI_NONE value; the optimizer
slot is written as 1, ADAM's ordinal, which is what TrainingOptimizerDefault always carried
(ENUM_OPTIMIZATION pins SGD/ADAM at 0/1 precisely so this cannot drift). I first wrote 0 there,
which would have silently changed the fingerprint on every database this EA has written.
The trade journal's run label was the enabled NN roster ("MLP+LSTM"); it is now "Book".
Compiles 0 errors / 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
4e5e6fcae3 |
refactor(ea): delete on-chart training - the EA stops learning and starts executing
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> |
||
|
|
5ffab1a84a |
docs(refactor): the plan, measured against the code and a real tester log
The operator's complaint is three complaints with three causes, so this separates them and orders the work. Written after reading the code and a 1,570,535-line tester log rather than from the shape of the codebase. The framing fact, verified in situ: the EA wires ZERO enabled book setups (ClassicSignals.mqh:100, EnableInsideBarGap = false, and it is the only one that exists in MQL5), while the research repo has 36 book triggers built and measured. What the EA trades is the leg-ride NN vote. Almost every mechanism in the "whether to trade at all" column exists to serve on-chart training and its deploy decision, so the deletions are downstream of moving the net offline - and the wiring has to come first. Records the confluence specification the research just produced, because it is what the EA should be executing and what the meta-label net has to beat: one setup alone is negative everywhere, three or more agreeing is +1.39 bp pooled OOS and +7.80 bp on BTCUSD at M60 after a full 6 bp round turn, both sides firing is a veto, and the timeframe sets the base. Also records, as the worked example of "mechanisms fighting", the 2026-09-06 sequence where a setup asked for a buy stop at 2930.49 with a 14 bp stop and got a market buy at 2929.46 with a 2 ATR stop, closed twenty seconds later by the protective path. That log predates the three fixes of 09-06/07 and the document says so - re-running it is slice 2, and slice 2 is left for the operator because starting the fleet terminal unattended is not mine to do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0686410b42 |
refactor(logs): two verbosity levels, and a throttle that works in the tester
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> |
||
|
|
f4c36eebce |
fix(fleet): stop the sweep that printed on every bar of every tester run
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>
|
||
|
|
6df9132274 |
fix(gate+label): certify the trade the EA places - traded population, real fills, no survivorship
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> |
||
|
|
84d0af6e91 |
fix(vote): the classic modules were the live book - inputs, not voters; certify with the live stop
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> |
||
|
|
a765e0f61f |
fix(news): weight by scheduled importance, not the post-release verdict
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>
|
||
|
|
50fad051d4 |
fix(news): the calendar was not missing, it was still synchronising
The export now works: 200,507 releases of 1,494 distinct events, 2010.01.01 to 2026.09.06, written on all eight charts and loading clean through research/newsdata.py. What it actually was: none of the four query variants. The identical (NULL, NULL) call that returned zero rows at 16:12 returned everything at 16:22 - the calendar base had simply not finished synchronising, which the original failure message offered as a possibility and which I then talked myself out of, twice: first into "this broker has no calendar feed" (wrong - the Calendar tab comes from MetaQuotes, only the News tab is the broker's, and the empty news.dat I reasoned from was the wrong subsystem), then into "the query is being refused" (also wrong). So the variants stay - a terminal that genuinely refuses the query is still something this can meet, and a silent zero is precisely the failure that cost hours - but the normal path must not pay for them in log noise. The plain call now runs SILENTLY, and only when it comes back empty does anything print, at which point it prints everything: CalendarCountries(), then each variant with its n and error. The failure message leads with synchronisation, since that is what it actually was. Compile-verified in _claude_stage: 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
dc6daa2ff9 |
fix(news): probe the calendar call instead of declaring the feed absent
The operator checked the Toolbox: the Calendar tab is FULL. Calendar data comes
from MetaQuotes' servers; the News tab is the broker's own feed and is separately
empty. So my conclusion this morning - "this broker delivers no calendar" - was
wrong, and it was built on proxies: an empty news.dat, which is the unrelated
news-headline subsystem, and the absence of any file named *calendar* on disk,
which only means the store is not named what I guessed. One glance at the tab
settled what three filesystem checks could not.
The real fact is narrower and stranger: CalendarValueHistory over 2010..now with
NULL country and NULL currency returns zero rows and error 0 - success, no data -
while the terminal plainly holds the data. So the script now probes before it
exports, reporting CalendarCountries() and then trying the call four ways with n
and error printed for each: (NULL,NULL), ("",""), a narrow recent range, and a
per-country loop over CalendarCountries(). It exports the first variant that
returns rows, preferring the widest, and if all four are empty it says so rather
than offering a cause.
Compile-verified in _claude_stage: 0 errors, 0 warnings. Deployed to the fleet
terminal's MQL5\Scripts so it can be dragged onto a chart.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
15a1e18a5c |
feat(news): standalone calendar dump, for a terminal that actually has one
Measured on a live attach today: the fleet terminal returns ZERO calendar values with error 0, news.dat is a 428-byte empty header, and no calendar base exists on disk. NewsEnable=1, so it is the broker and not a local setting - Five Percent Online does not deliver the calendar. NewsExport.mqh is correct and has nothing to export there, which no amount of EA work changes. FILE_COMMON resolves to the same Common\Files folder for every standard MT5 install of one Windows user, so a terminal that DOES carry a calendar can write the file the research reads without anything about the fleet terminal changing. A script rather than the EA because that terminal is a borrowed one: it should not have to host a trading system, its symbols and its models to hand over a CSV. Column-for-column identical to NewsExport.mqh, since newsdata.py reads whichever of the two wrote the file. Does not replace Scripts/CalendarRecorder.mq5 and says so in its header. That one exists because actual_value is post-revision, so a surprise from history is leaky; this dumps history, which is honest for the columns that are never restated - the release TIME and importance - and those are what the T-5/T+15 blackout and the two news setups need. Compile-verified in _claude_stage: 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9cfcb5f42d |
fix(news): export the calendar on the upkeep tick, not only at attach
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
|
||
|
|
eb2a443582 |
fix(setup): the entry window is measured in BARS, as every book states - the 60 seconds came from an artefact
No book counts an entry window in seconds. The Forex book offers next-bar-only, next-2, next-3 or unlimited and deliberately refuses to fix one (p199); the Bitcoin book fixes the immediately following bar and says the setup is dead if it does not trigger there (p117). The sixty seconds in this class was fitted to a measurement artefact, not read from a book: the fast-fill population it selected for was 87% session-gap fills booked at the mother-bar high while the market had opened far above it, which is where the whole apparent +0.398 R came from. Removing the artefact removes the parameter. m_entryWindowBars defaults to 1 - the tightest reading either book offers - and the expiry is computed from the START of the bar the order is placed on. The per-tick expiry clock and the GTC fallback in CExpertCustom stay: those were correct and are general to any setup with a window. The class header still advertised the sixty seconds as the validated rule; corrected. Compile-verified in _claude_stage: 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0dcad40f38 |
feat(news): export the terminal's own economic calendar - the two news setups become buildable
The operator found a paid MQL5 product that exports historical news, and drew the right
conclusion: MT5 carries the calendar itself and exposes it to MQL5, so the EA can export it the
same way it already maintains alt data. Verified before writing anything - a probe script
compiled clean against CalendarValueHistory / CalendarEventById / CalendarCountryById and the
full MqlCalendarValue field set on this terminal.
THE CONSTRAINT THAT SHAPES IT: the calendar API returns nothing inside the Strategy Tester. So
this follows the alt-data contract exactly - a LIVE chart writes
Common\Files\Warrior_EA\News\calendar.csv, and backtests, research scripts and models all read
the FILE. In the tester it says so once and leaves the existing file alone, because a backtest
must never truncate what a live session wrote.
WHAT IT UNBLOCKS, and it is three things rather than one:
1. The two NEWS SETUPS - two of the Forex book's six, unbuildable until now. The News Straddle
carries the only explicit R-multiple in the whole book (~8:1).
2. The news AVOIDANCE rule both Walsh books state and we have never honoured: flat five minutes
before a release, nothing new until fifteen after (forex s13). Every backtest so far has
been holding through releases it should have been flat for.
3. News as FEATURES for the networks - time to the next release, its importance, and the
surprise itself (actual minus forecast), which is an axis price data does not carry.
SCALING, STATED HONESTLY. MqlCalendarValue carries actual/forecast/previous as longs and
MetaQuotes documents them as the real value times a million, with LONG_MIN meaning "no value".
That is documentation, not something this code has observed, so every row carries BOTH the
scaled reading and the raw long, plus digits/unit/multiplier - the first live export settles the
convention and nothing downstream has to trust it in the meantime.
Written atomically through AtomicFile, refreshed hourly beside the alt-data fetch.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
b647b745ec |
feat(inputs): Asset_Class - the operator declares which book applies to this chart
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> |
||
|
|
c5da78dff3 |
feat(setup): the level is a breakout - if the market is already through it, we PASS and log it
Operator: "it's okay to miss certain entries as well, if we do not get filled, we log and wait
for the next setup. that's discipline."
This entry requires the market to trade UP THROUGH the mother bar's high. When the session has
already opened beyond it - on SP500 that is the 01:00 open after the daily break, and it is
most of them - a buy stop is impossible and the trade layer routed a buy LIMIT instead: a
pullback entry the books never described and the research never measured. LongCondition now
declares the setup VOID in that case, logs it with its full 48-feature context, and waits.
That is not a safety measure, it is the better book. Measured on SP500 M60 2019-2026:
all signals chased +0.049 (n=3,244) -> disciplined +0.060 (n=2,518, missed 22%)
the rule chased +0.063 (n= 185) -> disciplined +0.066 (n= 102, missed 45%)
Every occurrence of the pattern is now written to Adapt\features_{SYM}_{PERIOD}.csv with a
status - taken, void_gapped, void_network - so what we passed on is exactly as analysable as
what we took. The passes are data; that is the point of the journal.
The class header carried the old headline (+0.116 / +0.285 out of sample, "the order expiry is
the load-bearing part"). It has been rewritten to what is actually true: 87% of the rule's
minute-0 fills were gap-throughs booked at a price never offered, the SP500 92.6% win rate was
the tell, and honestly priced the book is +0.044 R at 52%. EnableInsideBarGap stays FALSE.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
f5a3aaa131 |
chore(scripts): tester_results.py - one pass over a finished tester run (log counts, report, journal rows)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
92fa576224 |
feat(setup): the setup owns its trade - delegation, network confluence, feature port, expiry fallback
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>
|
||
|
|
4004c48d1b |
feat(journal+signals): the journal becomes a research journal, the setup becomes a voter, and the exit adapts
THE VOTER FIX, found in the strategy tester and worth stating plainly because it
will bite the next standalone setup too. CExpertSignalCustom weights each child
by VoteCapableWeight(), which is m_weight only when GetPatternCount() > 0 and
ZERO otherwise: a child with no patterns is a news/session-style VETO with no say
in the consensus. CSignalInsideBarGap declared none, so its first tester run
fired 192 times over 2019-2022 and placed nothing - it voted 100 with a capable
weight of 0 and the accumulator divided by zero into 0.00 on every bar. It now
carries one pattern at weight 100, an ID, and its used-series mask, exactly as
every classic module's constructor does. Tester validation of the fixed build is
in progress; this commit is compile-verified only (0 errors, 0 warnings, in a
scratch copy inside the MQL5 tree - the repo path cannot resolve Network.cl).
THE JOURNAL. Two TEXT columns, added IN PLACE with ALTER TABLE (new
EnsureColumn on the database managers) rather than a dbVersion bump, which wipes
the directory:
context key=value;... the setup's view at signal time, the same terms the
research conditioned on, named identically (PublishContext). Accepted
by the journal only when published within 180 s of the position
opening, so an expired order's line cannot be inherited by a later
position from another signal.
passages first-touch MINUTE of every stop and target level on the research
grid plus signed R at each time-stop horizon - the exact fields
research/pricing.py prices an exit from. A synthetic round-trip
proved the live format re-prices the traded exit identically to the
research (max |diff| 4e-7 R) once the horizons carry six decimals.
One honest limit is written into the file header: a live trade is closed by its
own barriers, so from live rows an exit can only be re-selected TIGHTER.
THE ADAPTIVE EXIT. CSignalInsideBarGap reads
Common\Files\Warrior_EA\Adapt\{SYMBOL}_{PERIOD}_inside_bar_u.cfg once per server
day, written by research/journal_adapt.py with the research's discipline (choose
on the older trades, confirm on the newer, placeable stop, stable after dropping
the best 5%). No file means the validated defaults; a file outside the research
grid is refused and logged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
358c8b74e0 |
feat(signals): CSignalInsideBarGap - the first research-validated standalone book
M60 inside bar, buy stop at the MOTHER bar's high, risk down to the inside bar's
low, stop 1.25R, target 8.0R, 15-minute time stop, and a SIXTY-SECOND ORDER
EXPIRY that carries the edge.
Validated in Warrior_Research (RESULTS.md, tag promising-results-20260906) on
2003-2026 across eight instruments, selected pre-2019 and measured on 2019+:
unfiltered +0.116 R/trade (t 4.57, n 1,203)
with the context rule +0.285 R (t 6.45, n 412), and 2022+ +0.309 - stronger
recently than in sample
The expiry is the finding, not the pattern. By time-to-fill, orders filling in
the first minute paid +0.398 R in sample and +0.116 out; every later fill paid
-0.004 and -0.015. Only ~4% fill that fast. Being an ORDER parameter rather than
a model feature, it is causal by construction and needs no run-time inference.
Maps onto the existing architecture without changing it: LongCondition() fires on
the closed bar, and OpenLongParams() returns a price ABOVE the market, which is
what makes CExpert place a pending buy stop - the order type the research
measured, and one that fills at its own level instead of crossing the spread.
The expiration is set absolutely to 60 s rather than the base class's whole
chart period, because that is the whole point.
Definitions match the research exactly and are pinned in the class comment, since
they are easy to "tidy up" into something that no longer matches what was
measured: entry is the MOTHER bar's high, and valid_test is Wyckoff p118 - volume
below EACH of the two preceding bars, a two-bar lookback and not a moving average.
EnableInsideBarGap DEFAULTS TO FALSE. Unlike the vote modules it sits beside, this
fires at weight 100 and names its own entry, stop and target, so enabling it
changes what the expert trades rather than how it weighs an opinion. Shorts return
0 deliberately: the mirror setup measured +0.054 R against the long side's +0.285.
Compiles clean - the expert built with exactly the same single pre-existing error
(Network.cl resource path, unrelated) and zero warnings, before and after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
035a4920bc |
docs(research): pipeline analysis, and the two scripts the research depended on
DumpSymbolSpecs exports symbol specifications and deal history so the research cost model uses the account's real commission and swap rather than assumptions. ExportIndicatorBuffers dumps every AD/Wyckoff indicator buffer over full history, so the research conditions on the PRODUCTION detectors rather than a Python re-implementation of them - which is what made the Wyckoff verdict a verdict on the indicators rather than on my approximation of them. Both are read-only: handles, CopyBuffer, and writes under Common\Files. No orders, no chart changes, no writes to any model or AltData file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
52baf7cea5 |
refactor(signals): one predicate, one replay bracket - DRY the classic vote paths
I introduced two duplications in
|
||
|
|
3a925fa1d9 |
feat(features): the classic vote vector becomes model INPUT - meta-labelling wired end to end
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> |
||
|
|
095bd27a67 |
feat(signals): restore CCI, Stochastic, WPR, RVI and Parabolic SAR
Five more modules, 16 patterns, recovered from
|
||
|
|
c99020abb7 |
feat(signals): complete the Bill Williams suite - Alligator, Fractals, Gator, BWMFI
Four new modules, 11 patterns, built on the stdlib CiAlligator/CiFractals/CiGator/CiBWMFI
indicator classes and ported to CExpertSignalCustom like the rest.
ALLIGATOR (4). Three smoothed averages of median price, displaced forward - 13/8, 8/5, 5/3, which
is Williams specification rather than a choice. Patterns encode his own reading: ordered lines are
context (10), a lips/teeth cross is the trigger (40), price clear of the lips with the mouth still
opening is the trend running (45), and AWAKENING - previous bar intertwined, this one ordered -
carries the most weight (55) because it times the transition instead of reporting a state that may
have been true for twenty bars. A SLEEPING alligator produces no vote at all rather than a weak one.
FRACTALS (3). A five-bar pattern, and the two-bar confirmation delay is the whole difficulty: a
fractal at bar i is only knowable at i+2, so reading it at i is exactly the lookahead that faked a
+4 sigma reading in
|
||
|
|
3ed053e3a8 |
feat(signals): restore the classic votes as a meta-labelling PRIMARY, with fixed weights
WHAT AND WHY. |
||
|
|
416f232226 |
feat(signals): freeze the signal weights after the first self-ranking
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>
|
||
|
|
57abd6d31d |
test(fleet): add GBPUSD, USDCAD and DAX40 as HELD-OUT instruments for the width cut
THE PROBLEM THIS FIXES IS IN MY OWN EVIDENCE. The width cuts ( |
||
|
|
4092a94b23 |
feat(features): second width cut 372 -> 294 - Ichimoku and Wyckoff-failed-structure off
SAME EVIDENCE BASE AS |
||
|
|
95adc798b7 |
feat(features): cut the input width 552 -> 372 on doubly-replicated evidence
THE BINDING CONSTRAINT ON THIS PROJECT IS INPUT WIDTH, and this is the first change that actually
moves it. The first-layer budget effN/(width+1) went from 1.6-6.6 to 7.0-9.7 across the fleet,
against a rule-of-thumb 10-30 it had never been close to.
WHAT WAS MEASURED. The per-column keep-screen (MI against the leg-ride label, Benjamini-Hochberg at
q=0.10) was run on all five charts TWICE, on two independent cold starts with every model wiped in
between - so the second reading shares no weights, no pool and no cache with the first.
* The screen is sound: 0.0147-0.0173 nats/feature against a shuffled-label null of
0.0021 +/- 0.0002, p=0.005 over 200 permutations; strongest single column 0.14 vs a null max of
0.0068. That is 7-8x the null.
* The screen REPLICATES: per-chart hex masks differ between the two runs by one or two nibbles,
29 columns clear on all five charts in BOTH runs, 28 clear on none in either, and only 5 of 92
columns disagree at all.
SWITCHED OFF (each has ZERO columns clearing on all five charts, in both runs):
ma 0/5, rsi 0/1, macd 0/3 - dead on every chart in both runs
swing 0/7, fracdiff 0/3, volume 0/4, time 0/6, atr 0/1
30 columns, 180 inputs. As `const`, not a mask - the indicators are never created and never
computed, because "a mask that hides output is not a switch that stops work".
WHAT CARRIES THE SIGNAL, all 29 unanimous columns: cumdelta 6/6, wyckoffEvent 12/16, alt 7/12,
sot 2/4, wyckoffBarInv 2/5. Every CLASSIC technical block contributes nothing that survives on all
five instruments; the Wyckoff suite, cumulative delta and the alt block carry it.
THAT REVERSES THE 2026-09-03 READING, where `ma` was the strongest block (5/5 on five charts) and
wyckoffEvent was dead on 5/5 - and the reversal has a cause rather than being drift: ADWyckoffEvent-
Stream had a PROVABLY UNREACHABLE continuation branch until it was fixed hours ago, and the chart
set changed.
EnableMAFeature and EnableSwingContext were `input` and are now `const` with the other six. They
move m_neuronsCount, field 4 of the model fingerprint, so they are retrain-forcing - and MT5 stores
inputs PER CHART, so changing an input default reaches no already-attached chart.
A MAPPING ERROR I MADE AND CORRECTED, in the file rather than quietly: my first pass assumed
fracdiff was 8 columns and alt was 7. FRACDIFF_COLUMNS is 3 and alt emits 12, so every block after
fracdiff was attributed columns shifted by +5. The eight blocks below are unaffected - each is 0/N
unanimous under both mappings, and 92-30=62 emitted columns confirms the corrected layout - but the
verdicts I first wrote for ichimoku (claimed 3/8, actually 0/8), spread (claimed 2/2, actually 0/2),
cumdelta (claimed 4/6, actually 6/6) and wyckoffEvent (claimed 8/16, actually 12/16) were wrong.
Verify a block layout against an emitted width before trusting a column index.
VERIFIED IN SITU: 372 inputs (6 bars x 62 features), five charts training, zero casting errors, zero
out-of-range, zero stalls, models re-keyed (PAI-19ba / CONV-f3c2 / LSTM-aa25 / HYB-d8d7).
STILL OPEN: ichimoku (0/8, five columns dead), spread (0/2) and wyckoffFail (0/5) also carry no
unanimous column and are the next candidates - left ON for now so this cut can be judged alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
abda14856f |
refactor(features): delete the cross-asset panel - 564 lines and 117 touch points of dead code
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 (
|
||
|
|
954020b261 |
feat(optimizers): RMSprop, Nesterov and AMSGrad across all four backends
THREE NEW OPTIMIZERS, APPENDED TO ENUM_OPTIMIZATION (never inserted - the ordinal is written into
every .nnw and hashed into the weights filename, so renumbering would silently reinterpret every
saved model). SGD=0 and ADAM=1 keep their values.
WHAT EACH ACTUALLY COSTS, because two of the three are nearly free and it is worth saying why:
RMSPROP = the EXISTING Adam kernel with beta1 = 0. With no first moment mt collapses to the raw
gradient and the step becomes lr*g/sqrt(v), which is RMSprop exactly. Not one kernel
was added in any backend - it is a scalar, not an algorithm. The bias correction had
to move into WarriorOptLearnRate() because the literal (1 - pow(beta1,t)) denominator
is (1 - 0^t) = 1 for t>=1 but ZERO at t==0, a division by zero on an unadvanced
counter.
NESTEROV = the momentum kernels' `optimizer` argument, which had been retained-but-unused since
the DFA entry was deleted, finally selects something. Heavy-ball applies the velocity;
Nesterov applies lr*g + mu*v_new (the Bengio/PyTorch reformulation, which expresses NAG
without evaluating the gradient at a shifted point). No new state, no signature change
on three of the four momentum entry points.
AMSGRAD = the only one needing extra state: a non-decreasing denominator needs the running max of
the second moment (Reddi et al. 2018 - Adam can fail to converge because a rare large
gradient inflates v and then DECAYS away, so the effective step GROWS again just after
the event that should have shrunk it).
IT REUSES DeltaWeights AS THE v-MAX. The Adam family already frees that buffer, it is
the same element count, already zeroed by ZeroOptimizerBuffer, already saved and
loaded. Net memory versus SGD: zero. That mattered: an extra per-tensor allocation is
exactly what took this machine down twelve hours ago.
FOUR TIERS, IN LOCKSTEP, WHICH IS THIS FILE'S OWN STANDING RULE:
* MQL5 host (NeuronCPU.mqh): all three, with CConnection gaining a vMax field (persisted
unconditionally - CConnection cannot know which optimizer owns it, and a conditional would make
the record length depend on state the reader does not have yet).
* CPU DLL (WarriorCPU.cpp + .h): Nesterov in all four momentum kernels; a nullable vm pointer in
all four Adam kernels (vmHandle < 0 leaves it null, so every non-amsgrad caller is
byte-identical to before). Rebuilt and deployed.
* MQL5 host FALLBACK inside ApplyAccumToBlock: the same amsgrad branch. This is the path the DLL
drops to when the element-wise apply fails, and a model that trained differently depending on
whether the DLL was healthy would be unreproducible.
* OpenCL (Network.cl): Nesterov via the same `optimizer` argument; matrix_vm + an amsgrad flag on
all four Adam kernels, bound to a harmless dummy buffer when off so there is still ONE kernel
per operation, matching the DLL's shape rather than forking a second set.
THE OPENCL TIER IS UNVERIFIED AND SAYS SO AT RUNTIME. This machine has no OpenCL device ("cannot get
OpenCL platforms / opencl.dll not found"), so the CPU-DLL tier is what actually executed these. The
kernels are written and wired; they have never run. A throttled line names that on first use rather
than implying a parity that was never tested.
THE BINDINGS THAT WOULD HAVE BROKEN SILENTLY, and why the predicates exist:
`optimization == SGD` and `== ADAM` appeared at 34 sites and meant three different questions - which
buffers to allocate, which to persist, and which step to take. An unlisted optimizer would have
taken the ADAM branch for its buffers and the SGD branch for its persistence. They are now
WarriorOptUsesMoments / WarriorOptUsesDelta / WarriorOptIsMomentumStep, and the allocation and
persistence sites ask TWO INDEPENDENT questions instead of one if/else, because AMSGRAD is the first
optimizer that needs both families.
ASSIGNED PER ARCHITECTURE, so the ensemble's members now differ in their optimizer as well as their
shape - four members that fail in correlated ways average to nothing, and member correlation
(printed every era, r 0.19-0.36) is the measurement that says whether this bought diversity:
Perceptron/MLP -> NESTEROV (no structural prior, so a non-adaptive step is the implicit
regulariser that makes it the conservative ANCHOR vote; the lookahead
damps heavy-ball's overshoot at zero extra state)
Conv -> ADAM (unchanged - AdamW already)
LSTM -> AMSGRAD (the most heavy-tailed gradients here: shared weights across timesteps
mean one bad window contributes a burst of correlated updates, which
is the regime AMSGrad was derived for)
ConvLSTM -> RMSPROP (two very differently-scaled gradient sources; per-weight
normalisation without a first moment stops the conv stage's momentum
dragging the recurrent one)
VERIFIED IN SITU, not by compiling: 5 charts, 18 eras, zero UpdateWeights failures, zero fallback
warnings, one fingerprint per architecture (PAI-0bd8 / CONV-5327 / LSTM-44d1 / HYB-452a). Commit
3,975 MB against 29,982 MB headroom.
COST: every model re-keys and retrains, the optimizer being a field of the weights filename.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
d8c629c8ba |
feat(research): buffer-health diagnostic for the AD suite - it found a provable dead branch
DiagnoseADBuffers.mq5 reports, per indicator per buffer per chart, the share of EMPTY_VALUE, the share of exact zeros, the single most common value share, and the standard deviation - flagging NEVER-FILLED / ALL-ZERO / CONSTANT / NEAR-CONSTANT. It answers the question that has to be settled BEFORE any "this feature carries no signal" verdict: is the buffer computing anything at all. A constant column is a code fault, not a weak feature - a real but weak signal still varies. FIRST RUN, 40,000 H1 bars x 5 charts: ADWyckoffEventStream buffers 12 and 13 (Reaccumulation, Redistribution) are identically zero with sd exactly 0 on ALL FIVE charts. Tracing it gives a proof rather than a suspicion: line 465 accumulation ranges open only under tr<=0, calling OpenRange(+1, bi, tr) line 392 continuation needs (dir>0 && trendAtOpen>0), and trendAtOpen IS that tr so the reaccumulation branch is unreachable by construction, and redistribution is unreachable symmetrically. gRIsContinuation can never be true. The conceptual error is one Trend(bi) reading serving two different questions - the climax gate asks about the trend the climax TERMINATES, reaccumulation asks about a larger trend the range sits INSIDE. Different horizons; collapsing them makes the conditions mutually exclusive. This corrects my own earlier reading. I reported the block as "dead at every parameter set" from a 41-candidate sweep on two bands; no parameter can make an unreachable branch fire, so that search was never a test of the concept. The feature set is unchanged - the operator is having the suite reviewed for correctness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2b97bf6573 |
refactor(tuning): delete the in-EA indicator search, replace it with an offline research pass
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>
|
||
|
|
b31f4623e5 |
fix(memory): the 43 GB was the indicator tuner leaking full-history buffers - 33.3 GB -> 2.85 GB
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
|
||
|
|
0405f1e8a4 |
feat(training): optimizer is per ARCHITECTURE, and the era cap goes 100 -> 10000
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
|
||
|
|
780bb97fb5 |
fix(persistence): a saved "complete" model came back FROZEN - migrate it to live-and-learning
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> |
||
|
|
ca47183c40 |
fix(labels): the ZigZag readiness test asked the wrong question - the fleet would idle all weekend
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>
|
||
|
|
f78f567804 |
fix(lifecycle): deploying makes a model LIVE, not FINISHED - let the NN keep learning
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>
|
||
|
|
fd7759596e |
fleet: 6 -> 5 in the symbol list - six charts left only 4.2 GB of headroom
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> |
||
|
|
528178776c |
feat(fleet): 3 -> 6 instruments - pooled observations are the only remaining lever
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>
|
||
|
|
8141c4e000 |
feat(pca): measure the STANDARDISED component count - the one that should set k
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> |
||
|
|
c3dde7cdba |
fix(capacity): the budget line claimed the encoder was at its floor - it is not
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> |
||
|
|
3988f01820 |
feat(capacity): state the standard parameter budget - total params <= N_eff / alpha
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> |
||
|
|
e8c7d275c4 |
fix(capacity): count both neuron hierarchies, and never print an uncounted zero
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> |
||
|
|
0847529707 |
fix(capacity): the capacity line was counting one layer of a sequence model
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> |
||
|
|
3580324573 |
feat(features): sweep the prune threshold - can correlation pruning alone get the width down
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>
|
||
|
|
10f90c5091 |
feat(features): name the redundant columns and run the greedy correlation prune
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>
|
||
|
|
94628ba7a9 |
feat(risk): make the tight stop rungs falsifiable - break-even slippage per hit
The stop frontier on all three charts says a TIGHT stop improves the ATR book: EURUSD -0.125 ->
-0.075, USDJPY -0.055 -> -0.014, NAS100 +0.040 -> +0.121, all at a 0.50 ATR stop. That is the
opposite of the mechanism it was built to test - it is cutting LOSERS early, not letting winners run
- and it rests entirely on the one thing the report does not model.
At an 80-85% hit rate, slippage lands on five of every six trades. A caveat is not good enough for a
number that would otherwise look like the best result of the day, so it is now falsifiable:
BEslip = advantage over no-stop / hit rate
the slippage PER HIT that erases a rung entire advantage. A rung whose whole advantage is 0.05 ATR
at an 84% hit rate dies at 0.06 ATR of slippage, which on an H1 stop-out is an ordinary number
rather than a pessimistic one. If BEslip comes back smaller than a realistic slip, the rung is an
artefact of the frictionless assumption and must be read as one.
This is the same discipline the cost-split and horizon reports were held to: state the assumption
that is doing the work, then print the number at which it breaks.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
01c3a1f2d8 |
feat(risk): measure what the STOP is costing - the exact counterfactual, in R
Operator asked whether the NN magnitude signal can drive dynamic SL/TP to stop cutting winners
early. Two prior findings shape the answer, and both are respected here rather than re-litigated:
* DIRECTION IS CLOSED, but excursion SIZE clears at 3.9-5.4x its null on three instruments. So
magnitude is the right thing to lean on - for RISK CONTROL, which is what the excursion verdict
already says it is good for.
* THE RATIO IS EV-NEUTRAL for a driftless walk (reaching +m before -k has probability k/(k+m),
which IS break-even). Geometry cannot manufacture alpha, so nothing here claims it does.
What CAN be true is the operator premise: with Exit_On_Leg_Flip and NO take-profit, the stop is the
ONLY thing that can end a trade early, so a stop inside the ride typical adverse excursion silently
converts winners into losses. Nothing in the era report could see that - g_ensVoteUpR/DnR are
excursions over a FIXED horizon, not over the ride.
So the missing measurement is what each ride had to SURVIVE. LegRideLabel already walks every bar
between entry and the flip to find it; it now also tracks the worst adverse excursion on that walk
(lowest low for a long, highest high for a short, from the entry close, in ATR at entry) at one
comparison per bar and no extra pass. Cached per bar, carried onto each scored vote row.
The STOP FRONTIER then reports, at the certified rung: the book with NO stop at all, and the book
with a stop at each of 9 distances, with hit rate. THE COUNTERFACTUAL IS EXACT, not modelled - a
stop at S fired iff the measured ride MAE >= S.
REPORTED IN R, NOT ATR, and that is the point. A wider stop means a proportionally SMALLER position
at the same risk percentage, so comparing stop distances in ATR compares trades of different size
and always flatters the widest. Dividing by the stop distance is what the account actually
experiences.
Slippage is not modelled - a hit is priced at exactly -S - so the TIGHT rungs are flattered, and the
report says so rather than hiding it.
ALSO CONFIRMED, NOT BUILT: the operator second request (reject a trade when even the smallest lot
breaches the risk threshold) ALREADY EXISTS and is correct - MoneyRiskBase.mqh:147, "BELOW-MINIMUM
MEANS NO TRADE, NOT A BIGGER TRADE", refusing rather than letting TCNormalizeVolume round a
risk-derived 0.05 up to a 0.10 minimum and double the intended risk.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|