Warrior_EA/Warrior_EA.mq5

1905 lines
98 KiB
MQL5
Raw Permalink Normal View History

fix: the recall gate was unsatisfiable and the LR decay was a spiral Both made the run structurally unable to succeed, independently of any signal in the data. Found by reading the 13:01 log. RECALL GATE. m_objectiveMet required Buy, Sell AND Neutral OOS recall each >= 40%. First-touch resolution (ce52654) collapsed Neutral from the ~94% majority it was under exact-pivot labels to a same-bar-tie residue - 250 of 38,261 bars, 0.65% - so the floor was asking the model to identify 40% of coin-flip ties before it could converge. Measured: CONV, LSTM and HYBRID all logged "Neutral:0% (need >=40% each)" on every era. No model could ever satisfy it; every run was destined for the plateau ladder or the era cap. Only the DIRECTIONAL floors are load-bearing for the anti-collapse job the gate exists to do: an all-Neutral model shows Buy and Sell recall at 0% and is blocked by them. Neutral's own floor guarded the mirror bias (over-calling Buy/Sell at Neutral's expense), which was real at 94% prevalence and is not at 0.65% - there, almost never calling Neutral is correct rather than biased. Prevalence-guarded rather than hardcoded off, so it returns by itself if a future label rule makes Neutral substantial again. Deliberately NOT extended to Buy/Sell: exempting a thin directional class reopens the era-44-46 hole, which directionalRecallMeasured only half-covers - it checks those classes were MEASURED, not that they passed. ETA DECAY. A regressing era restored the checkpoint, reset the optimizer and cut eta - all on the FIRST regression. The next era then started from an identical state with a smaller step, regressed again, and got the same treatment. The loop is self-sustaining and cannot discover anything, because rolling the weights back is exactly what removes the exploration that would end it. Measured on PAI: eras 2-11 every one a regression against era 1, eta 0.000594 -> 0.000024, dW/W 0.000%/0.000% from era 2 onward. Ten eras, ~45s each, reproducing era 1 exactly and unable to do anything else. Now requires ETA_DECAY_PATIENCE_ERAS consecutive regressions - the standard ReduceLROnPlateau formulation. A single bad era is noise, and an improving era clears the counter so alternating runs never accumulate into a decay. Build tag -> gate-patience-v3. It had not moved in six commits, which is why the running binary could not be identified from its own log. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 13:28:58 -04:00
//+------------------------------------------------------------------+
//| Warrior_EA |
//| AnimateDread |
//| |
//+------------------------------------------------------------------+
//--- MQL5 Market rule IV: "Products must not contain calls to any DLL". Leave commented out for the
//--- private/prop-firm deployment build, which keeps the DirectML/ CPU-DLL fallback tier for hosts
//--- without OpenCL.
//#define WARRIOR_MARKET_BUILD
//--- MARKET build only: embed every custom indicator this EA calls via CiCustom so one self-
//--- contained .ex5 ships to the Market. The private build uses bare names (no resources), so it
//--- builds from anywhere.
#ifdef WARRIOR_MARKET_BUILD
#resource "\\Indicators\\ADCumulativeDelta.ex5"
#resource "\\Indicators\\ADShorteningOfThrust.ex5"
#resource "\\Indicators\\ADWyckoffEventStream.ex5"
#resource "\\Indicators\\ADWyckoffFailedStructure.ex5"
#resource "\\Indicators\\ADWyckoffSignificantBarInversion.ex5"
feat(indicators): run the built-in iMA and MetaTrader's ZigZag; add a classic-vote shift MA: CustomIndicators\ADMovingAverage is replaced by the built-in iMA (CiMA) on both consumers - the classic vote and the NN MA input feature. This drops the five advanced types ALMA/DEMA/ZLEMA/T3/Kalman, which have no iMA equivalent; MA_TYPE_PRESETS is now ENUM_MA_METHOD's own codes and the tuner searches all four. It also removes a documented failure mode: a custom indicator's depth is bounded by TERMINAL_MAXBARS, and m_MA was the one whose feature block REJECTS the bar on a short read - the "feature 25 fails on every bar" incident of 2026-08-17. A built-in is served at any depth. MIGRATION. SMA moves from code 5 to 0, so persisted type codes change meaning. SanitizeMaType() is the single validity rule; TunedPeriods records now carry a version field and a v1 record remaps 5..8 -> 0..3, falling back to SMA for a stored advanced type (unrecoverable - old 0..4 are indistinguishable from valid new codes). Existing .nnw files re-key on their own, because MA_Type is hashed into the topology fingerprint, so models retrain rather than silently running on different MA values. EXPECT A FULL RETRAIN. ZigZag: ADZigZag was a byte-identical rename of MetaQuotes' Examples\ZigZag - verified by normalising identifiers and stripping comments, 233 significant lines each with only renamed symbols differing. It now loads the stock one, so nothing is bundled and MetaQuotes' fixes arrive without a rebuild here. Both #resource entries are gone. Classic_Shift: a new input, the BAR the four classic votes evaluate on (0 = forming, 1 = last closed, default 1). One implementation on CExpertSignalCustom, inherited by all four rather than repeated per module. Defaults to a sentinel meaning "unset", so the AI signals and the aggregate keep the stock every_tick rule and their feature/label alignment is untouched. The META corpus sweep still takes precedence. CExpertBase::StartIndex turns out to be virtual, so this is a real override, not the name-hiding the old comment claimed. Not compiled - MetaEditor compile pending. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:09:58 -04:00
//--- ADZigZag and ADMovingAverage are no longer bundled: the ZigZag now loads MetaTrader's own
//--- Examples\ZigZag (our copy was a byte-identical rename) and the MA runs the built-in iMA.
#endif
//--- Inputs FIRST so the EA's own grouped inputs lead the Inputs tab. Safe because Inputs.mqh
//--- depends only on Enumerations\InputEnums.mqh (which carries a guarded ENUM_OPTIMIZATION copy) -
//--- no AI header needed.
#include "Variables\Inputs.mqh"
//--- chart-level tuned indicator periods: read at OnInit before the DB fingerprint and the classic
//--- signal configuration, written by a gated auto-tune install - see the file's header contract
#include "Variables\TunedPeriods.mqh"
//--- database classes
#include "Database\DatabaseManager.mqh"
#include "Database\TradeJournalManager.mqh"
//--- available custom classes
#include "Expert\ExpertCustom.mqh"
#include "Expert\AIBase\MetaCorpus.mqh"
feat(rng): ALGLIB's L'Ecuyer generator replaces MathRand, and a seed collision goes with it MQL5's MathRand() is the 15-bit MSVC LCG - 32768 distinct values and the lattice structure that shape of generator has. Two places here actually lean on randomness and both were hurt by it: WEIGHT INIT. Six He/LeCun-uniform sites drew ((MathRand()+1)/32768.0 - 0.5) * 2 * scale, so a first dense layer of ~250k weights had only 32768 possible values and thousands of connections started byte-identical. Breaking that symmetry is the whole job of random init. SHUFFLING. ShuffleRandomIndex() already had to splice TWO MathRand() draws to reach 30 bits, and its own comment documented the residual modulo bias it still carried. HQRndUniformI() is rejection-sampled and exactly uniform, so the splice and the bias note both go. CHighQualityRand is L'Ecuyer's combined multiplicative congruential generator - two differenced streams, 31-bit output, period ~2.3e18 - and it ships with the terminal. AND A BUG THE MIGRATION EXPOSED. The three MathSrand(GetTickCount()) calls sit immediately before "build a fresh topology", once per model. GetTickCount() steps in ~15.6 ms on Windows and an ensemble builds every member inside one OnInit, so members could be handed the SAME seed and draw the SAME weights wherever their shapes coincide - and members that start identical are not an ensemble. WarriorRandSeed() takes a salt (the model id) plus a never-reset call counter, so a collision is impossible rather than merely unlikely, while the tick keeps the run itself genuinely unrepeatable the way those call sites asked for. Seeds are masked positive rather than trusted: HQRndSeed computes s % (M-1) + 1 and MQL5's % keeps the sign, so a negative seed leaves the generator in a state its own assertions reject. GetTickCount() is a uint and goes negative as an int after ~24 days of uptime - a fault that would surface as "training is broken" on a long-running terminal and nowhere else. The indicator tuner's 52 draws move across too: its random search is where sample quality earns its keep. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:29:12 -04:00
#include "System\Random.mqh"
#include "System\PrintVerbose.mqh"
#include "System\StatusLabel.mqh"
#include "System\AltDataFetch.mqh"
//--- available signals
#include "Signals\Signals.mqh"
//--- available trailing
#include "Trailing\Trailing.mqh"
//--- available money management
#include "Money\Money.mqh"
//--- Variables
#include "Variables\Variables.mqh"
//--- Control panel GUI (standard MQL5 Controls library)
#include "Panel\ControlPanel.mqh"
feat(altdata): EIA wired, 24-instrument symbol catalog, mapping dialog for unknown symbols EIA (user directive: "the NN might find patterns in it for both oil and regular symbols"). Weekly Petroleum Status Report via the v2 API - crude stocks ex-SPR, field production, refinery utilization - three features (1y percentile, 4w change, utilization) on EVERY catalog symbol, not just oil. EIA screened NULL on WTI's short 7y sample, so these ship as EXPLORATORY inputs: the deploy gate, not the screen, decides whether a model trained on them trades. Publication stamp observed+6d mirrors research/altdata/eia.py. Symbol handling was hardcoded to three if-blocks; it is now a catalog of 24 instruments x alias lists covering The5ers/FTMO/AvaTrade/Dukascopy/OANDA/IC Markets naming, with prefix matching for the broker suffix zoo (US500.cash, XAUUSDm, EURUSD.r). Adding an instrument is one AddSpec row. COT caches are named by CANONICAL so two brokers' names for one contract share a download. Unrecognised symbol -> a chart dialog (Panel\AltDataMapDialog.mqh, CAppDialog + dropdown) asks which instrument it is; the answer persists in symbol_map.cfg and "No alternative data" is a recorded choice, not a nag. Non-blocking by design: an unmapped symbol contributes 0 features and must never hold up a chart. Also: UrlEncodePart now escapes '%' - SoQL like-predicates use it as the wildcard and an unescaped one corrupts the query; docs/ gains the whitelist URLs, an API-key backup, and the catalog reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:18:29 -04:00
//--- One-time "which instrument is this?" dialog for symbols the alt-data catalog does not know
#include "Panel\AltDataMapDialog.mqh"
//+------------------------------------------------------------------+
//| The CustomIndicators\*.mq5 files (ADCumulativeDelta, |
//| ADShorteningOfThrust, ADWyckoffEventStream, |
//| ADWyckoffFailedStructure, ADWyckoffSignificantBarInversion) are |
//| loaded via CiCustom/IND_CUSTOM (see ExpertSignalAIBase.mqh). |
//+------------------------------------------------------------------+
//
CExpertCustom Expert;
CDatabaseManager dbm();
CTradeJournalManager journal;
//+------------------------------------------------------------------+
//| Pointers to whichever AI signal instances this run actually |
feat(ensemble): per-NN inputs replace the preset selector - the meta head becomes the vote's gate User design (2026-08-19): 'remove the enum menu that selects neural networks... individual inputs for every NN just like classic signals... the META NN should be integrated into the voting decision pipeline when enabled... as a bonus meta labelling is applied to enabled NNs.' - AI_CHOICE is GONE (tombstoned per the stale-.set doctrine). Use_MLP/Use_CONV/Use_LSTM/ Use_CONVLSTM are ordinary bools like the classic votes; the ensemble arithmetic adapts to any subset because the consensus divisor is the enabled capable weight. Two or more enabled = ensemble (|ENS1 token + joint gate, exactly the old AI_HYBRID fingerprints, so existing weight files keep loading); one = the old solo preset; none = classic-only. - Use_MetaLabeling un-couples META from the direction NNs (the old selector made them mutually exclusive). S3 ships: CSignalMETA::LiveMetaGate scores each vote-cleared entry (shared window at bar 1 + proposal descriptor: side, net vote, live geometry, spread/ATR; pattern one-hot ZEROED - ranking, not calibrated probability, documented in the body) and vetoes below the cost-adjusted break-even. Entries only; fail-open everywhere, loudly. - COEXISTENCE HAZARDS closed: VoteCapableWeight()=0 and ProspectiveVote()=false for the meta target - solo-only until today, a trained META would otherwise sit in the consensus divisor as a permanent abstainer and shrink every vote by its module weight. - CERTIFIED == TRADED: the ensemble era verdict replays the identical veto through the same g_warriorMetaGate pointer over its OOS fired bars (bar re-resolved from the row's own time; fail-open counted as fires and reported: 'metaGate: N approved, M vetoed, K unscored'). The overlay deliberately does NOT replay it (veto-filter-in-replay class, calendar-cliff precedent) - documented at the sweep site. Solo charts' own gate does not model the veto - the standing solo-gate caveat, documented at the input. - DB continuity: the pattern/journal DB fingerprint's first slot was (int)AIType; DbLegacyAiSlot() maps every legacy-expressible config to its OLD value (new 2-3 member subsets get 100+bitmask, outside the legacy range) so no existing database re-keys. filterID becomes the enabled roster via one EnabledNNSummary(). - HUD: the meta line shows the gate (armed/(trn), last P vs BE, ok/veto tally); the armed/disarmed announcement fires on state change via one latch (MetaGateArmedNow), not only when an entry happens to be proposed. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:01:02 -04:00
//| created (any enabled Use_* subset, plus the meta head), so the |
//| panel can drive training/weight actions on exactly the signal(s) |
//| in play this run and never touch another config's files. |
//+------------------------------------------------------------------+
//--- Must be >= the number of AI signal instances one run can create at once. At 3 the CONVLSTM
//--- member was once silently dropped on the floor (609be10); the array holds borrowed pointers, so
//--- headroom is free - but a cap that silently discards a model is a trapdoor, hence the loud
//--- refusal below.
fix(ensemble): MAX_AI_SIGNALS was 3 - the ensemble creates 4, so CONVLSTM was silently dropped AI_HYBRID enables PAI + CONV + LSTM + CONVLSTM and RegisterAISignal registers them in exactly that order. MAX_AI_SIGNALS was 3, and the guard returned silently, so the FOURTH - CONVLSTM - never entered g_aiSignals[]. Reported as "convlstm is not listening to the control panel buttons", which is the visible tip. Everything in Warrior_EA.mq5 that reaches a model does so by looping g_aiSignals[], so the dropped member also lost: - every control panel button (pause/resume, stop/start, retrain, deploy, save, load, reset weights) - PollTraining() in OnTimer - no wall-clock training progress, so it only advanced on ticks - AutosaveWeightsIfDue() -> SaveWeightsNow() - AltDataReload() on both the mapping-dialog and hourly-upkeep paths - StartChartSignalRescan()/RescanPending() - the Show Signals sequence - the All*/Any* aggregates (deployed/paused/stopped/complete), which were therefore computed over 3 of 4 members and could report the ensemble finished while CONVLSTM was still training - OnDeinit's MarkShutdown(), ShutdownChartCleanup() and FlushTrainRun() - so its arrows were stranded on the chart and its training run was never flushed on shutdown It stayed hidden because the model still trains and still votes: it lives in the signal's own filter array, and it registers itself with the status panel (ENSEMBLE_PANEL_MAX_MEMBERS is 6) rather than through g_aiSignals[]. So it appeared on the panel, drew arrows and moved the vote while being unreachable from every action and unsaveable on exit. MAX_AI_SIGNALS 3 -> 5 (4 is today's true maximum; the spare slot means adding META to a preset cannot reintroduce this - the array holds borrowed pointers, so unused slots cost nothing). RegisterAISignal now PRINTS on overflow instead of returning silently. A cap that discards a model without saying so is a trapdoor, not a guard. NOT COMPILED - user compiles. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 23:55:04 -04:00
#define MAX_AI_SIGNALS 5
//--- Printed at OnInit so tester logs prove which binary is actually running.
feat(ensemble): per-NN inputs replace the preset selector - the meta head becomes the vote's gate User design (2026-08-19): 'remove the enum menu that selects neural networks... individual inputs for every NN just like classic signals... the META NN should be integrated into the voting decision pipeline when enabled... as a bonus meta labelling is applied to enabled NNs.' - AI_CHOICE is GONE (tombstoned per the stale-.set doctrine). Use_MLP/Use_CONV/Use_LSTM/ Use_CONVLSTM are ordinary bools like the classic votes; the ensemble arithmetic adapts to any subset because the consensus divisor is the enabled capable weight. Two or more enabled = ensemble (|ENS1 token + joint gate, exactly the old AI_HYBRID fingerprints, so existing weight files keep loading); one = the old solo preset; none = classic-only. - Use_MetaLabeling un-couples META from the direction NNs (the old selector made them mutually exclusive). S3 ships: CSignalMETA::LiveMetaGate scores each vote-cleared entry (shared window at bar 1 + proposal descriptor: side, net vote, live geometry, spread/ATR; pattern one-hot ZEROED - ranking, not calibrated probability, documented in the body) and vetoes below the cost-adjusted break-even. Entries only; fail-open everywhere, loudly. - COEXISTENCE HAZARDS closed: VoteCapableWeight()=0 and ProspectiveVote()=false for the meta target - solo-only until today, a trained META would otherwise sit in the consensus divisor as a permanent abstainer and shrink every vote by its module weight. - CERTIFIED == TRADED: the ensemble era verdict replays the identical veto through the same g_warriorMetaGate pointer over its OOS fired bars (bar re-resolved from the row's own time; fail-open counted as fires and reported: 'metaGate: N approved, M vetoed, K unscored'). The overlay deliberately does NOT replay it (veto-filter-in-replay class, calendar-cliff precedent) - documented at the sweep site. Solo charts' own gate does not model the veto - the standing solo-gate caveat, documented at the input. - DB continuity: the pattern/journal DB fingerprint's first slot was (int)AIType; DbLegacyAiSlot() maps every legacy-expressible config to its OLD value (new 2-3 member subsets get 100+bitmask, outside the legacy range) so no existing database re-keys. filterID becomes the enabled roster via one EnabledNNSummary(). - HUD: the meta line shows the gate (armed/(trn), last P vs BE, ok/veto tally); the armed/disarmed announcement fires on state change via one latch (MetaGateArmedNow), not only when an entry happens to be proposed. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:01:02 -04:00
#define WARRIOR_BUILD_TAG "meta-gate-v1"
CExpertSignalAIBase *g_aiSignals[MAX_AI_SIGNALS];
int g_aiSignalCount = 0;
void RegisterAISignal(CExpertSignalAIBase *sig)
{
fix(ensemble): MAX_AI_SIGNALS was 3 - the ensemble creates 4, so CONVLSTM was silently dropped AI_HYBRID enables PAI + CONV + LSTM + CONVLSTM and RegisterAISignal registers them in exactly that order. MAX_AI_SIGNALS was 3, and the guard returned silently, so the FOURTH - CONVLSTM - never entered g_aiSignals[]. Reported as "convlstm is not listening to the control panel buttons", which is the visible tip. Everything in Warrior_EA.mq5 that reaches a model does so by looping g_aiSignals[], so the dropped member also lost: - every control panel button (pause/resume, stop/start, retrain, deploy, save, load, reset weights) - PollTraining() in OnTimer - no wall-clock training progress, so it only advanced on ticks - AutosaveWeightsIfDue() -> SaveWeightsNow() - AltDataReload() on both the mapping-dialog and hourly-upkeep paths - StartChartSignalRescan()/RescanPending() - the Show Signals sequence - the All*/Any* aggregates (deployed/paused/stopped/complete), which were therefore computed over 3 of 4 members and could report the ensemble finished while CONVLSTM was still training - OnDeinit's MarkShutdown(), ShutdownChartCleanup() and FlushTrainRun() - so its arrows were stranded on the chart and its training run was never flushed on shutdown It stayed hidden because the model still trains and still votes: it lives in the signal's own filter array, and it registers itself with the status panel (ENSEMBLE_PANEL_MAX_MEMBERS is 6) rather than through g_aiSignals[]. So it appeared on the panel, drew arrows and moved the vote while being unreachable from every action and unsaveable on exit. MAX_AI_SIGNALS 3 -> 5 (4 is today's true maximum; the spare slot means adding META to a preset cannot reintroduce this - the array holds borrowed pointers, so unused slots cost nothing). RegisterAISignal now PRINTS on overflow instead of returning silently. A cap that discards a model without saying so is a trapdoor, not a guard. NOT COMPILED - user compiles. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 23:55:04 -04:00
if(sig == NULL)
return;
//--- LOUD on overflow. A cap that silently discards a model is not a guard, it is a trapdoor.
fix(ensemble): MAX_AI_SIGNALS was 3 - the ensemble creates 4, so CONVLSTM was silently dropped AI_HYBRID enables PAI + CONV + LSTM + CONVLSTM and RegisterAISignal registers them in exactly that order. MAX_AI_SIGNALS was 3, and the guard returned silently, so the FOURTH - CONVLSTM - never entered g_aiSignals[]. Reported as "convlstm is not listening to the control panel buttons", which is the visible tip. Everything in Warrior_EA.mq5 that reaches a model does so by looping g_aiSignals[], so the dropped member also lost: - every control panel button (pause/resume, stop/start, retrain, deploy, save, load, reset weights) - PollTraining() in OnTimer - no wall-clock training progress, so it only advanced on ticks - AutosaveWeightsIfDue() -> SaveWeightsNow() - AltDataReload() on both the mapping-dialog and hourly-upkeep paths - StartChartSignalRescan()/RescanPending() - the Show Signals sequence - the All*/Any* aggregates (deployed/paused/stopped/complete), which were therefore computed over 3 of 4 members and could report the ensemble finished while CONVLSTM was still training - OnDeinit's MarkShutdown(), ShutdownChartCleanup() and FlushTrainRun() - so its arrows were stranded on the chart and its training run was never flushed on shutdown It stayed hidden because the model still trains and still votes: it lives in the signal's own filter array, and it registers itself with the status panel (ENSEMBLE_PANEL_MAX_MEMBERS is 6) rather than through g_aiSignals[]. So it appeared on the panel, drew arrows and moved the vote while being unreachable from every action and unsaveable on exit. MAX_AI_SIGNALS 3 -> 5 (4 is today's true maximum; the spare slot means adding META to a preset cannot reintroduce this - the array holds borrowed pointers, so unused slots cost nothing). RegisterAISignal now PRINTS on overflow instead of returning silently. A cap that discards a model without saying so is a trapdoor, not a guard. NOT COMPILED - user compiles. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 23:55:04 -04:00
if(g_aiSignalCount >= MAX_AI_SIGNALS)
{
Print(__FUNCTION__ + ": CANNOT REGISTER a further AI signal - MAX_AI_SIGNALS is " +
IntegerToString(MAX_AI_SIGNALS) + " and this run already created that many. The extra "
"model would still train and still vote, but the control panel could not reach it and its "
"weights would never be autosaved or flushed on shutdown. Raise MAX_AI_SIGNALS to at least "
feat(ensemble): per-NN inputs replace the preset selector - the meta head becomes the vote's gate User design (2026-08-19): 'remove the enum menu that selects neural networks... individual inputs for every NN just like classic signals... the META NN should be integrated into the voting decision pipeline when enabled... as a bonus meta labelling is applied to enabled NNs.' - AI_CHOICE is GONE (tombstoned per the stale-.set doctrine). Use_MLP/Use_CONV/Use_LSTM/ Use_CONVLSTM are ordinary bools like the classic votes; the ensemble arithmetic adapts to any subset because the consensus divisor is the enabled capable weight. Two or more enabled = ensemble (|ENS1 token + joint gate, exactly the old AI_HYBRID fingerprints, so existing weight files keep loading); one = the old solo preset; none = classic-only. - Use_MetaLabeling un-couples META from the direction NNs (the old selector made them mutually exclusive). S3 ships: CSignalMETA::LiveMetaGate scores each vote-cleared entry (shared window at bar 1 + proposal descriptor: side, net vote, live geometry, spread/ATR; pattern one-hot ZEROED - ranking, not calibrated probability, documented in the body) and vetoes below the cost-adjusted break-even. Entries only; fail-open everywhere, loudly. - COEXISTENCE HAZARDS closed: VoteCapableWeight()=0 and ProspectiveVote()=false for the meta target - solo-only until today, a trained META would otherwise sit in the consensus divisor as a permanent abstainer and shrink every vote by its module weight. - CERTIFIED == TRADED: the ensemble era verdict replays the identical veto through the same g_warriorMetaGate pointer over its OOS fired bars (bar re-resolved from the row's own time; fail-open counted as fires and reported: 'metaGate: N approved, M vetoed, K unscored'). The overlay deliberately does NOT replay it (veto-filter-in-replay class, calendar-cliff precedent) - documented at the sweep site. Solo charts' own gate does not model the veto - the standing solo-gate caveat, documented at the input. - DB continuity: the pattern/journal DB fingerprint's first slot was (int)AIType; DbLegacyAiSlot() maps every legacy-expressible config to its OLD value (new 2-3 member subsets get 100+bitmask, outside the legacy range) so no existing database re-keys. filterID becomes the enabled roster via one EnabledNNSummary(). - HUD: the meta line shows the gate (armed/(trn), last P vs BE, ok/veto tally); the armed/disarmed announcement fires on state change via one latch (MetaGateArmedNow), not only when an entry happens to be proposed. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:01:02 -04:00
"the number of instances the enabled Use_* inputs create and recompile.");
fix(ensemble): MAX_AI_SIGNALS was 3 - the ensemble creates 4, so CONVLSTM was silently dropped AI_HYBRID enables PAI + CONV + LSTM + CONVLSTM and RegisterAISignal registers them in exactly that order. MAX_AI_SIGNALS was 3, and the guard returned silently, so the FOURTH - CONVLSTM - never entered g_aiSignals[]. Reported as "convlstm is not listening to the control panel buttons", which is the visible tip. Everything in Warrior_EA.mq5 that reaches a model does so by looping g_aiSignals[], so the dropped member also lost: - every control panel button (pause/resume, stop/start, retrain, deploy, save, load, reset weights) - PollTraining() in OnTimer - no wall-clock training progress, so it only advanced on ticks - AutosaveWeightsIfDue() -> SaveWeightsNow() - AltDataReload() on both the mapping-dialog and hourly-upkeep paths - StartChartSignalRescan()/RescanPending() - the Show Signals sequence - the All*/Any* aggregates (deployed/paused/stopped/complete), which were therefore computed over 3 of 4 members and could report the ensemble finished while CONVLSTM was still training - OnDeinit's MarkShutdown(), ShutdownChartCleanup() and FlushTrainRun() - so its arrows were stranded on the chart and its training run was never flushed on shutdown It stayed hidden because the model still trains and still votes: it lives in the signal's own filter array, and it registers itself with the status panel (ENSEMBLE_PANEL_MAX_MEMBERS is 6) rather than through g_aiSignals[]. So it appeared on the panel, drew arrows and moved the vote while being unreachable from every action and unsaveable on exit. MAX_AI_SIGNALS 3 -> 5 (4 is today's true maximum; the spare slot means adding META to a preset cannot reintroduce this - the array holds borrowed pointers, so unused slots cost nothing). RegisterAISignal now PRINTS on overflow instead of returning silently. A cap that discards a model without saying so is a trapdoor, not a guard. NOT COMPILED - user compiles. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 23:55:04 -04:00
return;
}
g_aiSignals[g_aiSignalCount++] = sig;
}
//+------------------------------------------------------------------+
//| Control panel: a CAppDialog-based GUI (see Panel\ControlPanel.mqh) |
//| with show/hide signals, start/pause/stop training, and save/load/ |
//| delete-reset weights buttons for the currently-active AI |
//| signal(s) only. The dialog's own caption bar provides the show/ |
//| hide (minimize) control - no separate toggle button needed. |
//+------------------------------------------------------------------+
//--- default spawn position: top-right corner, clear of the status label text block (top-left) so the
//--- two don't overlap on first run - the panel is fully draggable afterwards via its caption bar,
//--- so this is only a starting point, not a constraint.
#define CP_Y0 10
#define CP_RIGHT_MARGIN 80
CControlPanel ExtPanel;
feat(altdata): EIA wired, 24-instrument symbol catalog, mapping dialog for unknown symbols EIA (user directive: "the NN might find patterns in it for both oil and regular symbols"). Weekly Petroleum Status Report via the v2 API - crude stocks ex-SPR, field production, refinery utilization - three features (1y percentile, 4w change, utilization) on EVERY catalog symbol, not just oil. EIA screened NULL on WTI's short 7y sample, so these ship as EXPLORATORY inputs: the deploy gate, not the screen, decides whether a model trained on them trades. Publication stamp observed+6d mirrors research/altdata/eia.py. Symbol handling was hardcoded to three if-blocks; it is now a catalog of 24 instruments x alias lists covering The5ers/FTMO/AvaTrade/Dukascopy/OANDA/IC Markets naming, with prefix matching for the broker suffix zoo (US500.cash, XAUUSDm, EURUSD.r). Adding an instrument is one AddSpec row. COT caches are named by CANONICAL so two brokers' names for one contract share a download. Unrecognised symbol -> a chart dialog (Panel\AltDataMapDialog.mqh, CAppDialog + dropdown) asks which instrument it is; the answer persists in symbol_map.cfg and "No alternative data" is a recorded choice, not a nag. Non-blocking by design: an unmapped symbol contributes 0 features and must never hold up a chart. Also: UrlEncodePart now escapes '%' - SoQL like-predicates use it as the wildcard and an unescaped one corrupts the query; docs/ gains the whitelist URLs, an API-key backup, and the catalog reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:18:29 -04:00
//--- Alt-data maintenance + the one-time symbol-mapping dialog. Declared HERE rather than beside
//--- OnTimer because OnDeinit (further up the file) tears the dialog down, and MQL5 resolves global
//--- variables in declaration order.
#define ALTDATA_CHECK_SECONDS 1800
datetime g_lastAltDataRun = 0;
CAltDataFetch g_altDataFetch;
CAltDataMapDialog g_altMapDialog;
bool g_altMapDialogOpen = false;
bool g_altMapAsked = false; // one prompt per attach, even if the user closes it unanswered
bool g_signalsVisible = true;
#define SIGNAL_VISIBILITY_STATE_SUFFIX ".sigvis"
string SignalsVisibilityStateFile(void)
{
return eaName + "_" + Symbol() + "_" + IntegerToString(Period()) + SIGNAL_VISIBILITY_STATE_SUFFIX;
}
bool LoadSignalsVisibilityState(void)
{
if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD))
return false;
string stateFile = SignalsVisibilityStateFile();
if(!FileIsExist(stateFile, FILE_COMMON))
return false;
int handle = FileOpen(stateFile, FILE_COMMON | FILE_BIN | FILE_READ | FILE_SHARE_READ | FILE_SHARE_WRITE);
if(handle == INVALID_HANDLE)
return false;
int storedVisible = FileReadInteger(handle);
FileClose(handle);
g_signalsVisible = (storedVisible != 0);
return true;
}
bool SaveSignalsVisibilityState(void)
{
if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD))
return true;
string stateFile = SignalsVisibilityStateFile();
int handle = FileOpen(stateFile, FILE_COMMON | FILE_BIN | FILE_WRITE | FILE_SHARE_READ | FILE_SHARE_WRITE);
if(handle == INVALID_HANDLE)
{
Print(__FUNCTION__ + ": failed to open " + stateFile + " for write, error " + IntegerToString(GetLastError()));
return false;
}
FileWriteInteger(handle, g_signalsVisible ? 1 : 0, INT_VALUE);
FileClose(handle);
return true;
}
//--- true while Show Signals has queued a rescan on one or more g_aiSignals and is waiting for all of
//--- them to finish (see ToggleSignalsVisibility/FinalizeSignalsRescanIfDone) - each signal's own rescan
//--- is now chunked across PollTraining's timer slices (CExpertSignalAIBase::AdvanceChartSignalRescan)
//--- instead of blocking the button click, so visibility can only be (re)applied and the "shown" Alert
//--- fired once every instance's RescanPending() has cleared.
bool g_signalsRescanPending = false;
//--- tracks the last known AlgoTrading permission state (terminal "Algo Trading" toggle AND this
//--- EA's own "Allow Algo Trading" property) so a change is logged exactly once, not spammed every tick
bool g_lastAlgoTradingAllowed = true;
//--- OnDeinit() is not guaranteed to run on a terminal crash/power loss/forced kill, so weights
//--- would otherwise only be as fresh as the last fully-completed training era.
datetime g_lastAutosaveBarTime = 0;
//--- last observed AllTrainingDeployed() value, so OnTimer() can spot training deploying itself (plateau
//--- ladder / era cap) and resync the panel's button labels exactly once on the transition
bool g_lastDeployedState = false;
//--- summarizes state across all currently-active AI signals for button labels;
//--- "paused"/"stopped" only report true if EVERY active signal agrees, so a mixed state
feat(ensemble): per-NN inputs replace the preset selector - the meta head becomes the vote's gate User design (2026-08-19): 'remove the enum menu that selects neural networks... individual inputs for every NN just like classic signals... the META NN should be integrated into the voting decision pipeline when enabled... as a bonus meta labelling is applied to enabled NNs.' - AI_CHOICE is GONE (tombstoned per the stale-.set doctrine). Use_MLP/Use_CONV/Use_LSTM/ Use_CONVLSTM are ordinary bools like the classic votes; the ensemble arithmetic adapts to any subset because the consensus divisor is the enabled capable weight. Two or more enabled = ensemble (|ENS1 token + joint gate, exactly the old AI_HYBRID fingerprints, so existing weight files keep loading); one = the old solo preset; none = classic-only. - Use_MetaLabeling un-couples META from the direction NNs (the old selector made them mutually exclusive). S3 ships: CSignalMETA::LiveMetaGate scores each vote-cleared entry (shared window at bar 1 + proposal descriptor: side, net vote, live geometry, spread/ATR; pattern one-hot ZEROED - ranking, not calibrated probability, documented in the body) and vetoes below the cost-adjusted break-even. Entries only; fail-open everywhere, loudly. - COEXISTENCE HAZARDS closed: VoteCapableWeight()=0 and ProspectiveVote()=false for the meta target - solo-only until today, a trained META would otherwise sit in the consensus divisor as a permanent abstainer and shrink every vote by its module weight. - CERTIFIED == TRADED: the ensemble era verdict replays the identical veto through the same g_warriorMetaGate pointer over its OOS fired bars (bar re-resolved from the row's own time; fail-open counted as fires and reported: 'metaGate: N approved, M vetoed, K unscored'). The overlay deliberately does NOT replay it (veto-filter-in-replay class, calendar-cliff precedent) - documented at the sweep site. Solo charts' own gate does not model the veto - the standing solo-gate caveat, documented at the input. - DB continuity: the pattern/journal DB fingerprint's first slot was (int)AIType; DbLegacyAiSlot() maps every legacy-expressible config to its OLD value (new 2-3 member subsets get 100+bitmask, outside the legacy range) so no existing database re-keys. filterID becomes the enabled roster via one EnabledNNSummary(). - HUD: the meta line shows the gate (armed/(trn), last P vs BE, ok/veto tally); the armed/disarmed announcement fires on state change via one latch (MetaGateArmedNow), not only when an entry happens to be proposed. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:01:02 -04:00
//--- (e.g. several NNs enabled with one paused and one running) still shows an actionable label
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
//--- Counted over the SIGNAL TREE, not over g_aiSignals[]: the tree is the population a panel command
//--- actually reaches, so a label can no longer describe a different set of models than the button acts
//--- on. SIGTRAIT_TRAINABLE is the denominator - "all paused" means nothing without it.
int TrainableSignalCount(void) { return Expert.CountSignalTrait(SIGTRAIT_TRAINABLE); }
bool AllTrainingPaused(void)
{
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
int n = TrainableSignalCount();
return n > 0 && Expert.CountSignalTrait(SIGTRAIT_TRAINING_PAUSED) == n;
}
bool AllTrainingStopped(void)
{
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
int n = TrainableSignalCount();
return n > 0 && Expert.CountSignalTrait(SIGTRAIT_TRAINING_STOPPED) == n;
}
//--- "deployed" = every active signal has finalised a model and is running live inference rather
//--- than training.
bool AllTrainingDeployed(void)
{
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
int n = TrainableSignalCount();
return n > 0 && Expert.CountSignalTrait(SIGTRAIT_TRAINING_COMPLETE) == n;
}
//--- true only while at least one signal is still trainable AND has never checkpointed an era that
//--- cleared the per-class recall floor - i.e. deploying right now would ship a model that ignores Buy
//--- or Sell. Same bar the plateau ladder's automatic deploy refuses to cross on its own.
bool AnyDeployWouldSkipRecallFloor(void)
{
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
return Expert.CountSignalTrait(SIGTRAIT_DEPLOY_SKIPS_RECALL) > 0;
}
void ApplySignalsVisibility(void)
{
//--- TYPED-BLIND, PREFIX-SCOPED. A mark is a line AND an arrow (see WarriorPlotSignalLevel), so
//--- a type-filtered sweep would toggle half of each one and leave the chart showing arrows for
//--- signals whose levels are hidden.
feat(ui): thresholds pick from a dropdown, and the finder arrows are back beside the level lines Two UX changes the operator asked for. THRESHOLDS. Signal_ThresholdOpen/Close were raw ints with the legal range written in the label ("[0...100, 101 = never]") - the one input style this codebase converted away from everywhere else. Open now takes the existing PERCENTAGE_PRESETS, whose comment already declared itself to be "Signal_ ThresholdOpen's scale" but was never wired to it; Close takes a new SIGNAL_CLOSE_PRESETS carrying the same rungs plus CLOSE_DISABLED = 101, which is why it cannot just reuse the other enum. Member names are prefixed because MQL5 enum members share ONE flat namespace - a bare PCT_25 in the second enum would silently resolve to the first one's, warning only. Values are unchanged, so existing .set files keep their settings. Both call sites now cast explicitly at the CExpertSignal boundary rather than leaning on an implicit enum-to-int conversion that only warns. ARROWS. 2026-08-19 replaced the low/high arrows WITH trigger-price lines; that was a swap where it should have been an addition, and it cost the zoomed-out view. A mark is now both objects: the line is the precise entry/exit level, the arrow off the candle's extreme is the finder that says there is something here to zoom into. The arrow's name is the line's plus a suffix, so it stays inside SIG_ARROW_PREFIX and every prefix-scoped purge already reaches it. The two type-filtered sweeps had to widen or they would clear one half and leave the other: the Hide/Show visibility loop and the pre-rescan scoped delete both walked OBJ_TREND only. Both are typed-blind and prefix-scoped now - the same widening this file's 2026-08-09 note describes, for the same reason it gives. Deletes go through one WarriorDeleteSignalMark() so an arrow cannot outlive the line it belongs to, and the sidecar deliberately still records one row per mark off the line (the half carrying the price), with the restore redrawing the pair. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 13:50:33 -04:00
for(int i = ObjectsTotal(0, -1, -1) - 1; i >= 0; i--)
{
feat(ui): thresholds pick from a dropdown, and the finder arrows are back beside the level lines Two UX changes the operator asked for. THRESHOLDS. Signal_ThresholdOpen/Close were raw ints with the legal range written in the label ("[0...100, 101 = never]") - the one input style this codebase converted away from everywhere else. Open now takes the existing PERCENTAGE_PRESETS, whose comment already declared itself to be "Signal_ ThresholdOpen's scale" but was never wired to it; Close takes a new SIGNAL_CLOSE_PRESETS carrying the same rungs plus CLOSE_DISABLED = 101, which is why it cannot just reuse the other enum. Member names are prefixed because MQL5 enum members share ONE flat namespace - a bare PCT_25 in the second enum would silently resolve to the first one's, warning only. Values are unchanged, so existing .set files keep their settings. Both call sites now cast explicitly at the CExpertSignal boundary rather than leaning on an implicit enum-to-int conversion that only warns. ARROWS. 2026-08-19 replaced the low/high arrows WITH trigger-price lines; that was a swap where it should have been an addition, and it cost the zoomed-out view. A mark is now both objects: the line is the precise entry/exit level, the arrow off the candle's extreme is the finder that says there is something here to zoom into. The arrow's name is the line's plus a suffix, so it stays inside SIG_ARROW_PREFIX and every prefix-scoped purge already reaches it. The two type-filtered sweeps had to widen or they would clear one half and leave the other: the Hide/Show visibility loop and the pre-rescan scoped delete both walked OBJ_TREND only. Both are typed-blind and prefix-scoped now - the same widening this file's 2026-08-09 note describes, for the same reason it gives. Deletes go through one WarriorDeleteSignalMark() so an arrow cannot outlive the line it belongs to, and the sidecar deliberately still records one row per mark off the line (the half carrying the price), with the restore redrawing the pair. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 13:50:33 -04:00
string name = ObjectName(0, i, -1, -1);
feat(chart): signal marks become price LEVELS at the trigger, not arrows beside the candle User request: 'move from arrows on lows and highs to small horizontal lines at the actual prices the entry/exit would trigger, just a bit larger than the candles. dark green for buy, dark red for sell.' Every mark is now an OBJ_TREND segment with both anchors at one price and both rays off, spanning 1.3 bar widths, drawn at the bar's CLOSE - the price a market order actually fires at, and the exact entry TripleBarrierLabel assumes. It used to sit on the candle's LOW for a Buy and its HIGH for a Sell: prices the trade never touches, picked so an arrow glyph would clear the candle. The tooltip now carries that price too. COLOUR NOW MEANS DIRECTION AND ONLY DIRECTION on every layer (dark green / dark red). Layer moves to width+style - the traded vote is solid and thick and drawn in front, a single model's raw opinion is thin, dotted and behind the candles - which keeps the distinction the old palette existed to draw (a model's opinion must never read as a trade) while freeing colour to say one thing consistently. Consequences handled, all of them the same 'a typed scan went blind' failure: - SaveChartSignals filtered OBJPROP_TYPE == OBJ_ARROW and read OBJPROP_ARROWCODE. It now filters OBJ_TREND and recovers direction from the colour. The sidecar keeps the old 217/218 numbers as its buy/sell token deliberately, so existing .arrows files still load. - AdvanceChartSignalRestore now rebuilds through the SAME creation point the live path uses, so a restored mark and a fresh one are identical objects. - The rescan-scoped delete enumerated ObjectsTotal(OBJ_ARROW) - retyped, or it silently deletes nothing. - ApplySignalsVisibility enumerated OBJ_ARROW with NO prefix filter. Under the new type that would have hidden and shown THE USER'S OWN trend lines on every Hide/Show click; it is now prefix-scoped. The old type was uncommon enough on a real chart to mask the missing check - trend lines are the most hand-drawn object there is. - DrawObject's high/low parameters are gone (6 call sites pass m_Close instead), so no caller can hand it a price it no longer draws at. - Fixed a pre-existing stale comment that still described the purge sweep as OBJ_ARROW-only three lines above the note explaining it had been widened to every type. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 14:11:49 -04:00
if(StringFind(name, SIG_ARROW_PREFIX) != 0)
continue;
ObjectSetInteger(0, name, OBJPROP_TIMEFRAMES, g_signalsVisible ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS);
}
ChartRedraw(0);
}
void ToggleSignalsVisibility(void)
{
g_signalsVisible = !g_signalsVisible;
//--- Hide->Show is also the operator's manual "these arrows look stale" refresh: rescan each
//--- deployed model against recent history BEFORE re-showing, so Show Signals reveals a fresh
//--- set instead of just re-exposing whatever old render the .arrows sidecar happened to hold
//--- (see CExpertSignalAIBase::StartChartSignalRescan/AdvanceChartSignalRescan).
if(g_signalsVisible)
{
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
bool anyQueued = (Expert.DispatchSignalCommand(SIGCMD_RESCAN_SIGNALS) > 0);
g_signalsRescanPending = anyQueued;
if(anyQueued)
{
//--- Immediate feedback that the click registered - the real "Hide Signals" label only lands
//--- once FinalizeSignalsRescanIfDone() runs RefreshControlPanelLabels() below.
ExtPanel.SetSignalsText("Scanning...");
return; // ApplySignalsVisibility()/labels/Alert deferred to FinalizeSignalsRescanIfDone()
}
}
ApplySignalsVisibility();
SaveSignalsVisibilityState();
}
//--- Called every OnTimer tick while g_signalsRescanPending: applies visibility and fires the "shown"
//--- Alert only once every queued rescan (see ToggleSignalsVisibility) has finished, since the arrows
//--- being toggled visible don't exist yet until each instance's AdvanceChartSignalRescan completes.
void FinalizeSignalsRescanIfDone(void)
{
if(!g_signalsRescanPending)
return;
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
if(Expert.CountSignalTrait(SIGTRAIT_RESCAN_PENDING) > 0)
return; // at least one instance still scanning - check again next tick
g_signalsRescanPending = false;
ApplySignalsVisibility();
RefreshControlPanelLabels();
Alert("Warrior EA: signal arrows shown");
}
//--- keeps every button's label in sync with live training/signal-visibility state; safe/cheap to
//--- call after every panel action
void RefreshControlPanelLabels(void)
{
ExtPanel.SetSignalsText(g_signalsVisible ? "Hide Signals" : "Show Signals");
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
bool noAI = (TrainableSignalCount() == 0);
//--- The four training buttons describe ONE state machine, so their labels are derived together
//--- rather than independently - otherwise the panel offers actions that silently do nothing.
bool deployed = !noAI && AllTrainingDeployed();
ExtPanel.SetPauseText(noAI ? "Pause Training (n/a)"
: deployed ? "Pause Training (deployed)"
: (AllTrainingPaused() ? "Resume Training" : "Pause Training"));
ExtPanel.SetStopText(noAI ? "Stop Training (n/a)"
: deployed ? "Stop Training (deployed)"
: (AllTrainingStopped() ? "Start Training" : "Stop Training"));
ExtPanel.SetDeployText(noAI ? "Deploy Model (n/a)" : (deployed ? "Retrain Model" : "Deploy Model"));
ChartRedraw(0);
}
//--- CAppDialog is user-draggable; a drag near an edge followed by shrinking the chart (or dragging
//--- past the visible area) can leave it partially or fully off-screen with no way to grab it back.
//--- Clamps it back inside the current chart bounds whenever the chart is resized/scrolled.
void ClampControlPanelToChart(void)
{
long chartWidth = ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
long chartHeight = ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
if(chartWidth <= 0 || chartHeight <= 0)
return;
int x = ExtPanel.Left();
int y = ExtPanel.Top();
int w = ExtPanel.Width();
int h = ExtPanel.Height();
int maxX = (int)chartWidth - w;
int maxY = (int)chartHeight - h;
int clampedX = (maxX < 0) ? 0 : MathMin(MathMax(x, 0), maxX);
int clampedY = (maxY < 0) ? 0 : MathMin(MathMax(y, 0), maxY);
if(clampedX != x || clampedY != y)
ExtPanel.Move(clampedX, clampedY);
}
//--- creates the control panel dialog once, from OnInit() - the standard CAppDialog usage pattern
//--- (create in OnInit, destroy in OnDeinit; see Controls\Dialog.mqh).
bool CreateControlPanel(void)
{
ResetLastError();
long chartWidth = ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
int panelX1 = (chartWidth > CP_PANEL_W + CP_RIGHT_MARGIN + 20) ? (int)(chartWidth - CP_PANEL_W - CP_RIGHT_MARGIN) : 10;
//--- Same orphan sweep the signal arrows get, for the same reason: CAppDialog namespaces every
//--- control it creates under the dialog name, those objects live in the CHART PROFILE, and
//--- Destroy() is the only thing that removes them.
fix: purge every EA object namespace on init and after deinit teardown Leftover objects survived deinit because the cleanup list had drifted. PurgeChart()'s own comment said it removed "our namespaced signal arrows plus the status-label objects" while the code removed arrows ONLY, and the panel prefix was swept at OnInit and nowhere else - so an ordinary deinit left the status line, and any panel straggler, on the chart. Three scattered call sites and a comment cannot be kept in step. There is now ONE list - WarriorChartPrefixes() - covering arrows, status label and panel, and one sweep, WarriorPurgeChartObjects(), used by every path. Add a prefix there when a new object family appears and every cleanup picks it up. Two call sites added: OnInit, before ANYTHING is drawn (including the status label it would otherwise delete). Chart objects live in the chart PROFILE, not in the EA, so they outlive the process: a deinit force-terminated at MetaTrader's ~4,500 ms budget, a crash, a terminal kill, or an .ex5 replaced while attached all strand objects no later deinit will ever own - and deleting the EA's files does not remove them, which is why they read as corruption. Arrows are included: LoadChartSignals restores them from their sidecar moments later and already opens with its own arrow sweep, so this only removes orphans the sidecar does not account for - the ones SaveChartSignals would otherwise ADOPT, since it rebuilds that sidecar by scanning the chart. OnDeinit, after ExtPanel.Destroy. Destroy walks an unbounded control tree and ClearStatusLabel clears text rather than guaranteeing object removal; either can leave a straggler and nothing looked afterwards. Bounded work - three prefix deletes and one object-list scan - so it respects the ordering rule that keeps the cheap visible cleanup ahead of the heavy save. Arrows excluded: ShutdownChartCleanup already persisted and removed them and re-deleting would race that write. The two are complementary: the deinit sweep closes the ordinary case, the OnInit purge closes the case where MetaTrader never let us finish. Only the second can help after a starved shutdown. Both sweeps rescan by name across EVERY object type and delete what the bulk call missed. ObjectsDeleteAll's return has already been observed disagreeing with a by-name scan of the same chart microseconds apart, and object commands are queued on the chart rather than applied inline, so a returned count is not evidence the objects are gone. Panel create site now uses WARRIOR_PANEL_PREFIX instead of a literal, so the name cannot drift away from the list that cleans it up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:19:26 -04:00
ObjectsDeleteAll(0, WARRIOR_PANEL_PREFIX);
if(!ExtPanel.Create(0, WARRIOR_PANEL_PREFIX, 0, panelX1, CP_Y0, panelX1 + CP_PANEL_W, CP_Y0 + CP_PANEL_H))
{
Print(__FUNCTION__ + ": failed to create control panel, error " + IntegerToString(GetLastError()));
return false;
}
if(!ExtPanel.Run())
{
Print(__FUNCTION__ + ": failed to run control panel, error " + IntegerToString(GetLastError()));
return false;
}
ExtPanel.ForceMaximize();
ClampControlPanelToChart();
//--- seed the transition watcher (see OnTimer) so a model that is ALREADY deployed at attach time
//--- doesn't register as a fresh transition on the first timer tick
g_lastDeployedState = AllTrainingDeployed();
RefreshControlPanelLabels();
return true;
}
//--- Blocking on purpose, unlike the Alert() calls below: this only ever runs in direct response to the
//--- trader clicking a destructive (red) panel button, so pausing for their yes/no is expected UX, not
//--- an unwanted stall of live trade management - MessageBox() briefly blocks the chart's UI thread,
//--- which is exactly what a "are you sure?" gate needs.
bool ConfirmDestructiveAction(string message)
{
return MessageBox(message + "\n\nThis action cannot be undone.", "Warrior EA - Confirm",
MB_YESNO | MB_ICONWARNING | MB_DEFBUTTON2) == IDYES;
}
//--- performs whatever button action ExtPanel recorded (see ConsumeAction() in ControlPanel.mqh);
//--- a no-op when nothing was clicked since the last call
void HandleControlPanelAction(ENUM_CP_ACTION action)
{
switch(action)
{
case CP_ACTION_TOGGLE_SIGNALS:
ToggleSignalsVisibility();
//--- If a rescan got queued (Show Signals with a deployed model to re-infer from), the
//--- label refresh + Alert are deferred to FinalizeSignalsRescanIfDone() - firing "shown"
//--- here would lie about arrows that don't exist on the chart yet.
if(!g_signalsRescanPending)
{
RefreshControlPanelLabels();
Alert("Warrior EA: signal arrows " + (g_signalsVisible ? "shown" : "hidden"));
}
break;
case CP_ACTION_TOGGLE_PAUSE:
{
//--- A deployed model has no training run to pause - say so instead of silently doing nothing.
if(AllTrainingDeployed())
{
Alert("Warrior EA: the model is deployed - there is no training run to pause.\nUse \"Retrain Model\" first if you want to train it further.");
break;
}
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
//--- Direction resolved ONCE here, then handed to every signal as a plain command. A toggle
//--- each model re-derived from its own state is how a mixed set ends up half paused.
bool pause = !AllTrainingPaused();
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
int touched = Expert.DispatchSignalCommand(pause ? SIGCMD_PAUSE_TRAINING : SIGCMD_RESUME_TRAINING);
RefreshControlPanelLabels();
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
Alert("Warrior EA: training " + (pause ? "paused" : "resumed") +
" (" + IntegerToString(touched) + " model(s))");
break;
}
case CP_ACTION_TOGGLE_STOP:
{
//--- Same as Pause: Stop/Start operate on a training run, and a deployed model isn't one.
//--- Routing this to RetrainDeployed() instead would silently do the Deploy button's job.
if(AllTrainingDeployed())
{
Alert("Warrior EA: the model is deployed and already running live inference, not training.\nUse \"Retrain Model\" to put it back into training.");
break;
}
bool doStop = !AllTrainingStopped();
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
int touched = Expert.DispatchSignalCommand(doStop ? SIGCMD_STOP_TRAINING : SIGCMD_START_TRAINING);
RefreshControlPanelLabels();
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
Alert("Warrior EA: training " + (doStop ? "stopped" : "restarted") +
" (" + IntegerToString(touched) + " model(s))");
break;
}
case CP_ACTION_TOGGLE_DEPLOY:
{
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
if(TrainableSignalCount() == 0)
{
Alert("Warrior EA: no AI signal is active - nothing to deploy (set the AI algorithm input to something other than Disabled).");
break;
}
//--- RETRAIN direction: put the finalised model back into training, continuing from its own
//--- weights. Reversible, non-destructive (the weights on disk stay), so no confirmation.
if(AllTrainingDeployed())
{
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
Expert.DispatchSignalCommand(SIGCMD_RETRAIN_DEPLOYED);
RefreshControlPanelLabels();
Alert("Warrior EA: retraining the deployed model - it continues from its current weights.\nUse \"Delete & Reset Weights\" instead to start from scratch.");
break;
}
//--- DEPLOY direction. The plateau ladder refuses to auto-deploy a model that never cleared the
//--- per-class recall floor (it would be one that ignores Buy or Sell); a manual deploy is the
//--- operator's call, but they should make it knowingly - so this is the one case that asks.
if(AnyDeployWouldSkipRecallFloor() &&
!ConfirmDestructiveAction("No training era has cleared the per-class recall floor yet, so this model may "
"be ignoring Buy or Sell entirely.\n\nDeploy it anyway as the final model?"))
{
Alert("Warrior EA: deploy cancelled - training continues");
break;
}
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
int deployed = Expert.DispatchSignalCommand(SIGCMD_DEPLOY);
RefreshControlPanelLabels();
if(deployed == 0)
Alert("Warrior EA: could not deploy - the AI signal is not initialised yet.");
else
Alert("Warrior EA: model deployed (" + IntegerToString(deployed) + " signal(s)).\nTraining stopped; it now runs live inference. Click \"Retrain Model\" to train it further.");
break;
}
case CP_ACTION_SAVE:
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
{
int saved = Expert.DispatchSignalCommand(SIGCMD_SAVE_WEIGHTS);
Alert("Warrior EA: weights saved (" + IntegerToString(saved) + " of " +
IntegerToString(TrainableSignalCount()) + " model(s))");
break;
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
}
case CP_ACTION_LOAD:
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
Expert.DispatchSignalCommand(SIGCMD_LOAD_WEIGHTS);
//--- A reload carries the saved file's own "deployed" flag, so it can flip the whole training
//--- state machine (loading a finalised model makes Deploy read "Retrain Model", and Pause/Stop
//--- read "(deployed)"). Resync the labels or the panel would keep offering stale actions.
RefreshControlPanelLabels();
Alert("Warrior EA: weights reloaded from disk");
break;
case CP_ACTION_RESET:
fix(reset): say what the reset actually did, per member and per file The user reports "Delete & Reset Weights only wipes the first NN". I could not find a code path that skips ensemble members, and I am not going to assert one: the handler loops g_aiSignals[0..g_aiSignalCount), all four topologies register unconditionally in OnInit, and SetIdentity gives each its own State\<id>\ folder so the six deleted paths are genuinely distinct per member. What IS true is that the whole success path was SILENT - six FileDelete calls per member printing only on failure, and one chart-wide Alert - so a four-member reset and a one-member reset produce byte-identical output. The symptom could be neither confirmed nor refuted from a log. That is the defect I can fix today. - COMPILED <timestamp> (__DATETIME__) beside the build tag. The hand-edited tag had sat at scan-nofwd-v5 across a week of commits, so it could not answer the question it exists for. The compile stamp cannot be forgotten. Tag bumped to reset-census-v6. - RegistryLine() (public): ID, active file path, common/local, era, deployed vs training, ensemble index. The reset handler prints a numbered census of the whole registry BEFORE the confirm dialog. If that says 1 on an AI_HYBRID chart the fault is registration, not the reset - and RegisterAISignal already has a loud MAX_AI_SIGNALS message for exactly that. - The confirmation dialog now names the count, so a wrong registry is visible before anything is deleted rather than after. - ResetWeights prints one line per member: N deleted / N already absent / N FAILED, plus a per-suffix breakdown. "absent" on a member that should have had a .nnw is a completely different fault from "deleted"; they were identical. - ResetWeights' return value was discarded. A member whose BuildFreshTopology fails has had its files deleted and has no network - and the Alert still said "weights reset". Counted now, with an INCOMPLETE alert when they disagree. - Same for dbm.ResetDatabase(), whose bool was also dropped. The DB is one shared file for every signal on the chart, so there is nothing per-member to loop - the log now says that explicitly, since it is the question being asked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:57:26 -04:00
{
//--- CENSUS BEFORE THE ACTION, walked over the SAME tree the reset itself walks - so the
//--- count and the identity lines describe exactly the models about to be wiped and cannot
//--- drift from them.
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
int trainable = TrainableSignalCount();
PrintFormat("%s: RESET requested - %d AI signal(s) reachable on this chart:",
__FUNCTION__, trainable);
Expert.DispatchSignalCommand(SIGCMD_REPORT_IDENTITY);
fix(reset): say what the reset actually did, per member and per file The user reports "Delete & Reset Weights only wipes the first NN". I could not find a code path that skips ensemble members, and I am not going to assert one: the handler loops g_aiSignals[0..g_aiSignalCount), all four topologies register unconditionally in OnInit, and SetIdentity gives each its own State\<id>\ folder so the six deleted paths are genuinely distinct per member. What IS true is that the whole success path was SILENT - six FileDelete calls per member printing only on failure, and one chart-wide Alert - so a four-member reset and a one-member reset produce byte-identical output. The symptom could be neither confirmed nor refuted from a log. That is the defect I can fix today. - COMPILED <timestamp> (__DATETIME__) beside the build tag. The hand-edited tag had sat at scan-nofwd-v5 across a week of commits, so it could not answer the question it exists for. The compile stamp cannot be forgotten. Tag bumped to reset-census-v6. - RegistryLine() (public): ID, active file path, common/local, era, deployed vs training, ensemble index. The reset handler prints a numbered census of the whole registry BEFORE the confirm dialog. If that says 1 on an AI_HYBRID chart the fault is registration, not the reset - and RegisterAISignal already has a loud MAX_AI_SIGNALS message for exactly that. - The confirmation dialog now names the count, so a wrong registry is visible before anything is deleted rather than after. - ResetWeights prints one line per member: N deleted / N already absent / N FAILED, plus a per-suffix breakdown. "absent" on a member that should have had a .nnw is a completely different fault from "deleted"; they were identical. - ResetWeights' return value was discarded. A member whose BuildFreshTopology fails has had its files deleted and has no network - and the Alert still said "weights reset". Counted now, with an INCOMPLETE alert when they disagree. - Same for dbm.ResetDatabase(), whose bool was also dropped. The DB is one shared file for every signal on the chart, so there is nothing per-member to loop - the log now says that explicitly, since it is the question being asked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:57:26 -04:00
if(!ConfirmDestructiveAction("Delete the saved AI weights of all " +
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
IntegerToString(trainable) +
fix(reset): say what the reset actually did, per member and per file The user reports "Delete & Reset Weights only wipes the first NN". I could not find a code path that skips ensemble members, and I am not going to assert one: the handler loops g_aiSignals[0..g_aiSignalCount), all four topologies register unconditionally in OnInit, and SetIdentity gives each its own State\<id>\ folder so the six deleted paths are genuinely distinct per member. What IS true is that the whole success path was SILENT - six FileDelete calls per member printing only on failure, and one chart-wide Alert - so a four-member reset and a one-member reset produce byte-identical output. The symptom could be neither confirmed nor refuted from a log. That is the defect I can fix today. - COMPILED <timestamp> (__DATETIME__) beside the build tag. The hand-edited tag had sat at scan-nofwd-v5 across a week of commits, so it could not answer the question it exists for. The compile stamp cannot be forgotten. Tag bumped to reset-census-v6. - RegistryLine() (public): ID, active file path, common/local, era, deployed vs training, ensemble index. The reset handler prints a numbered census of the whole registry BEFORE the confirm dialog. If that says 1 on an AI_HYBRID chart the fault is registration, not the reset - and RegisterAISignal already has a loud MAX_AI_SIGNALS message for exactly that. - The confirmation dialog now names the count, so a wrong registry is visible before anything is deleted rather than after. - ResetWeights prints one line per member: N deleted / N already absent / N FAILED, plus a per-suffix breakdown. "absent" on a member that should have had a .nnw is a completely different fault from "deleted"; they were identical. - ResetWeights' return value was discarded. A member whose BuildFreshTopology fails has had its files deleted and has no network - and the Alert still said "weights reset". Counted now, with an INCOMPLETE alert when they disagree. - Same for dbm.ResetDatabase(), whose bool was also dropped. The DB is one shared file for every signal on the chart, so there is nothing per-member to loop - the log now says that explicitly, since it is the question being asked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:57:26 -04:00
" AI model(s) on this chart and restart training from era 0?"))
{
Alert("Warrior EA: weights reset cancelled");
break;
}
//--- ResetWeights() returns whether the fresh topology was REBUILT, and that return has
//--- been discarded since it was written. Count it, and say so when they disagree.
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
int resetOk = Expert.DispatchSignalCommand(SIGCMD_RESET_WEIGHTS);
RefreshControlPanelLabels();
fix(reset): say what the reset actually did, per member and per file The user reports "Delete & Reset Weights only wipes the first NN". I could not find a code path that skips ensemble members, and I am not going to assert one: the handler loops g_aiSignals[0..g_aiSignalCount), all four topologies register unconditionally in OnInit, and SetIdentity gives each its own State\<id>\ folder so the six deleted paths are genuinely distinct per member. What IS true is that the whole success path was SILENT - six FileDelete calls per member printing only on failure, and one chart-wide Alert - so a four-member reset and a one-member reset produce byte-identical output. The symptom could be neither confirmed nor refuted from a log. That is the defect I can fix today. - COMPILED <timestamp> (__DATETIME__) beside the build tag. The hand-edited tag had sat at scan-nofwd-v5 across a week of commits, so it could not answer the question it exists for. The compile stamp cannot be forgotten. Tag bumped to reset-census-v6. - RegistryLine() (public): ID, active file path, common/local, era, deployed vs training, ensemble index. The reset handler prints a numbered census of the whole registry BEFORE the confirm dialog. If that says 1 on an AI_HYBRID chart the fault is registration, not the reset - and RegisterAISignal already has a loud MAX_AI_SIGNALS message for exactly that. - The confirmation dialog now names the count, so a wrong registry is visible before anything is deleted rather than after. - ResetWeights prints one line per member: N deleted / N already absent / N FAILED, plus a per-suffix breakdown. "absent" on a member that should have had a .nnw is a completely different fault from "deleted"; they were identical. - ResetWeights' return value was discarded. A member whose BuildFreshTopology fails has had its files deleted and has no network - and the Alert still said "weights reset". Counted now, with an INCOMPLETE alert when they disagree. - Same for dbm.ResetDatabase(), whose bool was also dropped. The DB is one shared file for every signal on the chart, so there is nothing per-member to loop - the log now says that explicitly, since it is the question being asked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:57:26 -04:00
PrintFormat("%s: RESET complete - %d of %d AI signal(s) rebuilt a fresh topology.",
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
__FUNCTION__, resetOk, trainable);
if(trainable == 0)
fix(reset): say what the reset actually did, per member and per file The user reports "Delete & Reset Weights only wipes the first NN". I could not find a code path that skips ensemble members, and I am not going to assert one: the handler loops g_aiSignals[0..g_aiSignalCount), all four topologies register unconditionally in OnInit, and SetIdentity gives each its own State\<id>\ folder so the six deleted paths are genuinely distinct per member. What IS true is that the whole success path was SILENT - six FileDelete calls per member printing only on failure, and one chart-wide Alert - so a four-member reset and a one-member reset produce byte-identical output. The symptom could be neither confirmed nor refuted from a log. That is the defect I can fix today. - COMPILED <timestamp> (__DATETIME__) beside the build tag. The hand-edited tag had sat at scan-nofwd-v5 across a week of commits, so it could not answer the question it exists for. The compile stamp cannot be forgotten. Tag bumped to reset-census-v6. - RegistryLine() (public): ID, active file path, common/local, era, deployed vs training, ensemble index. The reset handler prints a numbered census of the whole registry BEFORE the confirm dialog. If that says 1 on an AI_HYBRID chart the fault is registration, not the reset - and RegisterAISignal already has a loud MAX_AI_SIGNALS message for exactly that. - The confirmation dialog now names the count, so a wrong registry is visible before anything is deleted rather than after. - ResetWeights prints one line per member: N deleted / N already absent / N FAILED, plus a per-suffix breakdown. "absent" on a member that should have had a .nnw is a completely different fault from "deleted"; they were identical. - ResetWeights' return value was discarded. A member whose BuildFreshTopology fails has had its files deleted and has no network - and the Alert still said "weights reset". Counted now, with an INCOMPLETE alert when they disagree. - Same for dbm.ResetDatabase(), whose bool was also dropped. The DB is one shared file for every signal on the chart, so there is nothing per-member to loop - the log now says that explicitly, since it is the question being asked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:57:26 -04:00
Alert("Warrior EA: nothing to reset - no AI signal is registered on this chart.");
else
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
if(resetOk == trainable)
fix(reset): say what the reset actually did, per member and per file The user reports "Delete & Reset Weights only wipes the first NN". I could not find a code path that skips ensemble members, and I am not going to assert one: the handler loops g_aiSignals[0..g_aiSignalCount), all four topologies register unconditionally in OnInit, and SetIdentity gives each its own State\<id>\ folder so the six deleted paths are genuinely distinct per member. What IS true is that the whole success path was SILENT - six FileDelete calls per member printing only on failure, and one chart-wide Alert - so a four-member reset and a one-member reset produce byte-identical output. The symptom could be neither confirmed nor refuted from a log. That is the defect I can fix today. - COMPILED <timestamp> (__DATETIME__) beside the build tag. The hand-edited tag had sat at scan-nofwd-v5 across a week of commits, so it could not answer the question it exists for. The compile stamp cannot be forgotten. Tag bumped to reset-census-v6. - RegistryLine() (public): ID, active file path, common/local, era, deployed vs training, ensemble index. The reset handler prints a numbered census of the whole registry BEFORE the confirm dialog. If that says 1 on an AI_HYBRID chart the fault is registration, not the reset - and RegisterAISignal already has a loud MAX_AI_SIGNALS message for exactly that. - The confirmation dialog now names the count, so a wrong registry is visible before anything is deleted rather than after. - ResetWeights prints one line per member: N deleted / N already absent / N FAILED, plus a per-suffix breakdown. "absent" on a member that should have had a .nnw is a completely different fault from "deleted"; they were identical. - ResetWeights' return value was discarded. A member whose BuildFreshTopology fails has had its files deleted and has no network - and the Alert still said "weights reset". Counted now, with an INCOMPLETE alert when they disagree. - Same for dbm.ResetDatabase(), whose bool was also dropped. The DB is one shared file for every signal on the chart, so there is nothing per-member to loop - the log now says that explicitly, since it is the question being asked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:57:26 -04:00
Alert("Warrior EA: weights reset for " + IntegerToString(resetOk) +
" model(s) - training restarts from era 0");
else
Alert("Warrior EA: weights reset INCOMPLETE - " + IntegerToString(resetOk) + " of " +
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
IntegerToString(trainable) + " model(s) rebuilt.\nThe rest had their files "
fix(reset): say what the reset actually did, per member and per file The user reports "Delete & Reset Weights only wipes the first NN". I could not find a code path that skips ensemble members, and I am not going to assert one: the handler loops g_aiSignals[0..g_aiSignalCount), all four topologies register unconditionally in OnInit, and SetIdentity gives each its own State\<id>\ folder so the six deleted paths are genuinely distinct per member. What IS true is that the whole success path was SILENT - six FileDelete calls per member printing only on failure, and one chart-wide Alert - so a four-member reset and a one-member reset produce byte-identical output. The symptom could be neither confirmed nor refuted from a log. That is the defect I can fix today. - COMPILED <timestamp> (__DATETIME__) beside the build tag. The hand-edited tag had sat at scan-nofwd-v5 across a week of commits, so it could not answer the question it exists for. The compile stamp cannot be forgotten. Tag bumped to reset-census-v6. - RegistryLine() (public): ID, active file path, common/local, era, deployed vs training, ensemble index. The reset handler prints a numbered census of the whole registry BEFORE the confirm dialog. If that says 1 on an AI_HYBRID chart the fault is registration, not the reset - and RegisterAISignal already has a loud MAX_AI_SIGNALS message for exactly that. - The confirmation dialog now names the count, so a wrong registry is visible before anything is deleted rather than after. - ResetWeights prints one line per member: N deleted / N already absent / N FAILED, plus a per-suffix breakdown. "absent" on a member that should have had a .nnw is a completely different fault from "deleted"; they were identical. - ResetWeights' return value was discarded. A member whose BuildFreshTopology fails has had its files deleted and has no network - and the Alert still said "weights reset". Counted now, with an INCOMPLETE alert when they disagree. - Same for dbm.ResetDatabase(), whose bool was also dropped. The DB is one shared file for every signal on the chart, so there is nothing per-member to loop - the log now says that explicitly, since it is the question being asked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:57:26 -04:00
"deleted but could not rebuild a topology - see the Experts log.");
break;
fix(reset): say what the reset actually did, per member and per file The user reports "Delete & Reset Weights only wipes the first NN". I could not find a code path that skips ensemble members, and I am not going to assert one: the handler loops g_aiSignals[0..g_aiSignalCount), all four topologies register unconditionally in OnInit, and SetIdentity gives each its own State\<id>\ folder so the six deleted paths are genuinely distinct per member. What IS true is that the whole success path was SILENT - six FileDelete calls per member printing only on failure, and one chart-wide Alert - so a four-member reset and a one-member reset produce byte-identical output. The symptom could be neither confirmed nor refuted from a log. That is the defect I can fix today. - COMPILED <timestamp> (__DATETIME__) beside the build tag. The hand-edited tag had sat at scan-nofwd-v5 across a week of commits, so it could not answer the question it exists for. The compile stamp cannot be forgotten. Tag bumped to reset-census-v6. - RegistryLine() (public): ID, active file path, common/local, era, deployed vs training, ensemble index. The reset handler prints a numbered census of the whole registry BEFORE the confirm dialog. If that says 1 on an AI_HYBRID chart the fault is registration, not the reset - and RegisterAISignal already has a loud MAX_AI_SIGNALS message for exactly that. - The confirmation dialog now names the count, so a wrong registry is visible before anything is deleted rather than after. - ResetWeights prints one line per member: N deleted / N already absent / N FAILED, plus a per-suffix breakdown. "absent" on a member that should have had a .nnw is a completely different fault from "deleted"; they were identical. - ResetWeights' return value was discarded. A member whose BuildFreshTopology fails has had its files deleted and has no network - and the Alert still said "weights reset". Counted now, with an INCOMPLETE alert when they disagree. - Same for dbm.ResetDatabase(), whose bool was also dropped. The DB is one shared file for every signal on the chart, so there is nothing per-member to loop - the log now says that explicitly, since it is the question being asked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:57:26 -04:00
}
case CP_ACTION_REPORT:
{
if(!UseDatabaseRanking)
{
Print(__FUNCTION__ + ": trade journal report requires \"Weight filters by DB win-rate\" (UseDatabaseRanking) to be enabled");
Alert("Warrior EA: trade journal report requires \"Weight filters by DB win-rate\" to be enabled");
break;
}
string reportPath, reportError;
if(journal.GenerateReport(reportPath, reportError))
{
Print(__FUNCTION__ + ": trade journal report ready - " + reportPath);
Alert("Warrior EA: trade journal report exported - see the Experts log for the file path");
}
else
{
Print(__FUNCTION__ + ": could not generate trade journal report - " + reportError);
Alert("Warrior EA: could not export trade journal report - " + reportError);
}
break;
}
//--- separate from CP_ACTION_RESET on purpose: resetting AI weights (a routine, frequent action
//--- while tuning) must never cost the trader their accumulated pattern-confidence/trade-journal
//--- history, and vice versa - these are two independent "start fresh" decisions now.
case CP_ACTION_RESET_DB:
if(UseDatabaseRanking)
{
if(!ConfirmDestructiveAction("Delete the trade-journal and pattern-confidence database?"))
{
Alert("Warrior EA: database reset cancelled");
break;
}
//--- ResetDatabase() returns false when the file could not be deleted or the connection
//--- could not be reopened, and that return was discarded too - the Alert said
//--- "database reset" either way.
fix(reset): say what the reset actually did, per member and per file The user reports "Delete & Reset Weights only wipes the first NN". I could not find a code path that skips ensemble members, and I am not going to assert one: the handler loops g_aiSignals[0..g_aiSignalCount), all four topologies register unconditionally in OnInit, and SetIdentity gives each its own State\<id>\ folder so the six deleted paths are genuinely distinct per member. What IS true is that the whole success path was SILENT - six FileDelete calls per member printing only on failure, and one chart-wide Alert - so a four-member reset and a one-member reset produce byte-identical output. The symptom could be neither confirmed nor refuted from a log. That is the defect I can fix today. - COMPILED <timestamp> (__DATETIME__) beside the build tag. The hand-edited tag had sat at scan-nofwd-v5 across a week of commits, so it could not answer the question it exists for. The compile stamp cannot be forgotten. Tag bumped to reset-census-v6. - RegistryLine() (public): ID, active file path, common/local, era, deployed vs training, ensemble index. The reset handler prints a numbered census of the whole registry BEFORE the confirm dialog. If that says 1 on an AI_HYBRID chart the fault is registration, not the reset - and RegisterAISignal already has a loud MAX_AI_SIGNALS message for exactly that. - The confirmation dialog now names the count, so a wrong registry is visible before anything is deleted rather than after. - ResetWeights prints one line per member: N deleted / N already absent / N FAILED, plus a per-suffix breakdown. "absent" on a member that should have had a .nnw is a completely different fault from "deleted"; they were identical. - ResetWeights' return value was discarded. A member whose BuildFreshTopology fails has had its files deleted and has no network - and the Alert still said "weights reset". Counted now, with an INCOMPLETE alert when they disagree. - Same for dbm.ResetDatabase(), whose bool was also dropped. The DB is one shared file for every signal on the chart, so there is nothing per-member to loop - the log now says that explicitly, since it is the question being asked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:57:26 -04:00
bool dbReset = dbm.ResetDatabase();
PrintFormat("%s: DATABASE RESET %s - one shared trade-journal/pattern-confidence DB serves all"
" %d AI signal(s) on this chart; per-model weights are NOT affected (use Delete &&"
" Reset Weights for those).", __FUNCTION__,
feat(panel): commands reach signals down the filter tree, not through a registry The control panel drove training by looping g_aiSignals[] - a hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had already dropped an ensemble member on the floor once (609be10). A model missing from it still trains and still votes, it just cannot be paused, stopped, deployed or reset, and every button label is computed from the same short list, so the panel described one set of models while acting on another. Classic signals could not respond to a panel action at all. Commands now walk the signal tree CExpert already owns: Expert.DispatchSignalCommand(cmd) -> root signal -> every filter, recursively, returning how many actually acted. CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait, both no-ops by default), so a classic signal opts in by overriding two methods and needs no registration and no cap. CExpertSignalAIBase implements the training commands over its existing Pause/Stop/Deploy/ Reset methods - the behaviour is unchanged, only its reach reported. Button labels ask the same tree via CountSignalTrait, with SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is meaningless without knowing how many could be paused. Pause/Stop resolve their toggle direction ONCE in the EA and hand every model the same plain command, instead of each re-deriving the direction from its own local state - which is how a mixed set ends up half paused. The alerts now report the count acted on rather than assuming it. Two dispatch bugs found on the way, both from a database guard copied onto event delivery: CExpertSignalCustom::OnTickHandler and ::OnChartEventHandler each skipped any filter whose GetFilterID() is "NULL". That id is a DB folder name, and CSignalNewsFilter, CSignalSessionFilter and CSignalRiskGuard never set one - so all three were silently receiving neither ticks nor chart events. The guard stays where it belongs, on the paths that write pattern tables. ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include- guarded) because the Expert bases have to name it and the panel is included long after them. The AI-only lifecycle loops - PollTraining, the weight autosave, AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and are untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00
(dbReset ? "succeeded" : "FAILED"), TrainableSignalCount());
fix(reset): say what the reset actually did, per member and per file The user reports "Delete & Reset Weights only wipes the first NN". I could not find a code path that skips ensemble members, and I am not going to assert one: the handler loops g_aiSignals[0..g_aiSignalCount), all four topologies register unconditionally in OnInit, and SetIdentity gives each its own State\<id>\ folder so the six deleted paths are genuinely distinct per member. What IS true is that the whole success path was SILENT - six FileDelete calls per member printing only on failure, and one chart-wide Alert - so a four-member reset and a one-member reset produce byte-identical output. The symptom could be neither confirmed nor refuted from a log. That is the defect I can fix today. - COMPILED <timestamp> (__DATETIME__) beside the build tag. The hand-edited tag had sat at scan-nofwd-v5 across a week of commits, so it could not answer the question it exists for. The compile stamp cannot be forgotten. Tag bumped to reset-census-v6. - RegistryLine() (public): ID, active file path, common/local, era, deployed vs training, ensemble index. The reset handler prints a numbered census of the whole registry BEFORE the confirm dialog. If that says 1 on an AI_HYBRID chart the fault is registration, not the reset - and RegisterAISignal already has a loud MAX_AI_SIGNALS message for exactly that. - The confirmation dialog now names the count, so a wrong registry is visible before anything is deleted rather than after. - ResetWeights prints one line per member: N deleted / N already absent / N FAILED, plus a per-suffix breakdown. "absent" on a member that should have had a .nnw is a completely different fault from "deleted"; they were identical. - ResetWeights' return value was discarded. A member whose BuildFreshTopology fails has had its files deleted and has no network - and the Alert still said "weights reset". Counted now, with an INCOMPLETE alert when they disagree. - Same for dbm.ResetDatabase(), whose bool was also dropped. The DB is one shared file for every signal on the chart, so there is nothing per-member to loop - the log now says that explicitly, since it is the question being asked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:57:26 -04:00
Alert(dbReset
? "Warrior EA: database reset"
: "Warrior EA: database reset FAILED - see the Experts log");
}
else
{
Print(__FUNCTION__ + ": database reset requires \"Weight filters by DB win-rate\" (UseDatabaseRanking) to be enabled");
Alert("Warrior EA: database reset requires \"Weight filters by DB win-rate\" to be enabled");
}
break;
default:
break;
}
}
// Helper function to pause execution for a random duration between 1 to 3 seconds
void RandomSleep()
{
feat(rng): ALGLIB's L'Ecuyer generator replaces MathRand, and a seed collision goes with it MQL5's MathRand() is the 15-bit MSVC LCG - 32768 distinct values and the lattice structure that shape of generator has. Two places here actually lean on randomness and both were hurt by it: WEIGHT INIT. Six He/LeCun-uniform sites drew ((MathRand()+1)/32768.0 - 0.5) * 2 * scale, so a first dense layer of ~250k weights had only 32768 possible values and thousands of connections started byte-identical. Breaking that symmetry is the whole job of random init. SHUFFLING. ShuffleRandomIndex() already had to splice TWO MathRand() draws to reach 30 bits, and its own comment documented the residual modulo bias it still carried. HQRndUniformI() is rejection-sampled and exactly uniform, so the splice and the bias note both go. CHighQualityRand is L'Ecuyer's combined multiplicative congruential generator - two differenced streams, 31-bit output, period ~2.3e18 - and it ships with the terminal. AND A BUG THE MIGRATION EXPOSED. The three MathSrand(GetTickCount()) calls sit immediately before "build a fresh topology", once per model. GetTickCount() steps in ~15.6 ms on Windows and an ensemble builds every member inside one OnInit, so members could be handed the SAME seed and draw the SAME weights wherever their shapes coincide - and members that start identical are not an ensemble. WarriorRandSeed() takes a salt (the model id) plus a never-reset call counter, so a collision is impossible rather than merely unlikely, while the tick keeps the run itself genuinely unrepeatable the way those call sites asked for. Seeds are masked positive rather than trusted: HQRndSeed computes s % (M-1) + 1 and MQL5's % keeps the sign, so a negative seed leaves the generator in a state its own assertions reject. GetTickCount() is a uint and goes negative as an int after ~24 days of uptime - a fault that would surface as "training is broken" on a long-running terminal and nowhere else. The indicator tuner's 52 draws move across too: its random search is where sample quality earns its keep. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:29:12 -04:00
Sleep(WarriorRandInt(2000) + 1000); // Sleeps between 1000ms (1s) and 3000ms (3s)
}
//+------------------------------------------------------------------+
//| The DB fingerprint's first slot - was (int)AIType until the |
//| preset selector was replaced by the per-NN toggles (2026-08-19). |
feat(ensemble): per-NN inputs replace the preset selector - the meta head becomes the vote's gate User design (2026-08-19): 'remove the enum menu that selects neural networks... individual inputs for every NN just like classic signals... the META NN should be integrated into the voting decision pipeline when enabled... as a bonus meta labelling is applied to enabled NNs.' - AI_CHOICE is GONE (tombstoned per the stale-.set doctrine). Use_MLP/Use_CONV/Use_LSTM/ Use_CONVLSTM are ordinary bools like the classic votes; the ensemble arithmetic adapts to any subset because the consensus divisor is the enabled capable weight. Two or more enabled = ensemble (|ENS1 token + joint gate, exactly the old AI_HYBRID fingerprints, so existing weight files keep loading); one = the old solo preset; none = classic-only. - Use_MetaLabeling un-couples META from the direction NNs (the old selector made them mutually exclusive). S3 ships: CSignalMETA::LiveMetaGate scores each vote-cleared entry (shared window at bar 1 + proposal descriptor: side, net vote, live geometry, spread/ATR; pattern one-hot ZEROED - ranking, not calibrated probability, documented in the body) and vetoes below the cost-adjusted break-even. Entries only; fail-open everywhere, loudly. - COEXISTENCE HAZARDS closed: VoteCapableWeight()=0 and ProspectiveVote()=false for the meta target - solo-only until today, a trained META would otherwise sit in the consensus divisor as a permanent abstainer and shrink every vote by its module weight. - CERTIFIED == TRADED: the ensemble era verdict replays the identical veto through the same g_warriorMetaGate pointer over its OOS fired bars (bar re-resolved from the row's own time; fail-open counted as fires and reported: 'metaGate: N approved, M vetoed, K unscored'). The overlay deliberately does NOT replay it (veto-filter-in-replay class, calendar-cliff precedent) - documented at the sweep site. Solo charts' own gate does not model the veto - the standing solo-gate caveat, documented at the input. - DB continuity: the pattern/journal DB fingerprint's first slot was (int)AIType; DbLegacyAiSlot() maps every legacy-expressible config to its OLD value (new 2-3 member subsets get 100+bitmask, outside the legacy range) so no existing database re-keys. filterID becomes the enabled roster via one EnabledNNSummary(). - HUD: the meta line shows the gate (armed/(trn), last P vs BE, ok/veto tally); the armed/disarmed announcement fires on state change via one latch (MetaGateArmedNow), not only when an entry happens to be proposed. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:01:02 -04:00
//+------------------------------------------------------------------+
int DbLegacyAiSlot()
{
int bits = (Use_MLP ? 1 : 0) | (Use_CONV ? 2 : 0) | (Use_LSTM ? 4 : 0) | (Use_CONVLSTM ? 8 : 0);
if(bits == 15)
return 6; // all four = the old AI_HYBRID ensemble preset
if(bits == 0)
return Use_MetaLabeling ? 5 : 0; // legacy AI_META / AI_NONE
if(bits == 1)
return 1; // AI_MLP
if(bits == 2)
return 2; // AI_CONV
if(bits == 4)
return 3; // AI_LSTM
if(bits == 8)
return 4; // AI_CONVLSTM
return 100 + bits; // new subset - outside the legacy value space by construction
}
//+------------------------------------------------------------------+
//| Config fingerprint appended to the pattern-confidence/trade- |
//| journal database's filename, so a topology or feature-set change |
//| that would produce a differently-shaped/behaving model gets its |
//| own database instead of silently reusing pattern-weight/journal |
//| history that no longer matches what's actually trading now. |
//+------------------------------------------------------------------+
string ComputeDbConfigFingerprint()
{
//--- InitialNeurons is gone from this key: the first-layer width is now DERIVED from the input
//--- width and the study period (CExpertSignalAIBase::ComputeFirstLayerWidth), not chosen, and
//--- every determinant of it that IS a user choice is already hashed here.
fix(db): per-side pattern journaling + versioned journaling semantics The labelMatchesVote gate compared a single last-writer-wins label (LongCondition then ShortCondition) against the net vote sign, which structurally censored the pattern tables: a long event co-occurring with any short-side state model lost its label to the later writer and was dropped, while the mirrored short event journaled fine. Ichimoku models 0/3 and MA model 1 could not produce a row at all by construction (MA model 1 was "revived" in 8710240 yet still could never journal - its weight-10 vote is exactly cancelled by the opposing Pattern_0 state), and every pattern's win rate was measured on a with-trend-only subset - the exact statistic UpdateSignalsWeights() feeds back into the weights, self-sealing: no rows -> no win rate -> default weight -> still censored. - Direction() now evaluates the two ladders separately and snapshots each ladder's matched pattern into its own side slot; each side that matched journals its own row. The flat-vote poisoning the old gate fixed stays fixed: a label can no longer contradict its side. - The filter's net vote (raw pattern-weight units) is stored as a new netVote column - data, never a drop filter. Snapshot is keyed on the ladder setting a label, not on its weight, so a 0%-win-rate pattern keeps journaling and can recover. - SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB filename fingerprint: pattern-definition changes (b2069bc, 8710240) re-key the database instead of blending incompatible Pattern_N populations under one key, which the input-hash fingerprint cannot see. 7 months of mixed-semantics rows shared one file because of it. - dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new column, so the version-mismatch folder wipe is the migration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
string fp = StringFormat("SEM%d|", SIGNAL_DB_SEMANTICS_VERSION)
+ StringFormat("%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d|%d",
feat(ensemble): per-NN inputs replace the preset selector - the meta head becomes the vote's gate User design (2026-08-19): 'remove the enum menu that selects neural networks... individual inputs for every NN just like classic signals... the META NN should be integrated into the voting decision pipeline when enabled... as a bonus meta labelling is applied to enabled NNs.' - AI_CHOICE is GONE (tombstoned per the stale-.set doctrine). Use_MLP/Use_CONV/Use_LSTM/ Use_CONVLSTM are ordinary bools like the classic votes; the ensemble arithmetic adapts to any subset because the consensus divisor is the enabled capable weight. Two or more enabled = ensemble (|ENS1 token + joint gate, exactly the old AI_HYBRID fingerprints, so existing weight files keep loading); one = the old solo preset; none = classic-only. - Use_MetaLabeling un-couples META from the direction NNs (the old selector made them mutually exclusive). S3 ships: CSignalMETA::LiveMetaGate scores each vote-cleared entry (shared window at bar 1 + proposal descriptor: side, net vote, live geometry, spread/ATR; pattern one-hot ZEROED - ranking, not calibrated probability, documented in the body) and vetoes below the cost-adjusted break-even. Entries only; fail-open everywhere, loudly. - COEXISTENCE HAZARDS closed: VoteCapableWeight()=0 and ProspectiveVote()=false for the meta target - solo-only until today, a trained META would otherwise sit in the consensus divisor as a permanent abstainer and shrink every vote by its module weight. - CERTIFIED == TRADED: the ensemble era verdict replays the identical veto through the same g_warriorMetaGate pointer over its OOS fired bars (bar re-resolved from the row's own time; fail-open counted as fires and reported: 'metaGate: N approved, M vetoed, K unscored'). The overlay deliberately does NOT replay it (veto-filter-in-replay class, calendar-cliff precedent) - documented at the sweep site. Solo charts' own gate does not model the veto - the standing solo-gate caveat, documented at the input. - DB continuity: the pattern/journal DB fingerprint's first slot was (int)AIType; DbLegacyAiSlot() maps every legacy-expressible config to its OLD value (new 2-3 member subsets get 100+bitmask, outside the legacy range) so no existing database re-keys. filterID becomes the enabled roster via one EnabledNNSummary(). - HUD: the meta line shows the gate (armed/(trn), last P vs BE, ok/veto tally); the armed/disarmed announcement fires on state change via one latch (MetaGateArmedNow), not only when an entry happens to be proposed. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:01:02 -04:00
DbLegacyAiSlot(), (int)OutputNeuronsCount, (int)TrainingOptimizer,
//--- LEGACY SLOT (was ind_Periods, derived since 2026-08-11). The literal
//--- is the shipped default so every existing database keeps its key.
20,
EnableVolume, EnableTime, EnableATR,
//--- TUNED values, not the seed constants (2026-08-16): the classic votes
//--- run at the adopted periods, so the DB key must describe THOSE - a
//--- tuning install re-keys the database, which is the semantics rule
//--- (new periods = new pattern definitions = fresh win-rate history).
EnableMA, g_TunedMaPeriod, g_TunedMaType, EnableRSI, g_TunedRsiPeriod,
EnableSwingContext, EnableNews,
EnableADCumulativeDelta, EnableADShorteningOfThrust, EnableADWyckoffEventStream)
feat(nn): derive conv filter count and LSTM hidden size from the data Same defect the first-layer width had before 2026-07-29: both were inputs whose defaults were fixed constants picked with no reference to the input they sit on, which is the only thing that decides whether either number is sane. The conv layer is a per-bar projection - AddConvStage sets window = step = one bar's features - so its filter count should be read against the per-bar feature count. Sixteen filters COMPRESSED a 50-feature configuration 3x but EXPANDED a minimal 4-feature one 4x, and the expanding case adds parameters below every learnable layer without adding information. Now derived as half the per-bar feature count, snapped down a power-of-two ladder. The LSTM stage was the bigger miss. Its weight count is exactly 4*H*(H+inputs+1) (CNeuronLSTMOCL::SetInputs) and AddLstmStage feeds it the whole flattened vector, so the shipped 32 units against a 540-wide input is ~73k weights - more than DOUBLE the entire derived dense taper it feeds. It was the one stage the capacity budget never covered, which is why deriving the dense stack alone did not stop LSTM and HYBRID from being over-parameterized. Now solved from the same one-weight-per-in-sample-bar budget the first layer spends. Factored EstimatedInSampleBars() out of ComputeFirstLayerWidth so all three decisions spend one budget rather than each guessing at the training-set size separately. Both new values are assigned alongside the first-layer width, before the fingerprint that hashes them, and are functions of inputs already in that hash - so they need no entry of their own, and the same reasoning removes them from the DB config key. Both builds compile 0 errors, 0 warnings. Re-keys existing models. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 09:22:11 -04:00
+ StringFormat("|%d|%d|%d|%d", EnableADWyckoffFailedStructure, EnableADWyckoffSignificantBarInversion,
EnableMAFeature, EnableRSIFeature);
//--- MACD/Ichimoku, appended ONLY WHEN ENABLED - same rule, and same reason, as the matching block
//--- in CExpertSignalAIBase::BuildConfigFingerprint(): appending unconditionally would re-key every
//--- existing database the moment this shipped, orphaning the accumulated per-pattern win-rate
//--- history of configs that use neither.
if(EnableMACD || EnableMACDFeature)
fp += StringFormat("|MACD:%d:%d:%d:%d:%d", EnableMACD, EnableMACDFeature,
g_TunedMacdFast, g_TunedMacdSlow, g_TunedMacdSignal);
if(EnableIchimoku || EnableIchimokuFeature)
fp += StringFormat("|ICHI:%d:%d:%d:%d:%d", EnableIchimoku, EnableIchimokuFeature,
g_TunedIchiTenkan, g_TunedIchiKijun, g_TunedIchiSenkou);
//--- Alt-data block, conditional like MACD/ICHI: enabling it changes what the model trades on, so it
//--- keys the database; only the FLAG goes in, never the per-symbol feature list - that is a measured
//--- property served by the AltData file and pinned per-model in the .cfg, and a measured quantity
//--- must not key a filename (same rule as the cross-asset pair set).
if(EnableAltData)
fp += "|ALT:1";
//--- Cross-asset, conditional for the same reason. Only the flag goes in, never the discovered
//--- reference set - see the matching comment in BuildConfigFingerprint() for why a measured
//--- quantity must not key a filename.
fix(signals): revive a dead MA model, and demote Sanyaku from state to event Two defects surfaced by research/test_classic.py, both verified fixed by re-running the transcription against 178k bars of EURUSD H1. CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so DiffMA(i) = a * (Close(i) - MA(i+1)) DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1)) are positive multiples of one quantity and always share a sign. Model 1 asks for a close BELOW a RISING average, which is precisely the combination that identity forbids: 0.000% of bars, either direction, any symbol. The MQL5 standard library this was ported from defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars. CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing STATES conjoined with no transition term, so it held across long stretches - and being last in the if-chain at the top weight, the module's highest-conviction reading was also its most common one, overwriting all eight event models below it on a quarter of all bars. The old comment rejected an event form because "demanding all three flip on the same bar would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1) fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback. Neither pattern showed edge before or after; this is about the models meaning what they say and the vote not being dominated by a constant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
if(EnableCrossAsset)
fp += StringFormat("|XA:%d", EnableCrossAsset);
feat(ai): spread as a volatility-regime feature, and fix a stale-index cache in both new blocks Adds spread/ATR and the spread change ratio as network inputs (EnableSpreadFeature, default on). Spread is the one microstructure channel that is both FX-available and genuinely historical in the Strategy Tester - "during testing, the spread is not modeled but is taken from historical data" - so unlike swap, signed tick flow or depth of market it is something a backtest can honestly validate. What it encodes, stated precisely because the raw measurement overstates it. research/test_spread.py found spr/atr the strongest single feature in this codebase, on 5 of 8 instrument/geometry cells at 2-4x any volume feature. But the barrier LABEL charges the spread inside its own barriers, so a wide-spread bar is mechanically likelier to resolve as a loss and the feature would partly be predicting its own cost model. Relabelling at zero cost and re-measuring the identical feature showed 20-40% of it WAS that tautology and the majority was not (XAUUSD retained 97%). What survives is a volatility-regime reading: spread is near-fixed while ATR is not, so the ratio runs high exactly when realised volatility is below its own ATR estimate, which genuinely predicts whether ATR-scaled barriers get reached. It is UNSIGNED - Neutral-vs-directional only, never a side. Also fixes a stale-index bug I introduced with the cross-asset panel and had just repeated in the spread series. Both cached on length alone: if(m_crossAsset.Bars() >= bars) return true; MQL5 series indices are relative to NOW, so one new closed candle shifts every index by one. Keyed only on length, the panel keeps serving its index 0 as a bar that is no longer the newest, and every cross-asset value is read one bar out of step with the price features sitting beside it in the same vector - silently, with no error and no shape change. This is the same class of defect as the dtStudied watermark behind the zero-direction backtests. Both now carry a datetime anchor on m_Time.GetData(0), the same invalidation key the label/feature bar caches already use. And a performance fix that fell out of it: with correct invalidation the panel rebuilds on every new bar, and RefreshConvergedSignal runs per bar - which in the tester would mean one full multi-symbol resample per simulated bar at training depth. Inference only reads bars 0..m_historyBars-1 plus the panel's own slow window, so it now requests exactly that. The cache check is >=, so a deeper panel left from training still satisfies it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:42:40 -04:00
if(EnableSpreadFeature)
fp += StringFormat("|SPR:%d", EnableSpreadFeature);
uint fpHash = 2166136261;
int fpLen = StringLen(fp);
for(int fpi = 0; fpi < fpLen; fpi++)
{
fpHash ^= (uint)StringGetCharacter(fp, fpi);
fpHash *= 16777619;
}
return StringFormat("%08x", fpHash);
}
fix: refuse invalid SL/TP, fix the unreachable deploy floor, scale the horizon Three defects found by reading the 2026-08-01 training logs, all of which only became visible because the relabel made the numbers mean something. 1. A STALE ENUM TRAINED FOUR MODELS ON THE WRONG TARGET. `OnInit: trade settings snapshot - SL_Mode=1 TP_Mode=-101` -101 was TP_PREV_SWING, deleted from TAKE_PROFIT_MODE on 2026-07-31 in 7eb48f5. MetaTrader does not validate a saved enum input against the enum's current members, so charts saved before that kept the old integer. BarrierMultiples()'s `if(tpMult <= 0.0) tpMult = slMult;` then quietly turned it into a 1:1 barrier, and all four topologies trained ~250 eras against a strategy nobody selected - while the log reported "target 1.00*ATR" as though it were configured. Since the relabel these two inputs ARE the label definition, so this is not a bad trade setting, it is a wrong dataset. ValidateBarrier- Inputs() now refuses to start (INIT_FAILED + Alert + an explicit fix) on any value that is not an enum member. Members are enumerated rather than range-checked because both enums are sparse and carry negative sentinels, so no min/max test can tell a legal value from a deleted one - which is the entire failure mode. The fallback survives as belt-and-braces but now announces itself: a fallback that cannot say it fired is indistinguishable from correct behaviour. 2. THE DEPLOYABILITY FLOOR BECAME MATHEMATICALLY UNREACHABLE. `tradeableOK` required `dirPrecPct >= baseRatePct`, where baseRatePct is Buy+Sell as a share of all bars. At the old exact-pivot target that was ~6%, so "beat the base rate" read as "beat chance" and the test looked sound. Triple-barrier labels put it at ~83%, so the gate now demanded 83% directional precision - impossible by construction. Observed live: all four topologies cycling "PLATEAU stage 3 ... nothing safe to deploy" at a perfectly healthy 43-45% precision, with no checkpoint able to ship however good it got. Replaced with ZERO-SKILL precision, max(Buy,Sell)/allBars: exactly the score of the degenerate always-call-one-direction model this floor exists to reject. Correct at any base rate - ~43% on the current labels, ~3% on the old rare-pivot ones. The era line now prints "(chance N%, edge +Mpp)" beside the selection score, because 44% precision is excellent against a 3% chance level and worthless against a 43% one, and reading the first as the second is what made tonight's run look better than it was. 3. THE HORIZON IGNORED THE BARRIER GEOMETRY. ComputeBarrierHorizonBars() returned the median ZigZag leg, which measures how long a ~1 ATR move takes and says nothing about how long the CONFIGURED barrier needs. First-passage time out of [-m,+k] scales with m*k, so a 1:3 barrier takes ~3x as long as 1:1; the unscaled horizon would have timed out most 1:3 trades and pushed Neutral straight back up, re-creating the imbalance the relabel removes. Now multiplied by slMult*tpMult, calibrated against a real measurement rather than assumed: the accidental 1:1 run resolved at horizon 12 with only 16.7% timeouts, so the swing median is the right scale at m*k=1. Verifiable, not just asserted: the prebuild now counts barriers that ended on the VERTICAL barrier and reports them as a share of Neutral. Neutral conflates "timed out" with "stopped out" and only the first indicts the horizon. Both builds compile 0 errors / 0 warnings. Forces a retrain - correcting TP_Mode re-keys the fingerprint (|TB:1:-101 -> |TB:1:3), which is right: no existing model was trained on the intended target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 00:30:49 -04:00
//+------------------------------------------------------------------+
//| Verifies SL_Mode / TP_Mode actually hold members of their enums. |
//| See the call site in OnInit() for why this is a hard gate and |
//| not a clamp. Returns false (and explains itself) on any stale |
//| value. |
fix: refuse invalid SL/TP, fix the unreachable deploy floor, scale the horizon Three defects found by reading the 2026-08-01 training logs, all of which only became visible because the relabel made the numbers mean something. 1. A STALE ENUM TRAINED FOUR MODELS ON THE WRONG TARGET. `OnInit: trade settings snapshot - SL_Mode=1 TP_Mode=-101` -101 was TP_PREV_SWING, deleted from TAKE_PROFIT_MODE on 2026-07-31 in 7eb48f5. MetaTrader does not validate a saved enum input against the enum's current members, so charts saved before that kept the old integer. BarrierMultiples()'s `if(tpMult <= 0.0) tpMult = slMult;` then quietly turned it into a 1:1 barrier, and all four topologies trained ~250 eras against a strategy nobody selected - while the log reported "target 1.00*ATR" as though it were configured. Since the relabel these two inputs ARE the label definition, so this is not a bad trade setting, it is a wrong dataset. ValidateBarrier- Inputs() now refuses to start (INIT_FAILED + Alert + an explicit fix) on any value that is not an enum member. Members are enumerated rather than range-checked because both enums are sparse and carry negative sentinels, so no min/max test can tell a legal value from a deleted one - which is the entire failure mode. The fallback survives as belt-and-braces but now announces itself: a fallback that cannot say it fired is indistinguishable from correct behaviour. 2. THE DEPLOYABILITY FLOOR BECAME MATHEMATICALLY UNREACHABLE. `tradeableOK` required `dirPrecPct >= baseRatePct`, where baseRatePct is Buy+Sell as a share of all bars. At the old exact-pivot target that was ~6%, so "beat the base rate" read as "beat chance" and the test looked sound. Triple-barrier labels put it at ~83%, so the gate now demanded 83% directional precision - impossible by construction. Observed live: all four topologies cycling "PLATEAU stage 3 ... nothing safe to deploy" at a perfectly healthy 43-45% precision, with no checkpoint able to ship however good it got. Replaced with ZERO-SKILL precision, max(Buy,Sell)/allBars: exactly the score of the degenerate always-call-one-direction model this floor exists to reject. Correct at any base rate - ~43% on the current labels, ~3% on the old rare-pivot ones. The era line now prints "(chance N%, edge +Mpp)" beside the selection score, because 44% precision is excellent against a 3% chance level and worthless against a 43% one, and reading the first as the second is what made tonight's run look better than it was. 3. THE HORIZON IGNORED THE BARRIER GEOMETRY. ComputeBarrierHorizonBars() returned the median ZigZag leg, which measures how long a ~1 ATR move takes and says nothing about how long the CONFIGURED barrier needs. First-passage time out of [-m,+k] scales with m*k, so a 1:3 barrier takes ~3x as long as 1:1; the unscaled horizon would have timed out most 1:3 trades and pushed Neutral straight back up, re-creating the imbalance the relabel removes. Now multiplied by slMult*tpMult, calibrated against a real measurement rather than assumed: the accidental 1:1 run resolved at horizon 12 with only 16.7% timeouts, so the swing median is the right scale at m*k=1. Verifiable, not just asserted: the prebuild now counts barriers that ended on the VERTICAL barrier and reports them as a share of Neutral. Neutral conflates "timed out" with "stopped out" and only the first indicts the horizon. Both builds compile 0 errors / 0 warnings. Forces a retrain - correcting TP_Mode re-keys the fingerprint (|TB:1:-101 -> |TB:1:3), which is right: no existing model was trained on the intended target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 00:30:49 -04:00
//+------------------------------------------------------------------+
bool ValidateBarrierInputs()
{
int sl = (int)SL_Mode;
bool slOk = (sl == SL_INTELLIGENT || sl == SL_ATR_x1 || sl == SL_ATR_x2 || sl == SL_ATR_x3);
int tp = (int)TP_Mode;
bool tpOk = (tp == TP_INTELLIGENT || tp == TP_ATR_x1 || tp == TP_ATR_x2 || tp == TP_ATR_x3 ||
tp == TP_ATR_x4 || tp == TP_ATR_x6 || tp == TP_ATR_x8 || tp == TP_ATR_x10);
if(slOk && tpOk)
return true;
string bad = (!slOk ? "Stop-loss mode (" + IntegerToString(sl) + ")" : "") +
(!slOk && !tpOk ? " and " : "") +
(!tpOk ? "Take-profit mode (" + IntegerToString(tp) + ")" : "");
Print("Warrior EA: REFUSING TO START - " + bad + " is not one of the available options.");
Print("Warrior EA: this happens when a chart's saved settings were written by an older version of the "
"EA that offered an option which no longer exists. MetaTrader keeps the old value silently.");
Print("Warrior EA: FIX - open the EA's Inputs tab, re-pick Stop-loss mode and Take-profit mode from "
"the dropdowns (the shipped pair is 'ATR * 1 from entry (classic)' and 'ATR * 3 from entry "
"(classic)'), then press OK.");
Print("Warrior EA: these two inputs define the neural network's TRAINING TARGET, not just order "
"placement - running with a wrong value would train the model on a strategy you did not choose, "
"so the EA stops here instead of guessing.");
Alert("Warrior EA: " + bad + " is invalid - re-pick Stop-loss / Take-profit mode in the Inputs tab. See the Experts log.");
return false;
}
//+------------------------------------------------------------------+
//| Risk-limit inputs are now free-entry doubles rather than a preset |
//| dropdown, which is what makes them expressive enough for a real |
//| funded-account agreement - and also what makes a typo possible. |
//| These limits gate every trade the EA will ever place, so a wrong |
//| value here is not a suboptimal setting, it is an unprotected |
//| account. Same reasoning as ValidateBarrierInputs() above: refuse to |
//| start rather than substitute something plausible. |
//+------------------------------------------------------------------+
bool ValidateRiskInputs()
{
if(!EnableRiskGuard)
return true;
string bad = "";
if(MaxDailyLossPct < 0.0 || MaxDailyLossPct >= 100.0)
bad += "Daily loss limit (" + DoubleToString(MaxDailyLossPct, 2) + "%) must be >= 0 and < 100. ";
if(MaxDrawdownPct < 0.0 || MaxDrawdownPct >= 100.0)
bad += "Max total drawdown (" + DoubleToString(MaxDrawdownPct, 2) + "%) must be >= 0 and < 100. ";
if(RiskPerTradeOfBudget <= 0.0 || RiskPerTradeOfBudget > 100.0)
bad += "Max % of remaining budget per trade (" + DoubleToString(RiskPerTradeOfBudget, 2) +
") must be > 0 and <= 100. ";
if(RiskDayResetHour < 0 || RiskDayResetHour > 23)
bad += "Risk day reset hour (" + IntegerToString(RiskDayResetHour) + ") must be 0-23. ";
if(bad != "")
{
Print("Warrior EA: REFUSING TO START - " + bad);
Print("Warrior EA: fix the Risk Guard section of the Inputs tab. Enter the limits from your account "
"agreement as plain percentages (e.g. 4 and 8), or 0 to disable a rule.");
Alert("Warrior EA: Risk Guard inputs are invalid - see the Experts log.");
return false;
}
//--- Not fatal, but always wrong in practice: a daily allowance at or above the total allowance means
//--- the daily rule can never trip first, so the first thing it ever protects is nothing.
if(MaxDailyLossPct > 0.0 && MaxDrawdownPct > 0.0 && MaxDailyLossPct >= MaxDrawdownPct)
Print("Warrior EA: WARNING - daily loss limit (", DoubleToString(MaxDailyLossPct, 2),
"%) is not tighter than the max drawdown limit (", DoubleToString(MaxDrawdownPct, 2),
"%). One full daily loss would end the account, so the daily rule protects nothing.");
if(!RiskGuardFlatten)
Print("Warrior EA: NOTE - 'Close own positions on breach' is OFF. The risk limits will decline new "
"entries and shrink position sizing, but an ALREADY-OPEN position can still run through the "
"limit - which is how a hard daily loss rule is usually breached. Turn it on for a funded "
"account where a breach ends the account.");
return true;
}
//+------------------------------------------------------------------+
//| Apply the configuration shared by every AI architecture. |
//+------------------------------------------------------------------+
void ConfigureAISignal(CExpertSignalAIBase *aiSignal)
{
if(CheckPointer(aiSignal) == POINTER_INVALID)
return;
aiSignal.OutputNeuronsCount(OutputNeuronsCount);
//--- HistoryBars is no longer seeded here: the window is DERIVED at InitNeuralNetwork (fresh
//--- model) or ADOPTED from the .cfg (existing model) - see DeriveHistoryBars.
aiSignal.MinDirectionalRecall(MinRecall);
aiSignal.LogitAdjustTau(LogitAdjustTau / 100.0);
aiSignal.SignalClusterWindow(SignalClusterWindow);
aiSignal.FreezePriorCalibration(FreezePriorCalibration);
aiSignal.SwingConfirmationBars(SwingConfirmationBars);
//--- SL/TP reach the AI signals because they now define the TRAINING TARGET, not just the order.
//--- InitializeSignal() sets them on the master signal only; the AI filters are separate objects.
aiSignal.SLMode((int)SL_Mode);
aiSignal.TPMode((int)TP_Mode);
//--- The training target is no longer selectable - direction models are unconditionally triple-
//--- barrier (see the withdrawn TrainingTarget input in Variables\Inputs.mqh for why, and for the
//--- three lines that bring the fractal option back).
feat(ensemble): per-NN inputs replace the preset selector - the meta head becomes the vote's gate User design (2026-08-19): 'remove the enum menu that selects neural networks... individual inputs for every NN just like classic signals... the META NN should be integrated into the voting decision pipeline when enabled... as a bonus meta labelling is applied to enabled NNs.' - AI_CHOICE is GONE (tombstoned per the stale-.set doctrine). Use_MLP/Use_CONV/Use_LSTM/ Use_CONVLSTM are ordinary bools like the classic votes; the ensemble arithmetic adapts to any subset because the consensus divisor is the enabled capable weight. Two or more enabled = ensemble (|ENS1 token + joint gate, exactly the old AI_HYBRID fingerprints, so existing weight files keep loading); one = the old solo preset; none = classic-only. - Use_MetaLabeling un-couples META from the direction NNs (the old selector made them mutually exclusive). S3 ships: CSignalMETA::LiveMetaGate scores each vote-cleared entry (shared window at bar 1 + proposal descriptor: side, net vote, live geometry, spread/ATR; pattern one-hot ZEROED - ranking, not calibrated probability, documented in the body) and vetoes below the cost-adjusted break-even. Entries only; fail-open everywhere, loudly. - COEXISTENCE HAZARDS closed: VoteCapableWeight()=0 and ProspectiveVote()=false for the meta target - solo-only until today, a trained META would otherwise sit in the consensus divisor as a permanent abstainer and shrink every vote by its module weight. - CERTIFIED == TRADED: the ensemble era verdict replays the identical veto through the same g_warriorMetaGate pointer over its OOS fired bars (bar re-resolved from the row's own time; fail-open counted as fires and reported: 'metaGate: N approved, M vetoed, K unscored'). The overlay deliberately does NOT replay it (veto-filter-in-replay class, calendar-cliff precedent) - documented at the sweep site. Solo charts' own gate does not model the veto - the standing solo-gate caveat, documented at the input. - DB continuity: the pattern/journal DB fingerprint's first slot was (int)AIType; DbLegacyAiSlot() maps every legacy-expressible config to its OLD value (new 2-3 member subsets get 100+bitmask, outside the legacy range) so no existing database re-keys. filterID becomes the enabled roster via one EnabledNNSummary(). - HUD: the meta line shows the gate (armed/(trn), last P vs BE, ok/veto tally); the armed/disarmed announcement fires on state change via one latch (MetaGateArmedNow), not only when an entry happens to be proposed. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:01:02 -04:00
if(!aiSignal.IsMetaTarget()
&& (EnablePAI ? 1 : 0) + (EnableCONV ? 1 : 0) + (EnableLSTM ? 1 : 0) + (EnableHYBRID ? 1 : 0) >= 2)
fix(ensemble): responsive panel + synchronized eras + combined-vote accuracy Four user-reported/requested items, one root cause chain: 1) DEAD CONTROL PANEL in AI_HYBRID mode. All members posted custom event id 1 and handled id 1001, and CExpertCustom broadcasts every chart event to every filter - so each posted event ran a train chunk in ALL N members (N*N chunks per round) and the chart thread never idled long enough to deliver clicks/drags. profiling.csv: 99.45% of time in OnChartEventHandler. Fix: per-instance study-event ids (STUDY_EVENT_ID_BASE + construction order, offset above the Controls library's ON_* codes - id 1 was also ON_DBL_CLICK, so panel double-clicks fired training chunks). ArmStudyEvent() is the single post site; lost-event watchdog replaces the accidental sibling-clears-my-flag rescue. 2) WARM-UP DUPLICATION. The auto-tune sweep is deterministic over identical features/labels, and it ends in the full MI diagnostic suite, which the MI-share gate never intercepted on the sweep path - four members ran four identical ~36s sweep+report blocks. First member publishes outcome (g_ensembleChartTuneDone/Installed/Settings); the rest apply it and skip both. 3) DEINIT STRANDED PANEL+ARROWS (user repro 18:52). Root cause from the log: the 4,500ms budget runs from MetaTrader's stop REQUEST - a heavy autosave in flight ate it, OnDeinit got ~430ms and died in the first member's arrow persist ("Abnormal termination" 432ms in). Fix: early visible-UI sweep (native prefix deletes for status/panel/dialog) right after ClearStatusLabel, and a fast path for still-training models - their arrows are re-rendered every era, so they get one bulk purge instead of scan+atomic-write in the death window. 4) ENSEMBLE FEATURES (user requests): era BARRIER - members advance era by era together; a member ahead of the slowest still-training member declines Train() calls and its chunk budget is donated (TRAIN_TIME_BUDGET_MS = 120/activeTrainers, UI headroom constant). COMBINED-VOTE OOS SCORE - each member's pass-3 scan contributes its adjusted per-bar decision (0.0 on abstain) to a shared row buffer; the last member to finish the era scores the averaged vote vs the mirrored Min_Vote_Open against the same target-before-stop outcomes members grade themselves on, publishing an "Ensemble vote" line on the aggregated panel. Member headlines now carry their lifetime win rate with break-even. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:06:04 -04:00
//--- second argument mirrors the open threshold into the combined-vote OOS scorer so the
//--- ensemble panel's "Ensemble vote" line fires on the same criterion the live trade does
refactor(stdlib): the vote thresholds are ints on the library's scale, not "confidence %" The MECHANISM was already stdlib and is untouched: ThresholdOpen() -> m_threshold_open, tested as `m_direction >= m_threshold_open` exactly as CExpertSignal does it. What was wrong was the presentation. Both inputs were preset ENUMS labelled "Min confidence to open/close (%)", which names the wrong quantity - m_direction is a WEIGHTED MEAN OF PATTERN WEIGHTS, not a probability, and nothing in this path is a confidence. They are now plain ints named the way the MQL5 wizard names them: input int Signal_ThresholdOpen = 25; // [0...100] input int Signal_ThresholdClose = 101; // [0...100, 101 = never] Values are exactly what shipped, so behaviour is unchanged. 101 rather than the library's default of 100 for close: a weighted mean of pattern weights cannot REACH 101, which is how the shipped config disables the vote exit, and quietly lowering it to 100 would re-arm a live exit route as a side effect of a naming change. VOTE_CLOSE_PRESETS is deleted (its only user is gone). PERCENTAGE_PRESETS stays - MinRecall genuinely is a percentage. ** ACTION NEEDED ON DEPLOYED CHARTS: the inputs are RENAMED, so saved .set files no longer match and charts fall back to the defaults above. Those defaults are the current shipped values, so a chart on 25/Disabled needs nothing; a tuned one does. Comment cleanup in the same pass, and this part was not cosmetic - three blocks documented mechanisms that no longer exist: - the AI early-exit route (deleted in 38a12a2) described as live and still firing every bar; - the m_lastNonNeutralSignal alternation gate (removed 2026-08-01) described as consuming the AI's vote; - 16 lines of VOTE_CLOSE_PRESETS documentation orphaned by that enum's deletion, ending with "see that enum's note directly above" pointing at nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 08:39:01 -04:00
aiSignal.EnsembleMember(true, (double)Signal_ThresholdOpen);
//--- THE EXIT POLICY, pushed into the AI signal so the GATE simulates what the live order path
//--- does.
revert(labels): drop the one-sided exit target; measure the calibration drift instead Reverts a863796 on the operator's call - "unnecessary complexity". It was right about the mechanism and wrong about the priority: it re-cut the classes for a case the measured verdict never reaches (SP500 H4 reads "both sides" at the derived geometry), while the drift that IS happening affects every chart and every era. Recoverable from a863796 if a one-sided book ever becomes real. Two pieces of it survive, both independent of the exit idea: The drift verdict keeps reading m_winLongCache/m_winShortCache rather than the collapsed label pair. That line reports always-long vs always-short win rates, which is what the win caches hold - each side scored on its own barriers, published before the collapse. The label pair carries only the side touched first, so it undercounted long wins by the both-won-goes-to-short share. There are zero both-won bars at any geometry with target >= stop, so this changes no number today; it changes the wrong number to the right one. And the .cfg gains nothing and loses nothing: the two appended ints go away again, and they were the last fields, so a .cfg written by yesterday's build still reads correctly - the loader simply stops before them. WHAT THE REVERT MAKES ROOM FOR. The operator's actual requirement is that the model reproduce the label distribution the scan measured, and nothing in the pipeline ties it to that. The loss trains on a rebalanced sample and the abstain rate is owned by a margin threshold fitted on EDGE, so the call rate and the label prior can drift arbitrarily far apart - and did, invisibly: at era 1350 the models call Buy on 20-28% and Sell on 22-32% of bars against a scan-measured 2.1% and 4.8%. Roughly a 10x over-call, and not one line in the journal said so. The era line now carries it: CALIBRATION calls vs true rate Buy 28% vs 2% (14.0x) Sell 32% vs 5% (6.4x) Neutral 40% vs 93% (0.4x) Reported as a ratio because that is the readable number - 1.0x is calibrated. This is deliberately a measurement and not yet a correction: matching the label rate would put coverage near 7%, below the ensemble gate's own 12.4% coverage floor, so calibration and the gate are in direct conflict and which one yields is the operator's call, not mine. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 10:14:26 -04:00
aiSignal.ExitPolicy(0.0, true);
aiSignal.EnableOnlineLearning(EnableOnlineLearning);
aiSignal.MaxErasPerRun(MaxErasPerRun);
aiSignal.OOSSplit(OOSSplit);
if(!UseDatabaseRanking)
aiSignal.Weight(1);
aiSignal.UseVolumes(EnableVolume);
aiSignal.UseTime(EnableTime);
aiSignal.UseATR(EnableATR);
aiSignal.UseMA(EnableMAFeature);
aiSignal.UseRSI(EnableRSIFeature);
aiSignal.UseMACD(EnableMACDFeature);
aiSignal.UseIchimoku(EnableIchimokuFeature);
aiSignal.UseSwingContext(EnableSwingContext);
aiSignal.UseNews(EnableNews);
aiSignal.NewsFeatureWindowMinutes(NewsFeatureWindowMinutes);
fix(signals): revive a dead MA model, and demote Sanyaku from state to event Two defects surfaced by research/test_classic.py, both verified fixed by re-running the transcription against 178k bars of EURUSD H1. CSignalMA model 1 could never fire. For any recursive average - and MA_TYPE_EMA is the shipped default - MA(i) = a*Close(i) + (1-a)*MA(i+1), so DiffMA(i) = a * (Close(i) - MA(i+1)) DiffCloseMA(i) = (1-a) * (Close(i) - MA(i+1)) are positive multiples of one quantity and always share a sign. Model 1 asks for a close BELOW a RISING average, which is precisely the combination that identity forbids: 0.000% of bars, either direction, any symbol. The MQL5 standard library this was ported from defaults to MODE_SMA, where the two are merely correlated - the bug arrived with the EMA default, not with the port. Reading the slope one bar back (DiffMAPrev) breaks the tie for every MA type while keeping the model's stated meaning. Now fires on 7.92% of bars. CSignalIchimoku model 11 fired on 27% of bars at weight 100. Sanyaku is three standing STATES conjoined with no transition term, so it held across long stretches - and being last in the if-chain at the top weight, the module's highest-conviction reading was also its most common one, overwriting all eight event models below it on a quarter of all bars. The old comment rejected an event form because "demanding all three flip on the same bar would fire almost never" - true, but that is not the alternative. Kouten is the TURN: the ALIGNMENT transitions, and only one role need change for it to. Testing !Sanyaku(idx+1) fires once per aligned stretch. Now 2.17%, in line with Kumo breakout (2.4%) and the strong TK cross (1.1%). DataReady() extended one bar deeper to cover the lookback. Neither pattern showed edge before or after; this is about the models meaning what they say and the vote not being dominated by a constant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:14:34 -04:00
aiSignal.UseCrossAsset(EnableCrossAsset);
feat(ai): spread as a volatility-regime feature, and fix a stale-index cache in both new blocks Adds spread/ATR and the spread change ratio as network inputs (EnableSpreadFeature, default on). Spread is the one microstructure channel that is both FX-available and genuinely historical in the Strategy Tester - "during testing, the spread is not modeled but is taken from historical data" - so unlike swap, signed tick flow or depth of market it is something a backtest can honestly validate. What it encodes, stated precisely because the raw measurement overstates it. research/test_spread.py found spr/atr the strongest single feature in this codebase, on 5 of 8 instrument/geometry cells at 2-4x any volume feature. But the barrier LABEL charges the spread inside its own barriers, so a wide-spread bar is mechanically likelier to resolve as a loss and the feature would partly be predicting its own cost model. Relabelling at zero cost and re-measuring the identical feature showed 20-40% of it WAS that tautology and the majority was not (XAUUSD retained 97%). What survives is a volatility-regime reading: spread is near-fixed while ATR is not, so the ratio runs high exactly when realised volatility is below its own ATR estimate, which genuinely predicts whether ATR-scaled barriers get reached. It is UNSIGNED - Neutral-vs-directional only, never a side. Also fixes a stale-index bug I introduced with the cross-asset panel and had just repeated in the spread series. Both cached on length alone: if(m_crossAsset.Bars() >= bars) return true; MQL5 series indices are relative to NOW, so one new closed candle shifts every index by one. Keyed only on length, the panel keeps serving its index 0 as a bar that is no longer the newest, and every cross-asset value is read one bar out of step with the price features sitting beside it in the same vector - silently, with no error and no shape change. This is the same class of defect as the dtStudied watermark behind the zero-direction backtests. Both now carry a datetime anchor on m_Time.GetData(0), the same invalidation key the label/feature bar caches already use. And a performance fix that fell out of it: with correct invalidation the panel rebuilds on every new bar, and RefreshConvergedSignal runs per bar - which in the tester would mean one full multi-symbol resample per simulated bar at training depth. Inference only reads bars 0..m_historyBars-1 plus the panel's own slow window, so it now requests exactly that. The cache check is >=, so a deeper panel left from training still satisfies it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:42:40 -04:00
aiSignal.UseSpreadFeature(EnableSpreadFeature);
aiSignal.UseAltData(EnableAltData);
aiSignal.UseADCumulativeDelta(EnableADCumulativeDelta);
aiSignal.UseADShorteningOfThrust(EnableADShorteningOfThrust);
aiSignal.UseADWyckoffEventStream(EnableADWyckoffEventStream);
aiSignal.UseADWyckoffFailedStructure(EnableADWyckoffFailedStructure);
aiSignal.UseADWyckoffSignificantBarInversion(EnableADWyckoffSignificantBarInversion);
aiSignal.AutoTuneIndicators(AutoTuneIndicators);
}
//+------------------------------------------------------------------+
// Helper function to retry signal creation with error handling
template <typename TSignal>
TSignal* CreateSignalWithRetry(int maxRetries, bool enableFlag)
{
if(!enableFlag)
return NULL;
TSignal* signal = NULL;
for(int tries = 0; tries < maxRetries; ++tries)
{
signal = new TSignal;
if(signal == NULL)
{
Print("Initialization of signal failed, retrying...");
RandomSleep();
}
else
break;
}
if(signal == NULL)
{
Print("Failed to create and initialize signal after retries");
}
return signal;
}
//+------------------------------------------------------------------+
//| ONE RETRY-AND-REPORT for the init steps that can lose a race |
//| with the terminal (indicator handles, timer registration, the |
//| trade objects' own setup). |
//+------------------------------------------------------------------+
typedef bool (*TInitStep)(void);
bool RetryInitStep(TInitStep step, const string what, const int maxRetries, const string caller)
{
for(int tries = 0; tries < maxRetries; ++tries)
{
if(step())
return true;
//--- A permanent refusal gives the same answer five times, and the retries push the one line
//--- that explains it off the top of the operator's log. Stop and repeat the reason instead.
if(g_initFatalReason != "")
{
Print(caller + ": Failed to " + what + " - " + g_initFatalReason +
". Retrying cannot change this; see the REFUSED line above.");
return false;
}
Print(caller + ": Failed to " + what + ", retrying...");
RandomSleep();
}
Print(caller + ": Failed to " + what + " after retries");
return false;
}
//--- The four steps above, each as the no-argument call RetryInitStep takes. Thin by necessity:
//--- MQL5 function pointers cannot bind a method call or an argument, and these are two of each.
bool StepInitTrailing(void) { return InitializeTrailing(); }
bool StepInitMoneyManagement(void) { return InitializeMoneyManagement(); }
bool StepValidateSettings(void) { return Expert.ValidationSettings(); }
bool StepInitIndicators(void) { return Expert.InitIndicators(); }
//--- 500ms: Train() does at most TRAIN_TIME_BUDGET_MS (120ms solo, 30ms per ensemble member) of
//--- work per call, then yields back here. EventSetMillisecondTimer is needed for sub-second
//--- resolution; EventSetTimer takes whole seconds only.
#define WARRIOR_TIMER_INTERVAL_MS 500
bool StepSetTimer(void) { return EventSetMillisecondTimer(WARRIOR_TIMER_INTERVAL_MS); }
//+------------------------------------------------------------------+
//| IMPORTANT: no failure branch below (nor in any helper it calls - |
//| AddFilterToSignal(), InitializeSignal(), InitializeTrailing(), |
//| InitializeMoneyManagement()) may call Expert.Deinit() before |
//| returning INIT_FAILED/false. |
//+------------------------------------------------------------------+
int OnInit()
{
//--- FIRST: adopt the chart's tuned indicator periods (if a gated auto-tune install ever wrote them).
//--- Must precede ComputeDbConfigFingerprint() and the classic-signal configuration below, both of
//--- which consume the g_Tuned* values - see Variables\TunedPeriods.mqh for the whole contract.
LoadTunedPeriods();
//--- clears out whatever status label text was left over from before this OnInit() ran (stale text
//--- from a prior "warm" re-init - e.g. an input-parameter change, which reuses this same running
//--- instance rather than a fresh one - would otherwise sit unchanged and look like nothing is
//--- happening) so it's obvious the moment training/signal init actually resumes producing new status text
fix: purge every EA object namespace on init and after deinit teardown Leftover objects survived deinit because the cleanup list had drifted. PurgeChart()'s own comment said it removed "our namespaced signal arrows plus the status-label objects" while the code removed arrows ONLY, and the panel prefix was swept at OnInit and nowhere else - so an ordinary deinit left the status line, and any panel straggler, on the chart. Three scattered call sites and a comment cannot be kept in step. There is now ONE list - WarriorChartPrefixes() - covering arrows, status label and panel, and one sweep, WarriorPurgeChartObjects(), used by every path. Add a prefix there when a new object family appears and every cleanup picks it up. Two call sites added: OnInit, before ANYTHING is drawn (including the status label it would otherwise delete). Chart objects live in the chart PROFILE, not in the EA, so they outlive the process: a deinit force-terminated at MetaTrader's ~4,500 ms budget, a crash, a terminal kill, or an .ex5 replaced while attached all strand objects no later deinit will ever own - and deleting the EA's files does not remove them, which is why they read as corruption. Arrows are included: LoadChartSignals restores them from their sidecar moments later and already opens with its own arrow sweep, so this only removes orphans the sidecar does not account for - the ones SaveChartSignals would otherwise ADOPT, since it rebuilds that sidecar by scanning the chart. OnDeinit, after ExtPanel.Destroy. Destroy walks an unbounded control tree and ClearStatusLabel clears text rather than guaranteeing object removal; either can leave a straggler and nothing looked afterwards. Bounded work - three prefix deletes and one object-list scan - so it respects the ordering rule that keeps the cheap visible cleanup ahead of the heavy save. Arrows excluded: ShutdownChartCleanup already persisted and removed them and re-deleting would race that write. The two are complementary: the deinit sweep closes the ordinary case, the OnInit purge closes the case where MetaTrader never let us finish. Only the second can help after a starved shutdown. Both sweeps rescan by name across EVERY object type and delete what the bulk call missed. ObjectsDeleteAll's return has already been observed disagreeing with a by-name scan of the same chart microseconds apart, and object commands are queued on the chart rather than applied inline, so a returned count is not evidence the objects are gone. Panel create site now uses WARRIOR_PANEL_PREFIX instead of a literal, so the name cannot drift away from the list that cleans it up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:19:26 -04:00
//--- START FROM A GUARANTEED-CLEAN CHART. Before anything is drawn, sweep every object namespace this
//--- EA owns (WarriorChartPrefixes). Chart objects live in the chart PROFILE, not in the EA, so they
//--- outlive the process: a deinit that ran out of MetaTrader's ~4,500 ms budget, a crash, a terminal
//--- kill, or an .ex5 replaced while attached all leave objects behind that no later deinit will ever
//--- own. Deleting the EA's files does not remove them either, which is why they read as corruption.
//--- Arrows are INCLUDED in this sweep: LoadChartSignals restores them from their sidecar moments later
//--- and already opens with its own arrow sweep, so purging here costs nothing and removes any orphan
//--- that the sidecar does not account for - the ones that would otherwise be adopted by the next model
//--- to attach, because SaveChartSignals rebuilds that sidecar by SCANNING the chart.
//--- Runs BEFORE SetStatusLabel below, or it would delete the label it just created.
int initLeftover = 0;
int initPurged = WarriorPurgeChartObjects(0, false, initLeftover);
if(initPurged > 0)
PrintFormat("%s: chart purge on init - removed %d leftover EA object(s)%s. Chart objects survive a"
" starved deinit, a crash and an .ex5 swap, so a clean start is asserted here rather"
" than assumed from the last shutdown.", __FUNCTION__, initPurged,
(initLeftover > 0
? StringFormat(" (%d of them needed a by-name delete after the bulk call)", initLeftover)
: ""));
//--- AND SAY WHAT SURVIVED IT. Names, not just a count - a count cannot be acted on. Reporting is
//--- the whole intervention. "Removed N, zero by-name leftovers" only ever meant "nothing matching
//--- OUR PREFIXES remains" - it was never a statement about the chart, and on 2026-08-17 22:00 all
//--- three charts printed exactly that and still came up with duplicated panels.
fix(chart): a purge that reports "zero leftovers" was only ever checking its own list 2026-08-17 21:58: all three charts hit "Abnormal termination" ~5.3 s into OnDeinit with NO cleanup-timings line - the teardown was starved again. The 22:00 init purge then removed 993 / 1373 / 1557 stranded objects and reported ZERO by-name leftovers on every chart, and the charts still came up with duplicated panels. "Nothing matching our prefixes remains" and "the chart is clean" are different statements and only the first was being made. Three changes, in the order they matter: 1. WHY the teardown starved, and it is a gap in ad80e0b. StartLabelCachePrebuild runs ResizeBuffers + RefreshData over the FULL study window (33,984 bars on XAUUSD), unchunked, and OnDeinit cannot begin until it returns. Normally a once-per-run cost. That night SP500 and XAUUSD LSTM were wedged in the "cache invalidated at era start" loop, which calls it on EVERY Train() call - two members re-preparing tens of thousands of bars indefinitely. The terminal closed into that. Guarded now, plus a resumable guard in the prebuild chunk loop (the tally pass after it is not chunked). 2. Catch-all "Warrior" prefix in WarriorChartPrefixes. Every family this EA creates is named Warrior* except the arrows (WarSig_), so one bare prefix covers the three named entries AND anything a rename or a stale .ex5 left under a name nobody remembers. Still a prefix delete, never ObjectsDeleteAll(chart) - the user's own drawings are not ours to remove. Does not defeat skipArrows: "WarSig_" does not start with "Warrior". 3. The init purge now REPORTS the residue it did not claim, by name (up to 12). Not deleted - an unmatched object may belong to the user or another indicator. If a Warrior panel is visible and appears in neither the removed count nor this list, the prefix list has drifted a third time and the name is in the journal instead of being inferred from a screenshot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:07:09 -04:00
{
string resPrefixes[];
int np = WarriorChartPrefixes(resPrefixes);
int resTotal = ObjectsTotal(0, -1, -1);
string residue = "";
int unmatched = 0;
for(int i = 0; i < resTotal; i++)
{
string nm = ObjectName(0, i, -1, -1);
bool ours = false;
for(int q = 0; q < np; q++)
if(StringFind(nm, resPrefixes[q]) == 0)
{
ours = true;
break;
}
if(ours)
continue;
unmatched++;
if(unmatched <= 12)
residue += (residue == "" ? "" : ", ") + nm;
}
if(unmatched > 0)
PrintFormat("%s: chart residue after the init purge - %d object(s) this EA did not create and did"
" not touch: %s%s. If a Warrior panel or status line is VISIBLE on the chart and is"
" not in this list and was not removed above, the prefix list has drifted again"
" (see WarriorChartPrefixes).", __FUNCTION__, unmatched, residue,
(unmatched > 12 ? StringFormat(" ... and %d more", unmatched - 12) : ""));
}
SetStatusLabel("Warrior EA: initializing...");
//--- WHICH BINARY IS ACTUALLY RUNNING. Read it FIRST when a fix appears not to have taken.
fix(reset): say what the reset actually did, per member and per file The user reports "Delete & Reset Weights only wipes the first NN". I could not find a code path that skips ensemble members, and I am not going to assert one: the handler loops g_aiSignals[0..g_aiSignalCount), all four topologies register unconditionally in OnInit, and SetIdentity gives each its own State\<id>\ folder so the six deleted paths are genuinely distinct per member. What IS true is that the whole success path was SILENT - six FileDelete calls per member printing only on failure, and one chart-wide Alert - so a four-member reset and a one-member reset produce byte-identical output. The symptom could be neither confirmed nor refuted from a log. That is the defect I can fix today. - COMPILED <timestamp> (__DATETIME__) beside the build tag. The hand-edited tag had sat at scan-nofwd-v5 across a week of commits, so it could not answer the question it exists for. The compile stamp cannot be forgotten. Tag bumped to reset-census-v6. - RegistryLine() (public): ID, active file path, common/local, era, deployed vs training, ensemble index. The reset handler prints a numbered census of the whole registry BEFORE the confirm dialog. If that says 1 on an AI_HYBRID chart the fault is registration, not the reset - and RegisterAISignal already has a loud MAX_AI_SIGNALS message for exactly that. - The confirmation dialog now names the count, so a wrong registry is visible before anything is deleted rather than after. - ResetWeights prints one line per member: N deleted / N already absent / N FAILED, plus a per-suffix breakdown. "absent" on a member that should have had a .nnw is a completely different fault from "deleted"; they were identical. - ResetWeights' return value was discarded. A member whose BuildFreshTopology fails has had its files deleted and has no network - and the Alert still said "weights reset". Counted now, with an INCOMPLETE alert when they disagree. - Same for dbm.ResetDatabase(), whose bool was also dropped. The DB is one shared file for every signal on the chart, so there is nothing per-member to loop - the log now says that explicitly, since it is the question being asked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:57:26 -04:00
PrintFormat("%s: build tag %s | COMPILED %s", __FUNCTION__, WARRIOR_BUILD_TAG,
TimeToString(__DATETIME__, TIME_DATE | TIME_MINUTES));
feat(ensemble): per-NN inputs replace the preset selector - the meta head becomes the vote's gate User design (2026-08-19): 'remove the enum menu that selects neural networks... individual inputs for every NN just like classic signals... the META NN should be integrated into the voting decision pipeline when enabled... as a bonus meta labelling is applied to enabled NNs.' - AI_CHOICE is GONE (tombstoned per the stale-.set doctrine). Use_MLP/Use_CONV/Use_LSTM/ Use_CONVLSTM are ordinary bools like the classic votes; the ensemble arithmetic adapts to any subset because the consensus divisor is the enabled capable weight. Two or more enabled = ensemble (|ENS1 token + joint gate, exactly the old AI_HYBRID fingerprints, so existing weight files keep loading); one = the old solo preset; none = classic-only. - Use_MetaLabeling un-couples META from the direction NNs (the old selector made them mutually exclusive). S3 ships: CSignalMETA::LiveMetaGate scores each vote-cleared entry (shared window at bar 1 + proposal descriptor: side, net vote, live geometry, spread/ATR; pattern one-hot ZEROED - ranking, not calibrated probability, documented in the body) and vetoes below the cost-adjusted break-even. Entries only; fail-open everywhere, loudly. - COEXISTENCE HAZARDS closed: VoteCapableWeight()=0 and ProspectiveVote()=false for the meta target - solo-only until today, a trained META would otherwise sit in the consensus divisor as a permanent abstainer and shrink every vote by its module weight. - CERTIFIED == TRADED: the ensemble era verdict replays the identical veto through the same g_warriorMetaGate pointer over its OOS fired bars (bar re-resolved from the row's own time; fail-open counted as fires and reported: 'metaGate: N approved, M vetoed, K unscored'). The overlay deliberately does NOT replay it (veto-filter-in-replay class, calendar-cliff precedent) - documented at the sweep site. Solo charts' own gate does not model the veto - the standing solo-gate caveat, documented at the input. - DB continuity: the pattern/journal DB fingerprint's first slot was (int)AIType; DbLegacyAiSlot() maps every legacy-expressible config to its OLD value (new 2-3 member subsets get 100+bitmask, outside the legacy range) so no existing database re-keys. filterID becomes the enabled roster via one EnabledNNSummary(). - HUD: the meta line shows the gate (armed/(trn), last P vs BE, ok/veto tally); the armed/disarmed announcement fires on state change via one latch (MetaGateArmedNow), not only when an entry happens to be proposed. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:01:02 -04:00
PrintFormat("%s: trade settings snapshot - NNs=%s Entry_Multiplier=%d SL_Mode=%d TP_Mode=%d TrailingStrategy=%d MM_STRATEGY=%d",
__FUNCTION__, EnabledNNSummary(), (int)Entry_Multiplier, (int)SL_Mode, (int)TP_Mode,
feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement The barrier geometry is derived from the instrument's own excursion distribution (stop at q75 of adverse travel, target at q50 of favourable), and then a 1:2 floor was applied on top, raising the target to twice whatever the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR, reached on 3.3% of bars inside the horizon - so the label became "almost never a win" and every topology was trained to predict an event that essentially does not occur. A measured target has to stay measured. The ratio never bought what it was believed to buy. A reward:risk floor does not create expectancy; it trades hit rate against payoff at a break-even the geometry already fixes - which this project has separately MEASURED (payoff 0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four consecutive Market validation rejections for "no trading operations" when it rejected 100% of setups, and the label corruption above. Removed: - the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a live enum with no input behind it is the shape of the stale-.set incident that trained ~250 eras on the wrong target) - the forced target raise in the label geometry - the rrOK eligibility gate in the barrier-geometry scan, so every unclamped pairing now competes on the measurement alone. Clamping stays disqualifying for its own unrelated reason. - the reward < minRR*risk veto in OpenParams Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing in MoneyIntelligent - the ratio as a SIZING input was always the sound use. Risk stays bounded where it actually is - account risk % and CRiskBudget. The low-reachability warning survives but is re-aimed: with nothing inflating the target, a target the market rarely reaches can only mean the horizon is truncating the excursions the geometry is derived from. Both build variants compile 0 errors / 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
(int)TrailingStrategy, (int)MM_STRATEGY);
//--- HARD GATE on the two inputs that define the training target. This is not hypothetical. Hours
//--- of training measuring a strategy nobody chose, with nothing in the log saying so.
fix: refuse invalid SL/TP, fix the unreachable deploy floor, scale the horizon Three defects found by reading the 2026-08-01 training logs, all of which only became visible because the relabel made the numbers mean something. 1. A STALE ENUM TRAINED FOUR MODELS ON THE WRONG TARGET. `OnInit: trade settings snapshot - SL_Mode=1 TP_Mode=-101` -101 was TP_PREV_SWING, deleted from TAKE_PROFIT_MODE on 2026-07-31 in 7eb48f5. MetaTrader does not validate a saved enum input against the enum's current members, so charts saved before that kept the old integer. BarrierMultiples()'s `if(tpMult <= 0.0) tpMult = slMult;` then quietly turned it into a 1:1 barrier, and all four topologies trained ~250 eras against a strategy nobody selected - while the log reported "target 1.00*ATR" as though it were configured. Since the relabel these two inputs ARE the label definition, so this is not a bad trade setting, it is a wrong dataset. ValidateBarrier- Inputs() now refuses to start (INIT_FAILED + Alert + an explicit fix) on any value that is not an enum member. Members are enumerated rather than range-checked because both enums are sparse and carry negative sentinels, so no min/max test can tell a legal value from a deleted one - which is the entire failure mode. The fallback survives as belt-and-braces but now announces itself: a fallback that cannot say it fired is indistinguishable from correct behaviour. 2. THE DEPLOYABILITY FLOOR BECAME MATHEMATICALLY UNREACHABLE. `tradeableOK` required `dirPrecPct >= baseRatePct`, where baseRatePct is Buy+Sell as a share of all bars. At the old exact-pivot target that was ~6%, so "beat the base rate" read as "beat chance" and the test looked sound. Triple-barrier labels put it at ~83%, so the gate now demanded 83% directional precision - impossible by construction. Observed live: all four topologies cycling "PLATEAU stage 3 ... nothing safe to deploy" at a perfectly healthy 43-45% precision, with no checkpoint able to ship however good it got. Replaced with ZERO-SKILL precision, max(Buy,Sell)/allBars: exactly the score of the degenerate always-call-one-direction model this floor exists to reject. Correct at any base rate - ~43% on the current labels, ~3% on the old rare-pivot ones. The era line now prints "(chance N%, edge +Mpp)" beside the selection score, because 44% precision is excellent against a 3% chance level and worthless against a 43% one, and reading the first as the second is what made tonight's run look better than it was. 3. THE HORIZON IGNORED THE BARRIER GEOMETRY. ComputeBarrierHorizonBars() returned the median ZigZag leg, which measures how long a ~1 ATR move takes and says nothing about how long the CONFIGURED barrier needs. First-passage time out of [-m,+k] scales with m*k, so a 1:3 barrier takes ~3x as long as 1:1; the unscaled horizon would have timed out most 1:3 trades and pushed Neutral straight back up, re-creating the imbalance the relabel removes. Now multiplied by slMult*tpMult, calibrated against a real measurement rather than assumed: the accidental 1:1 run resolved at horizon 12 with only 16.7% timeouts, so the swing median is the right scale at m*k=1. Verifiable, not just asserted: the prebuild now counts barriers that ended on the VERTICAL barrier and reports them as a share of Neutral. Neutral conflates "timed out" with "stopped out" and only the first indicts the horizon. Both builds compile 0 errors / 0 warnings. Forces a retrain - correcting TP_Mode re-keys the fingerprint (|TB:1:-101 -> |TB:1:3), which is right: no existing model was trained on the intended target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 00:30:49 -04:00
if(!ValidateBarrierInputs())
return INIT_FAILED;
if(!ValidateRiskInputs())
return INIT_FAILED;
//--- Configure the account loss budget BEFORE anything can size or place a trade. Everything
//--- downstream (the money manager's clamp, the risk-guard veto) reads this one object, so it must be
//--- live from the first tick rather than from whenever the signal pipeline happens to initialise.
g_riskBudget.Configure(EnableRiskGuard, MaxDailyLossPct, MaxDrawdownPct, MaxDrawdownIsTrailing,
RiskDayResetHour, RiskPerTradeOfBudget, RiskGuardFlatten,
(long)Expert_MagicNumber, Symbol());
feat: expectancy stop - halt when the measured result says the strategy loses The daily (4%) and total (8%) rules bound how FAST an account can lose. Nothing noticed WHETHER it was losing. A negative-expectancy signal traded at 1% inside that envelope breaches no rule and still arrives at zero - it just takes longer, with every limit green the whole way down. That is the realistic way this EA destroys an account, and no existing guard could see it. THE ARITHMETIC THIS ENFORCES. Expected value per trade is p*TP - (1-p)*SL - cost. With no directional edge p equals SL/(SL+TP), which is also the break-even rate, so the payoff terms cancel exactly and EV = -cost. Expected P&L is -(trades) x cost: strictly negative, proportional to activity. Measured here: directional precision 23-24% against a 25% break-even, flat across every confidence tier, with 58 points of spread on SP500. Sizing, stop placement and trailing move variance around that mean; none of them changes its sign. So every closed position now reports its result in R (net profit over money actually at risk) and the running mean is tested against zero. Above the configured minimum sample, if mean + sigma*SE < 0, new entries stop. - SIGNIFICANTLY below, not merely below. A run of losers is ordinary variance even for a profitable system; halting on the raw mean would be the same act-on-noise error the MI gates exist to prevent. Using the standard error means a wide spread simply demands more trades before the rule can fire. - NET of swap and commission (ResolveClose already sums all three). Deliberate and load-bearing: when the edge is zero, cost IS the expectancy, so a gross version would measure a strategy nobody can trade. - Reported in R so symbols, lot sizes and balances share one scale and one mean. Trades without a stop are not scored rather than assigned a guessed R. - LATCHED across restarts, like the daily halt and for the same reason: a latch a reattach clears is not a latch. Clearing it means deleting the risk state file, deliberately, after looking at why. State is appended to the risk file length-guarded, so files written before this still load and start their sample at zero rather than misreading. Defaults 40 trades / 2 sigma; ExpectancyMinTrades = 0 disables it. This does not make the strategy profitable and is not meant to. It stops paying tuition on one the results say is losing, and does it on measurement rather than on a drawdown limit finally being reached. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:20:00 -04:00
g_riskBudget.ConfigureExpectancy(ExpectancyMinTrades, ExpectancySigma);
g_riskBudget.Update();
if(EnableRiskGuard)
Print("Warrior EA: ", g_riskBudget.StatusLine());
feat: expectancy stop - halt when the measured result says the strategy loses The daily (4%) and total (8%) rules bound how FAST an account can lose. Nothing noticed WHETHER it was losing. A negative-expectancy signal traded at 1% inside that envelope breaches no rule and still arrives at zero - it just takes longer, with every limit green the whole way down. That is the realistic way this EA destroys an account, and no existing guard could see it. THE ARITHMETIC THIS ENFORCES. Expected value per trade is p*TP - (1-p)*SL - cost. With no directional edge p equals SL/(SL+TP), which is also the break-even rate, so the payoff terms cancel exactly and EV = -cost. Expected P&L is -(trades) x cost: strictly negative, proportional to activity. Measured here: directional precision 23-24% against a 25% break-even, flat across every confidence tier, with 58 points of spread on SP500. Sizing, stop placement and trailing move variance around that mean; none of them changes its sign. So every closed position now reports its result in R (net profit over money actually at risk) and the running mean is tested against zero. Above the configured minimum sample, if mean + sigma*SE < 0, new entries stop. - SIGNIFICANTLY below, not merely below. A run of losers is ordinary variance even for a profitable system; halting on the raw mean would be the same act-on-noise error the MI gates exist to prevent. Using the standard error means a wide spread simply demands more trades before the rule can fire. - NET of swap and commission (ResolveClose already sums all three). Deliberate and load-bearing: when the edge is zero, cost IS the expectancy, so a gross version would measure a strategy nobody can trade. - Reported in R so symbols, lot sizes and balances share one scale and one mean. Trades without a stop are not scored rather than assigned a guessed R. - LATCHED across restarts, like the daily halt and for the same reason: a latch a reattach clears is not a latch. Clearing it means deleting the risk state file, deliberately, after looking at why. State is appended to the risk file length-guarded, so files written before this still load and start their sample at zero rather than misreading. Defaults 40 trades / 2 sigma; ExpectancyMinTrades = 0 disables it. This does not make the strategy profitable and is not meant to. It stops paying tuition on one the results say is losing, and does it on measurement rather than on a drawdown limit finally being reached. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:20:00 -04:00
//--- Stated at init so the sample carried over from previous sessions is visible before any trade is
//--- placed, rather than only appearing in the line that halts trading.
if(ExpectancyMinTrades > 0 && g_riskBudget.ExpectancyTrades() > 0)
PrintFormat("Warrior EA: realised expectancy %.3f R over %d closed trades (halts below %.1f standard "
"errors under zero, after %d trades). Expected value per trade with no directional edge "
"is minus the cost, so a persistently negative figure here is the strategy, not variance.",
g_riskBudget.ExpectancyR(), g_riskBudget.ExpectancyTrades(), ExpectancySigma,
ExpectancyMinTrades);
LoadSignalsVisibilityState();
int maxRetryOnError = 5;
string functionName = __FUNCTION__;
// Initialize random seed based on the number of milliseconds since the system started
feat(rng): ALGLIB's L'Ecuyer generator replaces MathRand, and a seed collision goes with it MQL5's MathRand() is the 15-bit MSVC LCG - 32768 distinct values and the lattice structure that shape of generator has. Two places here actually lean on randomness and both were hurt by it: WEIGHT INIT. Six He/LeCun-uniform sites drew ((MathRand()+1)/32768.0 - 0.5) * 2 * scale, so a first dense layer of ~250k weights had only 32768 possible values and thousands of connections started byte-identical. Breaking that symmetry is the whole job of random init. SHUFFLING. ShuffleRandomIndex() already had to splice TWO MathRand() draws to reach 30 bits, and its own comment documented the residual modulo bias it still carried. HQRndUniformI() is rejection-sampled and exactly uniform, so the splice and the bias note both go. CHighQualityRand is L'Ecuyer's combined multiplicative congruential generator - two differenced streams, 31-bit output, period ~2.3e18 - and it ships with the terminal. AND A BUG THE MIGRATION EXPOSED. The three MathSrand(GetTickCount()) calls sit immediately before "build a fresh topology", once per model. GetTickCount() steps in ~15.6 ms on Windows and an ensemble builds every member inside one OnInit, so members could be handed the SAME seed and draw the SAME weights wherever their shapes coincide - and members that start identical are not an ensemble. WarriorRandSeed() takes a salt (the model id) plus a never-reset call counter, so a collision is impossible rather than merely unlikely, while the tick keeps the run itself genuinely unrepeatable the way those call sites asked for. Seeds are masked positive rather than trusted: HQRndSeed computes s % (M-1) + 1 and MQL5's % keeps the sign, so a negative seed leaves the generator in a state its own assertions reject. GetTickCount() is a uint and goes negative as an int after ~24 days of uptime - a fault that would surface as "training is broken" on a long-running terminal and nowhere else. The indicator tuner's 52 draws move across too: its random search is where sample quality earns its keep. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:29:12 -04:00
//--- One process-wide seeding at startup; every fresh topology re-seeds with its own model id
//--- on top of this (System\Random.mqh).
WarriorRandSeed("OnInit");
// Initialize expert
bool expertInitialized = false;
for(int tries = 0; !expertInitialized && tries < 5; ++tries)
{
if(!Expert.Init(Symbol(), Period(), Expert_EveryTick, Expert_MagicNumber))
{
Print(functionName + ": Failed initializing expert, retrying...");
RandomSleep();
}
else
{
expertInitialized = true;
break;
}
}
if(!expertInitialized)
{
Print(functionName + ": Failed to initialize expert after retries");
return INIT_FAILED;
}
Expert.OnChartEventProcess(true);
//--- Alt-data MUST be on disk BEFORE any model is built below, not after: InitNeuralNetwork's
//--- fingerprint/input-width pinning happens once, at construction, and never re-widens for data
//--- that lands later (see AltDataReload's declaration comment for the OLD failure - a fresh
//--- attach with an empty AltData folder built every model at 0 features while the OnTimer-driven
//--- download finished moments afterward, and every model then trained on price alone for the rest
//--- of the run). Blocking here instead of leaving it to the timer means the one-off backfill
//--- WebRequest(s) (seconds, per CAltDataFetch::Update's comment) delay this OnInit instead of
//--- racing it - a slower attach is the correct trade against a silently undersized model.
//--- Skipped for an unmapped symbol (Update() no-ops via FindSpec) and in the tester/optimizer,
//--- where WebRequest is unavailable by platform rule - same guard OnTimer's upkeep tick uses.
if(EnableAltData && !MQLInfoInteger(MQL_TESTER) && !MQLInfoInteger(MQL_OPTIMIZATION))
{
g_lastAltDataRun = TimeCurrent();
g_altDataFetch.Update(_Symbol);
}
//--- Same fix, same reason, for the cross-asset reference pairs (System\CrossAsset.mqh): a fresh
//--- model's first successful Build() pins whichever reference pairs happened to be synchronised at
//--- that instant, for the model's life (see WarmBlocking's declaration comment) - so a pair still
//--- mid-sync at attach time would be silently and permanently excluded, exactly the alt-data failure
//--- above. Skipped in the tester/optimizer, where the agent loads auxiliary symbols from the
//--- terminal rather than syncing them, so waiting here would only burn the timeout for nothing.
if(EnableCrossAsset && !MQLInfoInteger(MQL_TESTER) && !MQLInfoInteger(MQL_OPTIMIZATION))
{
CCrossAssetPanel crossAssetWarmer;
//--- A TIMEOUT HERE MUST BE AUDIBLE. Say so in the journal, because from that point on
//--- nothing else in the run ever mentions the missing pair again.
fix: the DB backfill could never run, and HEAD did not compile Four defects in 64c5dd5/1a05e63, found by review + a baseline compile. Goals 1-8 of that session are unchanged; this makes 6 and 8 actually reachable. 1. HEAD DID NOT COMPILE - 6 errors. CControlPanel::Minimize/Maximize were declared `virtual bool ... override`, but CAppDialog declares both as `virtual void` (Controls\Dialog.mqh). errors 265 + 404 on each, plus 151 on `bool ok = CAppDialog::Minimize()`. Return type is void now; there was never a success flag to forward. Verified: 0 errors, 0 warnings. 2. THE BACKFILL COULD NEVER ADVANCE, and neither could the OOS continual simulation (that one has been dead since it was written). Both are armed at the instant convergence is declared, and both advance only from inside Train(), one chunk per call. But ScheduleTrainingIfNeeded's only per-tick ArmStudyEvent site sits in the `else` of a branch taken whenever m_trainingComplete is set and m_trainRunActive is clear - which is exactly the state FinalizeTrainRun() leaves behind one line before they are armed. Train() was never called again, so the walks sat at their start index forever: no "simulation complete" line, and not one row written to the DB this feature exists to fill. Only a manual Resume/Retrain unstuck them. Both flags now keep the model schedulable. 3. IN AI_HYBRID - the mode this ships in - the backfill was never even armed. Ensemble members deploy at Train() ENTRY and return immediately (so no era is wasted), which skips the era-end block the backfill was started from. All four members were a no-op for a second, independent reason. Armed on the ensemble deploy path too, from m_resumeBars/m_resumeOosCutoff. 4. RE-RUNS DUPLICATED ROWS. RegisterSignal inserts unconditionally - no key, no duplicate check - and m_dbBackfillDone is in-memory, so every later attach that retrained to convergence wrote a second full set of rows for the same bars. The ranking would count one bar once per model that ever deployed, weighting superseded opinions as heavily as the live one. A .dbfill marker stamps the deployed era; written only on completion (an interrupted walk redoes itself rather than ranking a partial window) and deleted with the other sidecars on reset-weights. Also: WarmBlocking's timeout was silent, which restored the exact silent pin failure it was added to prevent - it now says so in the journal, and returns true for "no reference pairs to wait for" so the warning stays rare enough to be read. Not addressed, needs a decision: the backfill scores the OOS window with the checkpoint that was SELECTED as best on that same window, then writes those win rates into the table filter weights rank on - the selection set consumed twice, undiscounted, while the deploy gate right next to it applies a family-wise correction for exactly that effect. The rows are also simulated triple-barrier outcomes at today's spread sharing a table with realised fills. The completion log line now states both plainly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 21:25:51 -04:00
if(!crossAssetWarmer.WarmBlocking((ENUM_TIMEFRAMES)Period(), 4000))
Print("Cross-asset warm-up did not finish syncing every reference pair within 4s - the model"
" built below will PIN whichever pairs are ready at that moment and never add the rest."
" If the feature count looks short, detach and re-attach once the terminal has finished"
" downloading the reference symbols' history.");
}
// Creating signal
PrintVerbose("Initializing Signal...");
CExpertSignalCustom* signal = NULL;
for(int tries = 0; signal == NULL && tries < 5; ++tries)
{
signal = new CExpertSignalCustom;
if(signal == NULL)
{
Print(functionName + ": Failed to initialize Signal, retrying...");
RandomSleep();
}
else
{
break;
}
}
if(signal == NULL)
{
Print(functionName + ": Failed to initialize Signal after retries");
return INIT_FAILED;
}
InitializeSignal(signal);
// Initializing Database
if(UseDatabaseRanking)
{
bool dbInitialized = false;
string databaseFolderStructure[] = {eaName, "Databases", "Signals"};
//--- fingerprinted so a topology/feature-set change that would produce a differently-shaped or
//--- differently-behaving model gets its own database, instead of silently mixing pattern-weight/
//--- trade-journal history from an incompatible prior config into the one now trading.
const string dbName = Symbol() + "_" + IntegerToString(Period()) + "_" + ComputeDbConfigFingerprint() + ".db";
fix(db): per-side pattern journaling + versioned journaling semantics The labelMatchesVote gate compared a single last-writer-wins label (LongCondition then ShortCondition) against the net vote sign, which structurally censored the pattern tables: a long event co-occurring with any short-side state model lost its label to the later writer and was dropped, while the mirrored short event journaled fine. Ichimoku models 0/3 and MA model 1 could not produce a row at all by construction (MA model 1 was "revived" in 8710240 yet still could never journal - its weight-10 vote is exactly cancelled by the opposing Pattern_0 state), and every pattern's win rate was measured on a with-trend-only subset - the exact statistic UpdateSignalsWeights() feeds back into the weights, self-sealing: no rows -> no win rate -> default weight -> still censored. - Direction() now evaluates the two ladders separately and snapshots each ladder's matched pattern into its own side slot; each side that matched journals its own row. The flat-vote poisoning the old gate fixed stays fixed: a label can no longer contradict its side. - The filter's net vote (raw pattern-weight units) is stored as a new netVote column - data, never a drop filter. Snapshot is keyed on the ladder setting a label, not on its weight, so a 0%-win-rate pattern keeps journaling and can recover. - SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB filename fingerprint: pattern-definition changes (b2069bc, 8710240) re-key the database instead of blending incompatible Pattern_N populations under one key, which the input-hash fingerprint cannot see. 7 months of mixed-semantics rows shared one file because of it. - dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new column, so the version-mismatch folder wipe is the migration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:37:57 -04:00
//--- 3.0: the pattern tables gained the netVote column and journaling went per-side (see
//--- CExpertSignalCustom::Direction()). 4.0 (2026-08-19): DB timestamps switched from GMT to
//--- BROKER time (user decision: one clock everywhere).
refactor(time): broker time throughout - and the GMT DB basis was already a live bug User decision: "stick to the broker's time throughout the codebase and analysis, session filter, programmed close time etc". Investigation found the GMT choice was not just inconsistent but broken: live journaling stamped DB rows with TimeGMT() while the online-learning backfill stamped them with BAR time (server) - two clocks ~3h apart in the same column. The newest-row duplicate guard compares them on one axis, so a live row landing within the offset after a backfill row was silently rejected as "outdated". dbVersion 3.0 -> 4.0 wipes the Signals store: the only honest reset for a mixed-basis corpus. - Direction()'s clock (stamps every journaled row, keys the per-second vote window): TimeGMT -> TimeCurrent, variables renamed so the name cannot lie about the basis. - UpdateSignalsWeights' future-row bound: same clock as the rows. - Session filter: broker-time anchors (London 10-18, NY 15-23:59, Tokyo 2-11). The GMT anchors were backwards for an EET-family broker - such a broker follows European DST, so London is DST-STABLE in broker time and moved twice a year in GMT. Tokyo drifts 1h each European summer (no DST to track) - accepted, smallest error on offer. Also fixed: inTimeInterval ignored its datetime parameter and called TimeGMT fresh - a dead parameter hiding a hardwired clock. - MetaCorpus/SignalMETA: rows pre-4.0 are GMT, broker since; the GMT->server offset scan is KEPT because it measures rather than assumes - it pins 0 on new corpora and still resolves old ones. - AltDataFetch deliberately stays on GMT: FRED/COT/EIA release schedules are external UTC-anchored events; the as-of join maps them onto server bars downstream. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:37:44 -04:00
const string dbVersion = "4.0";
PrintVerbose("Initializing Database...");
for(int tries = 0; !dbInitialized && tries < 5; ++tries)
{
if(!dbm.Init(dbVersion, databaseFolderStructure, dbName))
{
Print(functionName + ": Failed to initialize Database, retrying...");
RandomSleep();
}
else
{
dbInitialized = true;
break;
}
}
if(!dbInitialized)
{
Print(functionName + ": Failed to initialize Database after retries");
return INIT_FAILED;
}
//--- Trade journal shares UseDatabaseRanking's DB connection/lifecycle rather than adding a
//--- second always-on toggle - see Database\TradeJournalManager.mqh's class comment.
if(!journal.Init(GetPointer(dbm), Expert_MagicNumber))
{
Print(functionName + ": Failed to initialize trade journal table");
return INIT_FAILED;
}
//--- Meta_Labeling_Design.md S1: the pattern DB doubles as the meta-label training corpus.
//--- The stale-DB check is ALWAYS on in the tester (a forgotten wipe silently voids a whole
//--- corpus run); the full report - volume per family, closed fraction, and the measured
//--- GMT->server bar-time offset S2's label plumbing pins to - stays under VerboseMode.
MetaCorpusStaleCheck();
if(VerboseMode)
MetaCorpusReport();
}
fix: four risk-layer holes a funded account would eventually find 1. The expectancy stop was stone dead at shipped defaults. Its only feed - RecordTradeResult inside CTradeJournalManager::Update() - ran solely under UseDatabaseRanking, which ships false, so the da54639 halt was armed (ExpectancyMinTrades=40) and never received a single closed trade. A risk rule must not be a side effect of an analytics toggle: the journal gains InitTrackingOnly(), Update() runs unconditionally from OnTick and skips only the DB insert when no DB was initialized. 2. Below-minimum lots were silently bumped UP to SYMBOL_VOLUME_MIN by TCNormalizeVolume - correct for a user-entered fixed lot, but in the risk-sizing path it turned a budget-capped 0.05 into 0.10 on min-0.10/ step-0.01 symbols: double the intended risk, after CapRiskAmount already clamped, exactly the routine-stop-out-breaches-the-daily-limit scenario the budget exists to close. CMoneyRiskBase now refuses the trade when the risk-derived lot is below the broker minimum. 3. All trading was async fire-and-forget (SetAsyncMode(true)) with no OnTradeTransaction handler and no retry: server retcodes were never observed. Fail-safe for entries, not for closes - a silently rejected close rode the position until the next bar (or next day for the timed close window). Now synchronous, matching the risk-budget flatten's own already-synchronous CTrade; on an H1 EA the latency is irrelevant. 4. FIXED_LOT bypassed the budget entirely (no CapRiskAmount, no OpenRiskAtStops) - pre-halt it could commit more than the remaining daily allowance. A fixed lot cannot be scaled, so the rule is binary: its loss-to-stop fits the remaining allowance whole or the trade is refused; unpriceable risk (no SL) is refused while the budget is enabled. Compile: 0 errors, 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:14:26 -04:00
else
//--- No DB, but the close-detection path still runs: it feeds the expectancy stop
//--- (g_riskBudget.RecordTradeResult). Until 2026-08-11 that feed existed only under
//--- UseDatabaseRanking (ships false), so the expectancy halt could never arm on a default
//--- install - see InitTrackingOnly's comment in Database\TradeJournalManager.mqh.
journal.InitTrackingOnly(Expert_MagicNumber);
//+------------------------------------------------------------------+
//| The per-NN inputs (Use_MLP/Use_CONV/Use_LSTM/Use_CONVLSTM) pick |
//| which direction architectures this run trades/trains - any |
//| subset, each an independent model - plus the optional meta head |
//| (Use_MetaLabeling). |
//+------------------------------------------------------------------+
feat(ensemble): per-NN inputs replace the preset selector - the meta head becomes the vote's gate User design (2026-08-19): 'remove the enum menu that selects neural networks... individual inputs for every NN just like classic signals... the META NN should be integrated into the voting decision pipeline when enabled... as a bonus meta labelling is applied to enabled NNs.' - AI_CHOICE is GONE (tombstoned per the stale-.set doctrine). Use_MLP/Use_CONV/Use_LSTM/ Use_CONVLSTM are ordinary bools like the classic votes; the ensemble arithmetic adapts to any subset because the consensus divisor is the enabled capable weight. Two or more enabled = ensemble (|ENS1 token + joint gate, exactly the old AI_HYBRID fingerprints, so existing weight files keep loading); one = the old solo preset; none = classic-only. - Use_MetaLabeling un-couples META from the direction NNs (the old selector made them mutually exclusive). S3 ships: CSignalMETA::LiveMetaGate scores each vote-cleared entry (shared window at bar 1 + proposal descriptor: side, net vote, live geometry, spread/ATR; pattern one-hot ZEROED - ranking, not calibrated probability, documented in the body) and vetoes below the cost-adjusted break-even. Entries only; fail-open everywhere, loudly. - COEXISTENCE HAZARDS closed: VoteCapableWeight()=0 and ProspectiveVote()=false for the meta target - solo-only until today, a trained META would otherwise sit in the consensus divisor as a permanent abstainer and shrink every vote by its module weight. - CERTIFIED == TRADED: the ensemble era verdict replays the identical veto through the same g_warriorMetaGate pointer over its OOS fired bars (bar re-resolved from the row's own time; fail-open counted as fires and reported: 'metaGate: N approved, M vetoed, K unscored'). The overlay deliberately does NOT replay it (veto-filter-in-replay class, calendar-cliff precedent) - documented at the sweep site. Solo charts' own gate does not model the veto - the standing solo-gate caveat, documented at the input. - DB continuity: the pattern/journal DB fingerprint's first slot was (int)AIType; DbLegacyAiSlot() maps every legacy-expressible config to its OLD value (new 2-3 member subsets get 100+bitmask, outside the legacy range) so no existing database re-keys. filterID becomes the enabled roster via one EnabledNNSummary(). - HUD: the meta line shows the gate (armed/(trn), last P vs BE, ok/veto tally); the armed/disarmed announcement fires on state change via one latch (MetaGateArmedNow), not only when an entry happens to be proposed. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:01:02 -04:00
//--- Re-derived from the per-NN inputs (they are also initialized at file scope in
//--- Variables\Variables.mqh; this re-assignment is the init-order-safe truth).
feat(ensemble): per-NN inputs replace the preset selector - the meta head becomes the vote's gate User design (2026-08-19): 'remove the enum menu that selects neural networks... individual inputs for every NN just like classic signals... the META NN should be integrated into the voting decision pipeline when enabled... as a bonus meta labelling is applied to enabled NNs.' - AI_CHOICE is GONE (tombstoned per the stale-.set doctrine). Use_MLP/Use_CONV/Use_LSTM/ Use_CONVLSTM are ordinary bools like the classic votes; the ensemble arithmetic adapts to any subset because the consensus divisor is the enabled capable weight. Two or more enabled = ensemble (|ENS1 token + joint gate, exactly the old AI_HYBRID fingerprints, so existing weight files keep loading); one = the old solo preset; none = classic-only. - Use_MetaLabeling un-couples META from the direction NNs (the old selector made them mutually exclusive). S3 ships: CSignalMETA::LiveMetaGate scores each vote-cleared entry (shared window at bar 1 + proposal descriptor: side, net vote, live geometry, spread/ATR; pattern one-hot ZEROED - ranking, not calibrated probability, documented in the body) and vetoes below the cost-adjusted break-even. Entries only; fail-open everywhere, loudly. - COEXISTENCE HAZARDS closed: VoteCapableWeight()=0 and ProspectiveVote()=false for the meta target - solo-only until today, a trained META would otherwise sit in the consensus divisor as a permanent abstainer and shrink every vote by its module weight. - CERTIFIED == TRADED: the ensemble era verdict replays the identical veto through the same g_warriorMetaGate pointer over its OOS fired bars (bar re-resolved from the row's own time; fail-open counted as fires and reported: 'metaGate: N approved, M vetoed, K unscored'). The overlay deliberately does NOT replay it (veto-filter-in-replay class, calendar-cliff precedent) - documented at the sweep site. Solo charts' own gate does not model the veto - the standing solo-gate caveat, documented at the input. - DB continuity: the pattern/journal DB fingerprint's first slot was (int)AIType; DbLegacyAiSlot() maps every legacy-expressible config to its OLD value (new 2-3 member subsets get 100+bitmask, outside the legacy range) so no existing database re-keys. filterID becomes the enabled roster via one EnabledNNSummary(). - HUD: the meta line shows the gate (armed/(trn), last P vs BE, ok/veto tally); the armed/disarmed announcement fires on state change via one latch (MetaGateArmedNow), not only when an entry happens to be proposed. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:01:02 -04:00
EnablePAI = Use_MLP;
EnableCONV = Use_CONV;
EnableLSTM = Use_LSTM;
EnableHYBRID = Use_CONVLSTM;
EnableMETA = Use_MetaLabeling;
// Creating instances of signals
CSignalPAI *PAI = CreateSignalWithRetry<CSignalPAI>(maxRetryOnError, EnablePAI);
CSignalCONV *CONV = CreateSignalWithRetry<CSignalCONV>(maxRetryOnError, EnableCONV);
CSignalLSTM *LSTM = CreateSignalWithRetry<CSignalLSTM>(maxRetryOnError, EnableLSTM);
CSignalHYBRID *HYBRID = CreateSignalWithRetry<CSignalHYBRID>(maxRetryOnError, EnableHYBRID);
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus The NN now has a target that is not per-bar direction (closed, best-of-999 p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of cost). One net for all 52 pattern-sides, AIType=AI_META. - NetForward.mqh: the host-side softmax+CE gradient generalized total==3 -> 2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no compute backend changes. - SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on disk (decoupled from the config fingerprint that burned four S1 runs); the GMT->server offset is measured PER ROW against entryPrice vs bar open (DST-immune, histogram logged); a window-span regime filter drops the pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR). - Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell lets checkpoint selection, the edge floor, the plateau ladder and the family-wise deploy gate run UNCHANGED: precision reads as win rate among traded candidates, chance as the base win rate, recalls as sensitivity/ specificity. Era-end META line: coverage x (p - break-even) vs the null. - Labels are the side-conditional triple-barrier win caches - never the DB's stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base rate). Live inference + online learning guarded off until S3. - Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename slot keep meta models fully separate from direction models. Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with AIType=AI_META; S3 wires the votes via the per-side hooks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
CSignalMETA *META = CreateSignalWithRetry<CSignalMETA>(maxRetryOnError, EnableMETA);
//--- register whichever AI signal instances this run created, so the control panel can drive
feat(ensemble): per-NN inputs replace the preset selector - the meta head becomes the vote's gate User design (2026-08-19): 'remove the enum menu that selects neural networks... individual inputs for every NN just like classic signals... the META NN should be integrated into the voting decision pipeline when enabled... as a bonus meta labelling is applied to enabled NNs.' - AI_CHOICE is GONE (tombstoned per the stale-.set doctrine). Use_MLP/Use_CONV/Use_LSTM/ Use_CONVLSTM are ordinary bools like the classic votes; the ensemble arithmetic adapts to any subset because the consensus divisor is the enabled capable weight. Two or more enabled = ensemble (|ENS1 token + joint gate, exactly the old AI_HYBRID fingerprints, so existing weight files keep loading); one = the old solo preset; none = classic-only. - Use_MetaLabeling un-couples META from the direction NNs (the old selector made them mutually exclusive). S3 ships: CSignalMETA::LiveMetaGate scores each vote-cleared entry (shared window at bar 1 + proposal descriptor: side, net vote, live geometry, spread/ATR; pattern one-hot ZEROED - ranking, not calibrated probability, documented in the body) and vetoes below the cost-adjusted break-even. Entries only; fail-open everywhere, loudly. - COEXISTENCE HAZARDS closed: VoteCapableWeight()=0 and ProspectiveVote()=false for the meta target - solo-only until today, a trained META would otherwise sit in the consensus divisor as a permanent abstainer and shrink every vote by its module weight. - CERTIFIED == TRADED: the ensemble era verdict replays the identical veto through the same g_warriorMetaGate pointer over its OOS fired bars (bar re-resolved from the row's own time; fail-open counted as fires and reported: 'metaGate: N approved, M vetoed, K unscored'). The overlay deliberately does NOT replay it (veto-filter-in-replay class, calendar-cliff precedent) - documented at the sweep site. Solo charts' own gate does not model the veto - the standing solo-gate caveat, documented at the input. - DB continuity: the pattern/journal DB fingerprint's first slot was (int)AIType; DbLegacyAiSlot() maps every legacy-expressible config to its OLD value (new 2-3 member subsets get 100+bitmask, outside the legacy range) so no existing database re-keys. filterID becomes the enabled roster via one EnabledNNSummary(). - HUD: the meta line shows the gate (armed/(trn), last P vs BE, ok/veto tally); the armed/disarmed announcement fires on state change via one latch (MetaGateArmedNow), not only when an entry happens to be proposed. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:01:02 -04:00
//--- training/weight actions on exactly this run's current config (never another config's files)
g_aiSignalCount = 0;
feat(ensemble): per-NN inputs replace the preset selector - the meta head becomes the vote's gate User design (2026-08-19): 'remove the enum menu that selects neural networks... individual inputs for every NN just like classic signals... the META NN should be integrated into the voting decision pipeline when enabled... as a bonus meta labelling is applied to enabled NNs.' - AI_CHOICE is GONE (tombstoned per the stale-.set doctrine). Use_MLP/Use_CONV/Use_LSTM/ Use_CONVLSTM are ordinary bools like the classic votes; the ensemble arithmetic adapts to any subset because the consensus divisor is the enabled capable weight. Two or more enabled = ensemble (|ENS1 token + joint gate, exactly the old AI_HYBRID fingerprints, so existing weight files keep loading); one = the old solo preset; none = classic-only. - Use_MetaLabeling un-couples META from the direction NNs (the old selector made them mutually exclusive). S3 ships: CSignalMETA::LiveMetaGate scores each vote-cleared entry (shared window at bar 1 + proposal descriptor: side, net vote, live geometry, spread/ATR; pattern one-hot ZEROED - ranking, not calibrated probability, documented in the body) and vetoes below the cost-adjusted break-even. Entries only; fail-open everywhere, loudly. - COEXISTENCE HAZARDS closed: VoteCapableWeight()=0 and ProspectiveVote()=false for the meta target - solo-only until today, a trained META would otherwise sit in the consensus divisor as a permanent abstainer and shrink every vote by its module weight. - CERTIFIED == TRADED: the ensemble era verdict replays the identical veto through the same g_warriorMetaGate pointer over its OOS fired bars (bar re-resolved from the row's own time; fail-open counted as fires and reported: 'metaGate: N approved, M vetoed, K unscored'). The overlay deliberately does NOT replay it (veto-filter-in-replay class, calendar-cliff precedent) - documented at the sweep site. Solo charts' own gate does not model the veto - the standing solo-gate caveat, documented at the input. - DB continuity: the pattern/journal DB fingerprint's first slot was (int)AIType; DbLegacyAiSlot() maps every legacy-expressible config to its OLD value (new 2-3 member subsets get 100+bitmask, outside the legacy range) so no existing database re-keys. filterID becomes the enabled roster via one EnabledNNSummary(). - HUD: the meta line shows the gate (armed/(trn), last P vs BE, ok/veto tally); the armed/disarmed announcement fires on state change via one latch (MetaGateArmedNow), not only when an entry happens to be proposed. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:01:02 -04:00
//--- The meta gate pointer must not survive a re-init: an input change re-enters OnInit in the
//--- same program instance, and the old object is freed by the filter-array teardown.
g_warriorMetaGate = NULL;
if(EnablePAI && PAI != NULL)
RegisterAISignal(PAI);
if(EnableCONV && CONV != NULL)
RegisterAISignal(CONV);
if(EnableLSTM && LSTM != NULL)
RegisterAISignal(LSTM);
if(EnableHYBRID && HYBRID != NULL)
RegisterAISignal(HYBRID);
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus The NN now has a target that is not per-bar direction (closed, best-of-999 p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of cost). One net for all 52 pattern-sides, AIType=AI_META. - NetForward.mqh: the host-side softmax+CE gradient generalized total==3 -> 2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no compute backend changes. - SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on disk (decoupled from the config fingerprint that burned four S1 runs); the GMT->server offset is measured PER ROW against entryPrice vs bar open (DST-immune, histogram logged); a window-span regime filter drops the pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR). - Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell lets checkpoint selection, the edge floor, the plateau ladder and the family-wise deploy gate run UNCHANGED: precision reads as win rate among traded candidates, chance as the base win rate, recalls as sensitivity/ specificity. Era-end META line: coverage x (p - break-even) vs the null. - Labels are the side-conditional triple-barrier win caches - never the DB's stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base rate). Live inference + online learning guarded off until S3. - Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename slot keep meta models fully separate from direction models. Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with AIType=AI_META; S3 wires the votes via the per-side hooks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
if(EnableMETA && META != NULL)
feat(ensemble): per-NN inputs replace the preset selector - the meta head becomes the vote's gate User design (2026-08-19): 'remove the enum menu that selects neural networks... individual inputs for every NN just like classic signals... the META NN should be integrated into the voting decision pipeline when enabled... as a bonus meta labelling is applied to enabled NNs.' - AI_CHOICE is GONE (tombstoned per the stale-.set doctrine). Use_MLP/Use_CONV/Use_LSTM/ Use_CONVLSTM are ordinary bools like the classic votes; the ensemble arithmetic adapts to any subset because the consensus divisor is the enabled capable weight. Two or more enabled = ensemble (|ENS1 token + joint gate, exactly the old AI_HYBRID fingerprints, so existing weight files keep loading); one = the old solo preset; none = classic-only. - Use_MetaLabeling un-couples META from the direction NNs (the old selector made them mutually exclusive). S3 ships: CSignalMETA::LiveMetaGate scores each vote-cleared entry (shared window at bar 1 + proposal descriptor: side, net vote, live geometry, spread/ATR; pattern one-hot ZEROED - ranking, not calibrated probability, documented in the body) and vetoes below the cost-adjusted break-even. Entries only; fail-open everywhere, loudly. - COEXISTENCE HAZARDS closed: VoteCapableWeight()=0 and ProspectiveVote()=false for the meta target - solo-only until today, a trained META would otherwise sit in the consensus divisor as a permanent abstainer and shrink every vote by its module weight. - CERTIFIED == TRADED: the ensemble era verdict replays the identical veto through the same g_warriorMetaGate pointer over its OOS fired bars (bar re-resolved from the row's own time; fail-open counted as fires and reported: 'metaGate: N approved, M vetoed, K unscored'). The overlay deliberately does NOT replay it (veto-filter-in-replay class, calendar-cliff precedent) - documented at the sweep site. Solo charts' own gate does not model the veto - the standing solo-gate caveat, documented at the input. - DB continuity: the pattern/journal DB fingerprint's first slot was (int)AIType; DbLegacyAiSlot() maps every legacy-expressible config to its OLD value (new 2-3 member subsets get 100+bitmask, outside the legacy range) so no existing database re-keys. filterID becomes the enabled roster via one EnabledNNSummary(). - HUD: the meta line shows the gate (armed/(trn), last P vs BE, ok/veto tally); the armed/disarmed announcement fires on state change via one latch (MetaGateArmedNow), not only when an entry happens to be proposed. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:01:02 -04:00
{
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus The NN now has a target that is not per-bar direction (closed, best-of-999 p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of cost). One net for all 52 pattern-sides, AIType=AI_META. - NetForward.mqh: the host-side softmax+CE gradient generalized total==3 -> 2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no compute backend changes. - SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on disk (decoupled from the config fingerprint that burned four S1 runs); the GMT->server offset is measured PER ROW against entryPrice vs bar open (DST-immune, histogram logged); a window-span regime filter drops the pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR). - Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell lets checkpoint selection, the edge floor, the plateau ladder and the family-wise deploy gate run UNCHANGED: precision reads as win rate among traded candidates, chance as the base win rate, recalls as sensitivity/ specificity. Era-end META line: coverage x (p - break-even) vs the null. - Labels are the side-conditional triple-barrier win caches - never the DB's stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base rate). Live inference + online learning guarded off until S3. - Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename slot keep meta models fully separate from direction models. Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with AIType=AI_META; S3 wires the votes via the per-side hooks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
RegisterAISignal(META);
feat(ensemble): per-NN inputs replace the preset selector - the meta head becomes the vote's gate User design (2026-08-19): 'remove the enum menu that selects neural networks... individual inputs for every NN just like classic signals... the META NN should be integrated into the voting decision pipeline when enabled... as a bonus meta labelling is applied to enabled NNs.' - AI_CHOICE is GONE (tombstoned per the stale-.set doctrine). Use_MLP/Use_CONV/Use_LSTM/ Use_CONVLSTM are ordinary bools like the classic votes; the ensemble arithmetic adapts to any subset because the consensus divisor is the enabled capable weight. Two or more enabled = ensemble (|ENS1 token + joint gate, exactly the old AI_HYBRID fingerprints, so existing weight files keep loading); one = the old solo preset; none = classic-only. - Use_MetaLabeling un-couples META from the direction NNs (the old selector made them mutually exclusive). S3 ships: CSignalMETA::LiveMetaGate scores each vote-cleared entry (shared window at bar 1 + proposal descriptor: side, net vote, live geometry, spread/ATR; pattern one-hot ZEROED - ranking, not calibrated probability, documented in the body) and vetoes below the cost-adjusted break-even. Entries only; fail-open everywhere, loudly. - COEXISTENCE HAZARDS closed: VoteCapableWeight()=0 and ProspectiveVote()=false for the meta target - solo-only until today, a trained META would otherwise sit in the consensus divisor as a permanent abstainer and shrink every vote by its module weight. - CERTIFIED == TRADED: the ensemble era verdict replays the identical veto through the same g_warriorMetaGate pointer over its OOS fired bars (bar re-resolved from the row's own time; fail-open counted as fires and reported: 'metaGate: N approved, M vetoed, K unscored'). The overlay deliberately does NOT replay it (veto-filter-in-replay class, calendar-cliff precedent) - documented at the sweep site. Solo charts' own gate does not model the veto - the standing solo-gate caveat, documented at the input. - DB continuity: the pattern/journal DB fingerprint's first slot was (int)AIType; DbLegacyAiSlot() maps every legacy-expressible config to its OLD value (new 2-3 member subsets get 100+bitmask, outside the legacy range) so no existing database re-keys. filterID becomes the enabled roster via one EnabledNNSummary(). - HUD: the meta line shows the gate (armed/(trn), last P vs BE, ok/veto tally); the armed/disarmed announcement fires on state change via one latch (MetaGateArmedNow), not only when an entry happens to be proposed. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:01:02 -04:00
//--- Arm the vote-pipeline meta gate (CSignalMETA::LiveMetaGate): CheckOpenPosition consults
//--- this pointer on every vote-cleared entry, and the ensemble era verdict replays the same
//--- veto over its OOS fired bars so certified == traded.
g_warriorMetaGate = META;
}
CSignalMA *MA = CreateSignalWithRetry<CSignalMA>(maxRetryOnError, EnableMA);
CSignalRSI *RSI = CreateSignalWithRetry<CSignalRSI>(maxRetryOnError, EnableRSI);
CSignalMACD *MACD = CreateSignalWithRetry<CSignalMACD>(maxRetryOnError, EnableMACD);
CSignalIchimoku *Ichimoku = CreateSignalWithRetry<CSignalIchimoku>(maxRetryOnError, EnableIchimoku);
CSignalNewsFilter *newsFilter = CreateSignalWithRetry<CSignalNewsFilter>(maxRetryOnError, EnableNewsFilter);
CSignalSessionFilter *sessionFilter = CreateSignalWithRetry<CSignalSessionFilter>(maxRetryOnError, EnableSessionFilter);
refactor(inputs): 96 -> 70 inputs; remove two untested/unusable filter modules Every removal below is FINGERPRINT-NEUTRAL by construction: each retired input is pinned to the exact value it already shipped with, so running models keep their filenames and resume rather than restarting at era 0. Verified field by field against BuildConfigFingerprint. Removed as inputs, kept as pinned constants (the value was never a preference the user had a basis to change): - OutputNeuronsCount. The regression head predicts a continuous quantity the triple-barrier label does not contain; the target is an EVENT, so the right output is its probability. The regression code paths stay implemented and dormant - they cost nothing and removing them would touch every scoring path at once. - MinRecall. A safety floor, not a preference, and the only direction a user can move it is the harmful one: raising it past what the config reaches yields NO model, not a better one (observed repeatedly at 60). - SwingConfirmationBars. Stopped gating the labels with the relabel, but is STILL load-bearing for the swing-context input features - it is the ZigZag repainting embargo, and without it those 9 features read a leg the live bar could not have had yet. Pinned, not deleted. - MaxErasPerRun (runaway backstop, never reached in a healthy run), FreezePriorCalibration (unanswerable by a user; near-balanced labels make the priors stable anyway), VerboseMode (developer view, joins DebuggingMode), MACD/Ichimoku periods x6 (both indicators ship disabled, and as optimizer dimensions they are pure overfitting surface - the AI auto-tuner is the supported way to move them). - SignalClusterWindow -> 3, no longer an input. Barrier labels make consecutive setups real, which argued for 0; it is not 0 because on D1+ a 6-bar window spans over a week and two arrows a day apart on a weekly-scale move are one event. 3 splits it correctly by timeframe. - EnableOnlineLearning -> ON. Adapting to a changing market is what keeps a months-attached model from going stale, and the rolling-accuracy freeze is what makes it safe. See the caveat noted in the handoff: it had not been forward-tested on a live feed when this became default. Removed entirely: - Intraday Time Filter (5 inputs + Signals/SignalITF.mqh). Two of its five inputs were raw BITMASKS, which is an implementation detail exposed as a control. The job is covered three times over by things that are declarative or that learn: the session filter, the time-of-day/day-of-week input features (the network discovers which hours are good rather than being told), and the journal's time buckets. - Market Depth Filter (5 inputs + Signals/SignalMarketDepth.mqh, plus its OnInit probe and OnDeinit release). It needs real level-2 data that this broker - and most retail MT5 brokers - do not provide, so the module has never once executed against real data. Shipping four tuning dropdowns for an untested path is worse than shipping nothing: the only users who could enable it would be its first-ever testers, live. If DOM returns it should be a FEATURE fed to the network, not a rule-based veto with hand-tuned thresholds - imbalance is data. - IndicatorTuneTrials, replaced by ComputeTuneTrialBudget(). The useful budget depends on how many parameters are actually being searched, which depends on which features are enabled - so one number meant wildly different things run to run. The shipped 32 was ~10 candidates per dimension against one enabled indicator (wasteful: each costs GA_SEEDS full training runs) and under one per dimension against all nine (blind). Now population ~ 4 x active dimensions, clamped [8,64], with CADIndicatorTuner::ActiveDimensions() defined immediately above PerturbRandom() so the two cannot drift apart. - Six orphaned enums (TUNE_TRIALS_PRESET, DOM_*, ENTRY_HOUR_OF_DAY, TIME_FILTER_DAY_OF_WEEK), 81 lines. Other UX: - SL_ATR_x1 / TP_ATR_x3 now carry the "(classic)" default marker every other preset enum in the file already used. Nothing in the SL/TP dropdowns previously told a user which pair was the shipped default - which matters far more since the relabel, because those two define the labels and changing either forces a retrain. - Neural Network section moved directly ABOVE AI Input Features: choose the architecture, then choose what it sees. NN Optimizer / Performance stays last - the Adam/Sgd inputs are declared in AI/Network.mqh and render immediately after that divider. - News feature + window moved to the end of the AI feature list, below Wyckoff Bar Inversion. - Dropped "(0-100)" from Min vote to open - it is an enum, not a number. Both builds compile 0 errors / 0 warnings. No retrain forced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 21:22:02 -04:00
//--- CSignalITF and CSignalMarketDepth were removed 2026-08-01 - see the removal notes in
//--- Variables\Inputs.mqh (bitmask-configured time filter, and an untestable DOM module).
CSignalRiskGuard *riskGuard = CreateSignalWithRetry<CSignalRiskGuard>(maxRetryOnError, EnableRiskGuard);
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus The NN now has a target that is not per-bar direction (closed, best-of-999 p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of cost). One net for all 52 pattern-sides, AIType=AI_META. - NetForward.mqh: the host-side softmax+CE gradient generalized total==3 -> 2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no compute backend changes. - SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on disk (decoupled from the config fingerprint that burned four S1 runs); the GMT->server offset is measured PER ROW against entryPrice vs bar open (DST-immune, histogram logged); a window-span regime filter drops the pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR). - Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell lets checkpoint selection, the edge floor, the plateau ladder and the family-wise deploy gate run UNCHANGED: precision reads as win rate among traded candidates, chance as the base win rate, recalls as sensitivity/ specificity. Era-end META line: coverage x (p - break-even) vs the null. - Labels are the side-conditional triple-barrier win caches - never the DB's stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base rate). Live inference + online learning guarded off until S3. - Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename slot keep meta models fully separate from direction models. Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with AIType=AI_META; S3 wires the votes via the per-side hooks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
if((EnableMA && MA == NULL) || (EnableRSI && RSI == NULL) || (EnableMACD && MACD == NULL) || (EnableIchimoku && Ichimoku == NULL) || (EnablePAI && PAI == NULL) || (EnableCONV && CONV == NULL) || (EnableLSTM && LSTM == NULL) || (EnableHYBRID && HYBRID == NULL) || (EnableMETA && META == NULL) || (EnableNewsFilter && newsFilter == NULL) || (EnableSessionFilter && sessionFilter == NULL) || (EnableRiskGuard && riskGuard == NULL))
{
Print("Critical signal initialization failed, cannot proceed");
return INIT_FAILED;
}
// Set filter parameters
//--- CSignalRiskGuard takes no parameters any more: the thresholds, the anchors and the state file
//--- all moved to g_riskBudget (configured in OnInit, evaluated per tick).
if(EnableSessionFilter)
{
sessionFilter.TradeLondonSession(SF_trade_LondonSession);
sessionFilter.TradeNewYorkSession(SF_trade_NewYorkSession);
sessionFilter.TradeTokyoSession(SF_trade_TokyoSession);
}
//--- Classic votes run at the chart's TUNED periods (g_Tuned*, loaded first thing in OnInit) -
//--- the same values the AI features seed from and the DB key hashes, so all three consumers
//--- always describe the same indicators. See Variables\TunedPeriods.mqh.
feat(indicators): run the built-in iMA and MetaTrader's ZigZag; add a classic-vote shift MA: CustomIndicators\ADMovingAverage is replaced by the built-in iMA (CiMA) on both consumers - the classic vote and the NN MA input feature. This drops the five advanced types ALMA/DEMA/ZLEMA/T3/Kalman, which have no iMA equivalent; MA_TYPE_PRESETS is now ENUM_MA_METHOD's own codes and the tuner searches all four. It also removes a documented failure mode: a custom indicator's depth is bounded by TERMINAL_MAXBARS, and m_MA was the one whose feature block REJECTS the bar on a short read - the "feature 25 fails on every bar" incident of 2026-08-17. A built-in is served at any depth. MIGRATION. SMA moves from code 5 to 0, so persisted type codes change meaning. SanitizeMaType() is the single validity rule; TunedPeriods records now carry a version field and a v1 record remaps 5..8 -> 0..3, falling back to SMA for a stored advanced type (unrecoverable - old 0..4 are indistinguishable from valid new codes). Existing .nnw files re-key on their own, because MA_Type is hashed into the topology fingerprint, so models retrain rather than silently running on different MA values. EXPECT A FULL RETRAIN. ZigZag: ADZigZag was a byte-identical rename of MetaQuotes' Examples\ZigZag - verified by normalising identifiers and stripping comments, 233 significant lines each with only renamed symbols differing. It now loads the stock one, so nothing is bundled and MetaQuotes' fixes arrive without a rebuild here. Both #resource entries are gone. Classic_Shift: a new input, the BAR the four classic votes evaluate on (0 = forming, 1 = last closed, default 1). One implementation on CExpertSignalCustom, inherited by all four rather than repeated per module. Defaults to a sentinel meaning "unset", so the AI signals and the aggregate keep the stock every_tick rule and their feature/label alignment is untouched. The META corpus sweep still takes precedence. CExpertBase::StartIndex turns out to be virtual, so this is a real override, not the name-hiding the old comment claimed. Not compiled - MetaEditor compile pending. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:09:58 -04:00
//--- Classic_Shift is set on these four and nowhere else - see that input's comment for why the AI
//--- signals keep the stock rule.
if(EnableMA)
{
MA.PeriodMA(g_TunedMaPeriod);
MA.Method(g_TunedMaType);
feat(indicators): run the built-in iMA and MetaTrader's ZigZag; add a classic-vote shift MA: CustomIndicators\ADMovingAverage is replaced by the built-in iMA (CiMA) on both consumers - the classic vote and the NN MA input feature. This drops the five advanced types ALMA/DEMA/ZLEMA/T3/Kalman, which have no iMA equivalent; MA_TYPE_PRESETS is now ENUM_MA_METHOD's own codes and the tuner searches all four. It also removes a documented failure mode: a custom indicator's depth is bounded by TERMINAL_MAXBARS, and m_MA was the one whose feature block REJECTS the bar on a short read - the "feature 25 fails on every bar" incident of 2026-08-17. A built-in is served at any depth. MIGRATION. SMA moves from code 5 to 0, so persisted type codes change meaning. SanitizeMaType() is the single validity rule; TunedPeriods records now carry a version field and a v1 record remaps 5..8 -> 0..3, falling back to SMA for a stored advanced type (unrecoverable - old 0..4 are indistinguishable from valid new codes). Existing .nnw files re-key on their own, because MA_Type is hashed into the topology fingerprint, so models retrain rather than silently running on different MA values. EXPECT A FULL RETRAIN. ZigZag: ADZigZag was a byte-identical rename of MetaQuotes' Examples\ZigZag - verified by normalising identifiers and stripping comments, 233 significant lines each with only renamed symbols differing. It now loads the stock one, so nothing is bundled and MetaQuotes' fixes arrive without a rebuild here. Both #resource entries are gone. Classic_Shift: a new input, the BAR the four classic votes evaluate on (0 = forming, 1 = last closed, default 1). One implementation on CExpertSignalCustom, inherited by all four rather than repeated per module. Defaults to a sentinel meaning "unset", so the AI signals and the aggregate keep the stock every_tick rule and their feature/label alignment is untouched. The META corpus sweep still takes precedence. CExpertBase::StartIndex turns out to be virtual, so this is a real override, not the name-hiding the old comment claimed. Not compiled - MetaEditor compile pending. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:09:58 -04:00
MA.Shift(Classic_Shift);
if(!UseDatabaseRanking)
MA.Weight(1);
}
if(EnableRSI)
{
RSI.PeriodRSI(g_TunedRsiPeriod);
feat(indicators): run the built-in iMA and MetaTrader's ZigZag; add a classic-vote shift MA: CustomIndicators\ADMovingAverage is replaced by the built-in iMA (CiMA) on both consumers - the classic vote and the NN MA input feature. This drops the five advanced types ALMA/DEMA/ZLEMA/T3/Kalman, which have no iMA equivalent; MA_TYPE_PRESETS is now ENUM_MA_METHOD's own codes and the tuner searches all four. It also removes a documented failure mode: a custom indicator's depth is bounded by TERMINAL_MAXBARS, and m_MA was the one whose feature block REJECTS the bar on a short read - the "feature 25 fails on every bar" incident of 2026-08-17. A built-in is served at any depth. MIGRATION. SMA moves from code 5 to 0, so persisted type codes change meaning. SanitizeMaType() is the single validity rule; TunedPeriods records now carry a version field and a v1 record remaps 5..8 -> 0..3, falling back to SMA for a stored advanced type (unrecoverable - old 0..4 are indistinguishable from valid new codes). Existing .nnw files re-key on their own, because MA_Type is hashed into the topology fingerprint, so models retrain rather than silently running on different MA values. EXPECT A FULL RETRAIN. ZigZag: ADZigZag was a byte-identical rename of MetaQuotes' Examples\ZigZag - verified by normalising identifiers and stripping comments, 233 significant lines each with only renamed symbols differing. It now loads the stock one, so nothing is bundled and MetaQuotes' fixes arrive without a rebuild here. Both #resource entries are gone. Classic_Shift: a new input, the BAR the four classic votes evaluate on (0 = forming, 1 = last closed, default 1). One implementation on CExpertSignalCustom, inherited by all four rather than repeated per module. Defaults to a sentinel meaning "unset", so the AI signals and the aggregate keep the stock every_tick rule and their feature/label alignment is untouched. The META corpus sweep still takes precedence. CExpertBase::StartIndex turns out to be virtual, so this is a real override, not the name-hiding the old comment claimed. Not compiled - MetaEditor compile pending. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:09:58 -04:00
RSI.Shift(Classic_Shift);
if(!UseDatabaseRanking)
RSI.Weight(1);
}
if(EnableMACD)
{
MACD.PeriodFast(g_TunedMacdFast);
MACD.PeriodSlow(g_TunedMacdSlow);
MACD.PeriodSignal(g_TunedMacdSignal);
feat(indicators): run the built-in iMA and MetaTrader's ZigZag; add a classic-vote shift MA: CustomIndicators\ADMovingAverage is replaced by the built-in iMA (CiMA) on both consumers - the classic vote and the NN MA input feature. This drops the five advanced types ALMA/DEMA/ZLEMA/T3/Kalman, which have no iMA equivalent; MA_TYPE_PRESETS is now ENUM_MA_METHOD's own codes and the tuner searches all four. It also removes a documented failure mode: a custom indicator's depth is bounded by TERMINAL_MAXBARS, and m_MA was the one whose feature block REJECTS the bar on a short read - the "feature 25 fails on every bar" incident of 2026-08-17. A built-in is served at any depth. MIGRATION. SMA moves from code 5 to 0, so persisted type codes change meaning. SanitizeMaType() is the single validity rule; TunedPeriods records now carry a version field and a v1 record remaps 5..8 -> 0..3, falling back to SMA for a stored advanced type (unrecoverable - old 0..4 are indistinguishable from valid new codes). Existing .nnw files re-key on their own, because MA_Type is hashed into the topology fingerprint, so models retrain rather than silently running on different MA values. EXPECT A FULL RETRAIN. ZigZag: ADZigZag was a byte-identical rename of MetaQuotes' Examples\ZigZag - verified by normalising identifiers and stripping comments, 233 significant lines each with only renamed symbols differing. It now loads the stock one, so nothing is bundled and MetaQuotes' fixes arrive without a rebuild here. Both #resource entries are gone. Classic_Shift: a new input, the BAR the four classic votes evaluate on (0 = forming, 1 = last closed, default 1). One implementation on CExpertSignalCustom, inherited by all four rather than repeated per module. Defaults to a sentinel meaning "unset", so the AI signals and the aggregate keep the stock every_tick rule and their feature/label alignment is untouched. The META corpus sweep still takes precedence. CExpertBase::StartIndex turns out to be virtual, so this is a real override, not the name-hiding the old comment claimed. Not compiled - MetaEditor compile pending. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:09:58 -04:00
MACD.Shift(Classic_Shift);
if(!UseDatabaseRanking)
MACD.Weight(1);
}
if(EnableIchimoku)
{
Ichimoku.PeriodTenkan(g_TunedIchiTenkan);
Ichimoku.PeriodKijun(g_TunedIchiKijun);
Ichimoku.PeriodSenkou(g_TunedIchiSenkou);
feat(indicators): run the built-in iMA and MetaTrader's ZigZag; add a classic-vote shift MA: CustomIndicators\ADMovingAverage is replaced by the built-in iMA (CiMA) on both consumers - the classic vote and the NN MA input feature. This drops the five advanced types ALMA/DEMA/ZLEMA/T3/Kalman, which have no iMA equivalent; MA_TYPE_PRESETS is now ENUM_MA_METHOD's own codes and the tuner searches all four. It also removes a documented failure mode: a custom indicator's depth is bounded by TERMINAL_MAXBARS, and m_MA was the one whose feature block REJECTS the bar on a short read - the "feature 25 fails on every bar" incident of 2026-08-17. A built-in is served at any depth. MIGRATION. SMA moves from code 5 to 0, so persisted type codes change meaning. SanitizeMaType() is the single validity rule; TunedPeriods records now carry a version field and a v1 record remaps 5..8 -> 0..3, falling back to SMA for a stored advanced type (unrecoverable - old 0..4 are indistinguishable from valid new codes). Existing .nnw files re-key on their own, because MA_Type is hashed into the topology fingerprint, so models retrain rather than silently running on different MA values. EXPECT A FULL RETRAIN. ZigZag: ADZigZag was a byte-identical rename of MetaQuotes' Examples\ZigZag - verified by normalising identifiers and stripping comments, 233 significant lines each with only renamed symbols differing. It now loads the stock one, so nothing is bundled and MetaQuotes' fixes arrive without a rebuild here. Both #resource entries are gone. Classic_Shift: a new input, the BAR the four classic votes evaluate on (0 = forming, 1 = last closed, default 1). One implementation on CExpertSignalCustom, inherited by all four rather than repeated per module. Defaults to a sentinel meaning "unset", so the AI signals and the aggregate keep the stock every_tick rule and their feature/label alignment is untouched. The META corpus sweep still takes precedence. CExpertBase::StartIndex turns out to be virtual, so this is a real override, not the name-hiding the old comment claimed. Not compiled - MetaEditor compile pending. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:09:58 -04:00
Ichimoku.Shift(Classic_Shift);
if(!UseDatabaseRanking)
Ichimoku.Weight(1);
}
if(EnableNewsFilter)
{
newsFilter.SetMinImpact(NF_MinImpact);
newsFilter.SetLookbackMinutes(NF_LookMinutes);
}
if(EnablePAI)
ConfigureAISignal(PAI);
if(EnableCONV)
ConfigureAISignal(CONV);
if(EnableLSTM)
ConfigureAISignal(LSTM);
if(EnableHYBRID)
ConfigureAISignal(HYBRID);
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus The NN now has a target that is not per-bar direction (closed, best-of-999 p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of cost). One net for all 52 pattern-sides, AIType=AI_META. - NetForward.mqh: the host-side softmax+CE gradient generalized total==3 -> 2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no compute backend changes. - SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on disk (decoupled from the config fingerprint that burned four S1 runs); the GMT->server offset is measured PER ROW against entryPrice vs bar open (DST-immune, histogram logged); a window-span regime filter drops the pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR). - Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell lets checkpoint selection, the edge floor, the plateau ladder and the family-wise deploy gate run UNCHANGED: precision reads as win rate among traded candidates, chance as the base win rate, recalls as sensitivity/ specificity. Era-end META line: coverage x (p - break-even) vs the null. - Labels are the side-conditional triple-barrier win caches - never the DB's stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base rate). Live inference + online learning guarded off until S3. - Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename slot keep meta models fully separate from direction models. Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with AIType=AI_META; S3 wires the votes via the per-side hooks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
if(EnableMETA)
{
ConfigureAISignal(META);
//--- AFTER the shared config (which stamps the 1/3-output const input): the meta head is a
//--- 2-output binary softmax - "did this candidate's trade win" - and both the topology and the
//--- weights-filename fingerprint key off this value. See Meta_Labeling_Design.md S2.
META.OutputNeuronsCount(2);
feat(meta): self-contained corpus - the META chart sweeps the real classic ladders over its own history The user should not need a tester corpus run per symbol. Every pattern condition in Signals\Signal{MA,RSI,MACD,Ichimoku}.mqh anchors its reads on `int idx = StartIndex()` with zero hardcoded indices (verified), so a name-hiding StartIndex override + EvalShift(i) on CExpertSignalCustom makes the EXACT live ladder code answer "what would you have fired at bar i" - the silent-divergence trap that justified the DB corpus does not exist on this path, and neither do the GMT-offset ambiguity, the DB row caps, or the wipe procedure. - CExpertSignalCustom: m_evalShift + StartIndex()/EvalShift() + SweepPrepare(bars) (deep-resizes the shared price series); the four classic signal classes override SweepPrepare to deep-resize their own indicator buffers. - CSignalMETA::BuildCorpusBySweep: per bar x per source filter, run Direction() shifted, harvest the per-side pattern slots + netVote into the same corpus arrays the DB loader fills; entry=bar open so MetaPrepareEra's resolution matches at offset +0 with zero price error. DB corpus remains the fallback when classic filters are disabled. - Warrior_EA.mq5: META gets the enabled classic filters as candidate sources (family ids match the descriptor one-hot). - UseDatabaseRanking default false -> true (user request): a META chart journals + ranks out of the box. Workflow per symbol is now: attach ONE chart with AIType=META (optionally Meta_ExportDataset=true for the offline pool) - candidates, labels, training and export all happen in place, ~10 seconds of sweep instead of a tester run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 16:19:43 -04:00
//--- candidate sources for the on-chart ladder sweep (BuildCorpusBySweep): the meta corpus is
//--- generated by evaluating these real filters over this chart's own history, so a META
//--- chart is fully self-contained - no tester corpus run.
feat(meta): self-contained corpus - the META chart sweeps the real classic ladders over its own history The user should not need a tester corpus run per symbol. Every pattern condition in Signals\Signal{MA,RSI,MACD,Ichimoku}.mqh anchors its reads on `int idx = StartIndex()` with zero hardcoded indices (verified), so a name-hiding StartIndex override + EvalShift(i) on CExpertSignalCustom makes the EXACT live ladder code answer "what would you have fired at bar i" - the silent-divergence trap that justified the DB corpus does not exist on this path, and neither do the GMT-offset ambiguity, the DB row caps, or the wipe procedure. - CExpertSignalCustom: m_evalShift + StartIndex()/EvalShift() + SweepPrepare(bars) (deep-resizes the shared price series); the four classic signal classes override SweepPrepare to deep-resize their own indicator buffers. - CSignalMETA::BuildCorpusBySweep: per bar x per source filter, run Direction() shifted, harvest the per-side pattern slots + netVote into the same corpus arrays the DB loader fills; entry=bar open so MetaPrepareEra's resolution matches at offset +0 with zero price error. DB corpus remains the fallback when classic filters are disabled. - Warrior_EA.mq5: META gets the enabled classic filters as candidate sources (family ids match the descriptor one-hot). - UseDatabaseRanking default false -> true (user request): a META chart journals + ranks out of the box. Workflow per symbol is now: attach ONE chart with AIType=META (optionally Meta_ExportDataset=true for the offline pool) - candidates, labels, training and export all happen in place, ~10 seconds of sweep instead of a tester run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 16:19:43 -04:00
if(EnableMA)
META.AddCandidateSource(MA, 0);
if(EnableRSI)
META.AddCandidateSource(RSI, 1);
if(EnableMACD)
META.AddCandidateSource(MACD, 2);
if(EnableIchimoku)
META.AddCandidateSource(Ichimoku, 3);
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus The NN now has a target that is not per-bar direction (closed, best-of-999 p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of cost). One net for all 52 pattern-sides, AIType=AI_META. - NetForward.mqh: the host-side softmax+CE gradient generalized total==3 -> 2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no compute backend changes. - SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on disk (decoupled from the config fingerprint that burned four S1 runs); the GMT->server offset is measured PER ROW against entryPrice vs bar open (DST-immune, histogram logged); a window-span regime filter drops the pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR). - Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell lets checkpoint selection, the edge floor, the plateau ladder and the family-wise deploy gate run UNCHANGED: precision reads as win rate among traded candidates, chance as the base win rate, recalls as sensitivity/ specificity. Era-end META line: coverage x (p - break-even) vs the null. - Labels are the side-conditional triple-barrier win caches - never the DB's stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base rate). Live inference + online learning guarded off until S3. - Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename slot keep meta models fully separate from direction models. Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with AIType=AI_META; S3 wires the votes via the per-side hooks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
}
// Add filters
PrintVerbose("Initializing Signal filters...");
//--- added exactly once, before the DB retry loop below - these calls don't depend on DB success at
//--- all (every pointer here was already validated non-NULL above), but living inside the loop body
//--- meant a DB open/transaction failure that triggered a retry would re-run AddFilterToSignal() and
//--- register the same filter pointer a second time in signal's CArrayObj; since that array frees its
//--- elements on destruction, a duplicate entry means the same pointer gets deleted twice on shutdown
//--- (heap corruption), which could easily explain instability across a later remove/re-add cycle.
bool filtersAdded = true;
filtersAdded &= (EnableMA ? AddFilterToSignal(signal, MA) : true);
filtersAdded &= (EnableRSI ? AddFilterToSignal(signal, RSI) : true);
filtersAdded &= (EnableMACD ? AddFilterToSignal(signal, MACD) : true);
filtersAdded &= (EnableIchimoku ? AddFilterToSignal(signal, Ichimoku) : true);
filtersAdded &= (EnableSessionFilter ? AddFilterToSignal(signal, sessionFilter) : true);
filtersAdded &= (EnableNewsFilter ? AddFilterToSignal(signal, newsFilter) : true);
filtersAdded &= (EnableRiskGuard ? AddFilterToSignal(signal, riskGuard) : true);
filtersAdded &= (EnablePAI ? AddFilterToSignal(signal, PAI) : true);
filtersAdded &= (EnableCONV ? AddFilterToSignal(signal, CONV) : true);
filtersAdded &= (EnableLSTM ? AddFilterToSignal(signal, LSTM) : true);
filtersAdded &= (EnableHYBRID ? AddFilterToSignal(signal, HYBRID) : true);
feat: S2 meta-labeling head - binary trade-quality model over the classic-candidate corpus The NN now has a target that is not per-bar direction (closed, best-of-999 p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of cost). One net for all 52 pattern-sides, AIType=AI_META. - NetForward.mqh: the host-side softmax+CE gradient generalized total==3 -> 2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no compute backend changes. - SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on disk (decoupled from the config fingerprint that burned four S1 runs); the GMT->server offset is measured PER ROW against entryPrice vs bar open (DST-immune, histogram logged); a window-span regime filter drops the pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR). - Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell lets checkpoint selection, the edge floor, the plateau ladder and the family-wise deploy gate run UNCHANGED: precision reads as win rate among traded candidates, chance as the base win rate, recalls as sensitivity/ specificity. Era-end META line: coverage x (p - break-even) vs the null. - Labels are the side-conditional triple-barrier win caches - never the DB's stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base rate). Live inference + online learning guarded off until S3. - Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename slot keep meta models fully separate from direction models. Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with AIType=AI_META; S3 wires the votes via the per-side hooks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 06:52:31 -04:00
//--- META registers like any AI filter so it gets InitIndicators + training scheduling; its votes
//--- are structurally 0 in S2 (dPrevSignal never leaves the sentinel - see SignalMETA.mqh header).
filtersAdded &= (EnableMETA ? AddFilterToSignal(signal, META) : true);
if(!filtersAdded)
{
Print(functionName + ": Error loading filters");
return INIT_FAILED;
}
bool filterSuccess = false;
for(int tries = 0; tries < maxRetryOnError; ++tries)
{
if(UseDatabaseRanking && !dbm.OpenDatabase())
{
Print(functionName + ": Error opening database, retrying...");
RandomSleep();
continue;
}
if(UseDatabaseRanking && !dbm.BeginTransaction())
{
Print(functionName + ": Error starting transaction, retrying...");
dbm.CloseDatabase(); // Ensure the database is closed before retry
RandomSleep();
continue;
}
if(UseDatabaseRanking && (!dbm.CommitTransaction() || !dbm.CloseDatabase()))
{
Print(functionName + ": Error committing transaction or closing database, retrying...");
RandomSleep();
continue;
}
filterSuccess = true;
break; // Success if all operations complete without error
}
if(!filterSuccess)
{
Print(functionName + ": Failed after all retries");
return INIT_FAILED; // Return failure if retries are exhausted
}
// Trailing logic
PrintVerbose("Initializing Trailing...");
if(!RetryInitStep(StepInitTrailing, "initialize Trailing", maxRetryOnError, functionName))
return INIT_FAILED;
// Creation of money object
if(!RetryInitStep(StepInitMoneyManagement, "initialize Money Management", maxRetryOnError, functionName))
return INIT_FAILED;
// Check all trading objects parameters
PrintVerbose("Validating settings...");
if(!RetryInitStep(StepValidateSettings, "validate settings", maxRetryOnError, functionName))
return INIT_FAILED;
// Tuning of all necessary indicators
PrintVerbose("Initializing Indicators...");
if(!RetryInitStep(StepInitIndicators, "initialize Indicators", maxRetryOnError, functionName))
return INIT_FAILED;
//--- setting timer: always on (short interval) so control-panel upkeep and other periodic checks
//--- run on a fixed schedule regardless of tick activity - a quiet/after-hours symbol can go long
//--- stretches without a single OnTick() call, and self-healing logic that only lives in OnTick()
//--- would never run during that stretch.
if(UseDatabaseRanking)
Expert.OnTimerProcess(true);
//--- Interval and the reasoning behind it: WARRIOR_TIMER_INTERVAL_MS, beside StepSetTimer().
if(!RetryInitStep(StepSetTimer, "set the timer", maxRetryOnError, functionName))
return INIT_FAILED;
// Initialization successful
PrintVerbose("Initialization successful");
if(!CreateControlPanel())
Print(functionName + ": WARNING - control panel failed to initialize; trading/training continue normally, "
"but no GUI panel will be available for this run");
//--- one-time cleanup: an earlier build used Comment() plus a separate background rectangle object
//--- that turned out to render ON TOP of the text (Comment() has no built-in background/styling
//--- parameters at all) - delete any leftover from a prior run now that status text is a single
//--- self-contained OBJ_LABEL (see SetStatusLabel()) with its own BGCOLOR fill instead
if(ObjectFind(0, "WarriorCommentBG") >= 0)
ObjectDelete(0, "WarriorCommentBG");
//--- required for CAppDialog's caption-bar drag to work at all - without it, the chart never delivers
//--- CHARTEVENT_MOUSE_MOVE and the panel silently ignores drag attempts
ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);
g_lastAlgoTradingAllowed = (bool)TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && (bool)MQLInfoInteger(MQL_TRADE_ALLOWED);
if(!g_lastAlgoTradingAllowed)
Print(functionName + ": WARNING - AlgoTrading is currently disabled (terminal or EA); signals will still train but no orders will be sent until it is re-enabled");
return INIT_SUCCEEDED;
}
// Called before optimization/backtesting in the strategy tester
int OnTesterInit()
{
IsBacktesting = true;
return(INIT_SUCCEEDED);
}
// Called after EA optimization in the strategy tester
void OnTesterDeinit()
{
//--- OnTesterInit/OnTesterDeinit fire once per tester session, even for a single (non-
//--- optimization) backtest - and by the time this runs, that pass's own OnDeinit() has ALREADY
//--- executed (the terminal calls it automatically at the end of every pass), including its own
//--- dbm.Deinit().
IsBacktesting = false;
}
//+------------------------------------------------------------------+
//| Tester function for smooth linear equity optimization |
//| Output Range: 0.0 (Worst/Failed) to 100.0 (Perfect Linear Curve) |
//+------------------------------------------------------------------+
double OnTester()
{
// 1. Enforce minimum performance thresholds
double totalTrades = TesterStatistics(STAT_TRADES);
if(totalTrades < 30) return(0.0);
double netProfit = TesterStatistics(STAT_PROFIT);
if(netProfit <= 0) return(0.0);
double maxDrawdownPct = TesterStatistics(STAT_EQUITY_DDREL_PERCENT);
if(maxDrawdownPct > 15.0) return(0.0);
// 2. Extract key performance components for linearity proxy
double profitFactor = TesterStatistics(STAT_PROFIT_FACTOR);
double recoveryFactor = TesterStatistics(STAT_RECOVERY_FACTOR);
double sharpeRatio = TesterStatistics(STAT_SHARPE_RATIO);
if(profitFactor <= 0 || recoveryFactor <= 0) return(0.0);
if(sharpeRatio < 0) sharpeRatio = 0.01;
// 3. Trade density multiplier
double tradeDensity = 1.0 - MathExp(-0.01 * (double)totalTrades);
// 4. Mathematical combination proxy targeting visual linearity
double rawScore = profitFactor * recoveryFactor * sharpeRatio;
// FIX: Convert percent to decimal (divide by 100) before penalization
double drawdownDecimal = maxDrawdownPct / 100.0;
rawScore /= (1.0 + (drawdownDecimal * 0.5));
// Apply trade density
rawScore *= tradeDensity;
// 5. Normalize the score to a 0.0 - 100.0 scale
double finalScore = 100.0 * (1.0 - MathExp(-0.12 * rawScore));
if(finalScore > 100.0) finalScore = 100.0;
if(finalScore < 0.0) finalScore = 0.0;
return(finalScore);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Maps a terminal deinit reason code to a short label for logging, |
//| so operators can tell a routine recompile/parameter change apart |
//| from a terminal shutdown or the EA actually being removed. |
//+------------------------------------------------------------------+
string DeinitReasonToString(const int reason)
{
switch(reason)
{
case REASON_PROGRAM: return "EA stopped by ExpertRemove()/self";
case REASON_REMOVE: return "EA removed from chart";
case REASON_RECOMPILE: return "EA recompiled";
case REASON_CHARTCHANGE: return "chart symbol/period changed";
case REASON_CHARTCLOSE: return "chart closed";
case REASON_PARAMETERS: return "input parameters changed";
case REASON_ACCOUNT: return "account changed";
case REASON_TEMPLATE: return "template applied";
case REASON_INITFAILED: return "OnInit() failed";
case REASON_CLOSE: return "terminal closed";
default: return "unknown (" + IntegerToString(reason) + ")";
}
}
void OnDeinit(const int reason)
{
static bool s_deinitInProgress = false;
if(s_deinitInProgress)
return;
s_deinitInProgress = true;
// Stop timer callbacks first so no more periodic work is queued while teardown runs.
EventKillTimer();
string reasonStr = DeinitReasonToString(reason);
Print(__FUNCTION__ + ": shutting down - reason: " + reasonStr);
bool isTesterRun = (MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD));
if(!isTesterRun)
SaveSignalsVisibilityState();
//--- Clear the VISIBLE EA objects FIRST and fast - status label, then control panel, then arrows -
//--- BEFORE the slow/fragile weight save below. Only then the heavy persistence runs, best-effort,
//--- last.
ClearStatusLabel();
//--- EARLY VISIBLE-UI SWEEP, 2026-08-16.
fix(ensemble): responsive panel + synchronized eras + combined-vote accuracy Four user-reported/requested items, one root cause chain: 1) DEAD CONTROL PANEL in AI_HYBRID mode. All members posted custom event id 1 and handled id 1001, and CExpertCustom broadcasts every chart event to every filter - so each posted event ran a train chunk in ALL N members (N*N chunks per round) and the chart thread never idled long enough to deliver clicks/drags. profiling.csv: 99.45% of time in OnChartEventHandler. Fix: per-instance study-event ids (STUDY_EVENT_ID_BASE + construction order, offset above the Controls library's ON_* codes - id 1 was also ON_DBL_CLICK, so panel double-clicks fired training chunks). ArmStudyEvent() is the single post site; lost-event watchdog replaces the accidental sibling-clears-my-flag rescue. 2) WARM-UP DUPLICATION. The auto-tune sweep is deterministic over identical features/labels, and it ends in the full MI diagnostic suite, which the MI-share gate never intercepted on the sweep path - four members ran four identical ~36s sweep+report blocks. First member publishes outcome (g_ensembleChartTuneDone/Installed/Settings); the rest apply it and skip both. 3) DEINIT STRANDED PANEL+ARROWS (user repro 18:52). Root cause from the log: the 4,500ms budget runs from MetaTrader's stop REQUEST - a heavy autosave in flight ate it, OnDeinit got ~430ms and died in the first member's arrow persist ("Abnormal termination" 432ms in). Fix: early visible-UI sweep (native prefix deletes for status/panel/dialog) right after ClearStatusLabel, and a fast path for still-training models - their arrows are re-rendered every era, so they get one bulk purge instead of scan+atomic-write in the death window. 4) ENSEMBLE FEATURES (user requests): era BARRIER - members advance era by era together; a member ahead of the slowest still-training member declines Train() calls and its chunk budget is donated (TRAIN_TIME_BUDGET_MS = 120/activeTrainers, UI headroom constant). COMBINED-VOTE OOS SCORE - each member's pass-3 scan contributes its adjusted per-bar decision (0.0 on abstain) to a shared row buffer; the last member to finish the era scores the averaged vote vs the mirrored Min_Vote_Open against the same target-before-stop outcomes members grade themselves on, publishing an "Ensemble vote" line on the aggregated panel. Member headlines now carry their lifetime win rate with break-even. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:06:04 -04:00
int earlySweepLeft = 0;
WarriorPurgeChartObjects(0, true, earlySweepLeft);
//--- ORDER, 2026-08-09: the arrow purge now runs BEFORE ExtPanel.Destroy(), not after. That 4.8 s
//--- is MetaTrader's OnDeinit budget expiring, not a fault: nothing threw, the process simply ran
//--- out of time in a step ahead of the cleanup.
fix: the Adam second moment was never Adam - all four tiers Root cause of the B=32 regression, and it predates F4 entirely. Every Adam kernel stored v already square-rooted and then fed that stored value back in as if it were the variance: v_new = sqrt(b2 * v_old + (1 - b2) * g^2) That recursion has a fixed point at v ~= b2 = 0.999 for ANY gradient below unit scale, so the denominator stops tracking the gradient and Adam degrades into plain SGD with lr = lt. Measured against the shipped WarriorCPU.dll (batch_accum_check.cpp, TestOptimizerScaleInvariance), 4000 steps of a constant gradient: 3285x less displacement at |g|=1e-5 than at |g|=1, where a scale-invariant optimizer gives the same distance for both. After the fix all six magnitudes read 1.199 and v tracks |g| exactly. It hit conv/LSTM specifically because they sit behind a batch-norm with running variance ~2.6e+05, so their gradients arrive divided by ~500 - deep in the degraded regime - while the dense stack near the loss stayed in the working one. In situ on SP500 H1: lstm1 dW/W 2.62/10.0/7.14% -> 0.024/0.022/ 0.003%, conv1 decaying to 0.000% by era 30. NeuronBatchNorm.mqh already squared v back for gamma/beta and its comment named the kernels as wrong, which is exactly why gamma/beta kept training while the stages behind froze. Persisted .nnw needs no migration - v keeps its std-dev meaning. Also, the two ways F4 exposed it, both mine: - No LR compensation for B fewer steps per era. sqrt(B) for adaptive methods (Krizhevsky 2014; Granziol et al. 2022), applied once in InitialEtaForOptimizer(). Linear scaling (Goyal et al. 2017) is for SGD. - Plateau patience denominated in eras, so raising B made the ladder 32x more impatient in its only unit. PAI converged at era 41 on ~49k updates where the same config had been finding new bests at era 1028. TrainPlateauPatienceEras() stretches it by the same sqrt(B). TRAIN_BATCH_SIZE 32 -> 8 so the patience stretch stays affordable (8 -> 23 eras per stage, not 8 -> 45). Both helpers are identities at B=1. Deploy gate: DEPLOY_MIN_SIDE_RECALL_PCT (10%) folded into tradeableOK. The perceptron reported Sell:0% recall in all 41 eras, cleared the floor on Buy alone at 36.6% vs 34% chance, deployed, and sprayed buy arrows. Folded into the ranking key rather than checked at deploy time so a one-sided era cannot become best-so-far in the first place. Deinit: the arrow purge now runs BEFORE ExtPanel.Destroy(), an unbounded CAppDialog teardown that sat ahead of it - the same ordering inversion the rule there exists to prevent. CONV was force-terminated 4.8 s into OnDeinit (vs ~1.1 s for the three that finished) having reached none of its cleanup, so its arrows stayed on the chart. Steps are now timed in the log. PurgeChart's verification rescan filtered on OBJ_ARROW, the same blind spot as the bulk delete, so "persisted 10 ... cleared 0" passed silently. It now walks every object type and reports the object counts when both are zero. Both build variants compile 0 errors / 0 warnings; both DLLs rebuilt. FORCES A RETRAIN (already forced by N1) and both DLLs must ship with the .ex5. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:02:35 -04:00
ulong deinitT0 = GetMicrosecondCount();
fix(deinit): a full model write was running ahead of the cheap cleanup "Abnormal termination" is back, and this time it is not the arrows. The timing names the culprit exactly: 16:02:31.547 OnDeinit: shutting down 16:02:36.003 Abnormal termination <- 4.46 s, MetaTrader gave up 16:02:36.226 chart signals - persisted <- cleanup finished 0.2 s LATE OnDeinit called StopTraining() BEFORE the chart cleanup. StopTraining() finalises an in-flight run, and FinalizeTrainRun() restores the best checkpoint and then persists it - a full ~1MB model write per signal. So the expensive step ran ahead of the cheap bounded one, which is precisely the inversion the shutdown ordering exists to prevent. The previous fix put PersistWeightsOnShutdown last and missed that StopTraining smuggles a second save in at the front. Two changes: Cleanup now runs FIRST, then StopTraining, then the weight save. The visible teardown is cheap and bounded, so it always completes even when everything after it is killed. And the deploy-persist inside FinalizeTrainRun is suppressed during shutdown. RestoreWeights() is an in-MEMORY swap, so the best checkpoint is already the live net by that line, and PersistWeightsOnShutdown writes exactly those weights moments later. The old path wrote the same model twice per signal - eight full writes across four charts - for no benefit. A user-pressed Stop still persists immediately, because nothing else would. Compiles 0 errors / 0 warnings. Build tag deinit-order-v2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 16:06:40 -04:00
for(int i = 0; i < g_aiSignalCount; i++)
g_aiSignals[i].MarkShutdown();
for(int i = 0; i < g_aiSignalCount; i++)
//--- Persist the drawn arrows to their sidecar and take them off the chart, on EVERY deinit
//--- reason. They come back on the next attach, from disk, if a model for that config exists -
//--- see PersistAndClearChartSignals().
fix(chart): arrows survived the EA that drew them - persist, then clear Reported: on deinit the panel and status label go, the signal arrows stay. Two independent causes, both fixed here. 1. It was partly deliberate. ShutdownChartCleanup carried a second behaviour selected by a `preserveChartArrows` flag derived from the deinit reason: on RECOMPILE / PARAMETERS / CHARTCHANGE / TEMPLATE the arrows were left on the chart on purpose, to avoid a reload flicker. That branch IS the reported symptom, an operator cannot tell it apart from a cleanup that failed, and it was outright wrong whenever the reload changed the config - REASON_PARAMETERS means exactly that, and the preserved arrows then belonged to a model the chart no longer runs, with nothing marking them stale. It is gone, along with the flag and m_purgeChartOnDestruct. One path now: persist, clear, restore on the next attach. 2. Whatever remains was unfalsifiable. PurgeChart was a single ObjectsDeleteAll(prefix) whose return value was discarded, with no caller ever looking at the chart again - so "the arrows are still there" and "the arrows were never there" produced identical evidence, which is why the report survived three sessions. It now verifies: after the bulk delete it walks the OBJ_ARROW-typed list (a handful of objects, not the whole chart), deletes any surviving WarSig_ by name, and says so. Costs one typed scan when the bulk delete works, which is the normal case; names the root cause when it does not. Every failure mode of SaveChartSignals was also silent - it returned void and had three bare early returns. It returns bool now, logs the open error with the filename, and the shutdown purge is CONDITIONAL on it: for a converged model the chart objects are the only copy of its signal history (nothing redraws them - the renderer runs per training era and a deployed model has none left), so a chart left littered because the disk write failed beats a clean chart bought by destroying the history. Either way the log now says which happened. Also states the user's rule once, where arrows come back rather than across InitNeuralNetwork's several exits: no weights loaded for this config => clear the sidecar and start visually clean. A fresh run must not inherit calls it never made, and the first save would otherwise adopt them (the sidecar is rebuilt by scanning the chart). Compiles 0 errors / 0 warnings, standard and Market. Needs redeploy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 13:28:34 -04:00
g_aiSignals[i].ShutdownChartCleanup();
fix: the Adam second moment was never Adam - all four tiers Root cause of the B=32 regression, and it predates F4 entirely. Every Adam kernel stored v already square-rooted and then fed that stored value back in as if it were the variance: v_new = sqrt(b2 * v_old + (1 - b2) * g^2) That recursion has a fixed point at v ~= b2 = 0.999 for ANY gradient below unit scale, so the denominator stops tracking the gradient and Adam degrades into plain SGD with lr = lt. Measured against the shipped WarriorCPU.dll (batch_accum_check.cpp, TestOptimizerScaleInvariance), 4000 steps of a constant gradient: 3285x less displacement at |g|=1e-5 than at |g|=1, where a scale-invariant optimizer gives the same distance for both. After the fix all six magnitudes read 1.199 and v tracks |g| exactly. It hit conv/LSTM specifically because they sit behind a batch-norm with running variance ~2.6e+05, so their gradients arrive divided by ~500 - deep in the degraded regime - while the dense stack near the loss stayed in the working one. In situ on SP500 H1: lstm1 dW/W 2.62/10.0/7.14% -> 0.024/0.022/ 0.003%, conv1 decaying to 0.000% by era 30. NeuronBatchNorm.mqh already squared v back for gamma/beta and its comment named the kernels as wrong, which is exactly why gamma/beta kept training while the stages behind froze. Persisted .nnw needs no migration - v keeps its std-dev meaning. Also, the two ways F4 exposed it, both mine: - No LR compensation for B fewer steps per era. sqrt(B) for adaptive methods (Krizhevsky 2014; Granziol et al. 2022), applied once in InitialEtaForOptimizer(). Linear scaling (Goyal et al. 2017) is for SGD. - Plateau patience denominated in eras, so raising B made the ladder 32x more impatient in its only unit. PAI converged at era 41 on ~49k updates where the same config had been finding new bests at era 1028. TrainPlateauPatienceEras() stretches it by the same sqrt(B). TRAIN_BATCH_SIZE 32 -> 8 so the patience stretch stays affordable (8 -> 23 eras per stage, not 8 -> 45). Both helpers are identities at B=1. Deploy gate: DEPLOY_MIN_SIDE_RECALL_PCT (10%) folded into tradeableOK. The perceptron reported Sell:0% recall in all 41 eras, cleared the floor on Buy alone at 36.6% vs 34% chance, deployed, and sprayed buy arrows. Folded into the ranking key rather than checked at deploy time so a one-sided era cannot become best-so-far in the first place. Deinit: the arrow purge now runs BEFORE ExtPanel.Destroy(), an unbounded CAppDialog teardown that sat ahead of it - the same ordering inversion the rule there exists to prevent. CONV was force-terminated 4.8 s into OnDeinit (vs ~1.1 s for the three that finished) having reached none of its cleanup, so its arrows stayed on the chart. Steps are now timed in the log. PurgeChart's verification rescan filtered on OBJ_ARROW, the same blind spot as the bulk delete, so "persisted 10 ... cleared 0" passed silently. It now walks every object type and reports the object counts when both are zero. Both build variants compile 0 errors / 0 warnings; both DLLs rebuilt. FORCES A RETRAIN (already forced by N1) and both DLLs must ship with the .ex5. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:02:35 -04:00
ulong deinitTArrows = GetMicrosecondCount();
//--- Destroy the control panel's own UI so CAppDialog removes its own objects cleanly (see
//--- Controls\Dialog.mqh). After the arrow purge now - see the ordering note above.
if(!isTesterRun)
feat(altdata): EIA wired, 24-instrument symbol catalog, mapping dialog for unknown symbols EIA (user directive: "the NN might find patterns in it for both oil and regular symbols"). Weekly Petroleum Status Report via the v2 API - crude stocks ex-SPR, field production, refinery utilization - three features (1y percentile, 4w change, utilization) on EVERY catalog symbol, not just oil. EIA screened NULL on WTI's short 7y sample, so these ship as EXPLORATORY inputs: the deploy gate, not the screen, decides whether a model trained on them trades. Publication stamp observed+6d mirrors research/altdata/eia.py. Symbol handling was hardcoded to three if-blocks; it is now a catalog of 24 instruments x alias lists covering The5ers/FTMO/AvaTrade/Dukascopy/OANDA/IC Markets naming, with prefix matching for the broker suffix zoo (US500.cash, XAUUSDm, EURUSD.r). Adding an instrument is one AddSpec row. COT caches are named by CANONICAL so two brokers' names for one contract share a download. Unrecognised symbol -> a chart dialog (Panel\AltDataMapDialog.mqh, CAppDialog + dropdown) asks which instrument it is; the answer persists in symbol_map.cfg and "No alternative data" is a recorded choice, not a nag. Non-blocking by design: an unmapped symbol contributes 0 features and must never hold up a chart. Also: UrlEncodePart now escapes '%' - SoQL like-predicates use it as the wildcard and an unescaped one corrupts the query; docs/ gains the whitelist URLs, an API-key backup, and the catalog reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:18:29 -04:00
{
fix: the Adam second moment was never Adam - all four tiers Root cause of the B=32 regression, and it predates F4 entirely. Every Adam kernel stored v already square-rooted and then fed that stored value back in as if it were the variance: v_new = sqrt(b2 * v_old + (1 - b2) * g^2) That recursion has a fixed point at v ~= b2 = 0.999 for ANY gradient below unit scale, so the denominator stops tracking the gradient and Adam degrades into plain SGD with lr = lt. Measured against the shipped WarriorCPU.dll (batch_accum_check.cpp, TestOptimizerScaleInvariance), 4000 steps of a constant gradient: 3285x less displacement at |g|=1e-5 than at |g|=1, where a scale-invariant optimizer gives the same distance for both. After the fix all six magnitudes read 1.199 and v tracks |g| exactly. It hit conv/LSTM specifically because they sit behind a batch-norm with running variance ~2.6e+05, so their gradients arrive divided by ~500 - deep in the degraded regime - while the dense stack near the loss stayed in the working one. In situ on SP500 H1: lstm1 dW/W 2.62/10.0/7.14% -> 0.024/0.022/ 0.003%, conv1 decaying to 0.000% by era 30. NeuronBatchNorm.mqh already squared v back for gamma/beta and its comment named the kernels as wrong, which is exactly why gamma/beta kept training while the stages behind froze. Persisted .nnw needs no migration - v keeps its std-dev meaning. Also, the two ways F4 exposed it, both mine: - No LR compensation for B fewer steps per era. sqrt(B) for adaptive methods (Krizhevsky 2014; Granziol et al. 2022), applied once in InitialEtaForOptimizer(). Linear scaling (Goyal et al. 2017) is for SGD. - Plateau patience denominated in eras, so raising B made the ladder 32x more impatient in its only unit. PAI converged at era 41 on ~49k updates where the same config had been finding new bests at era 1028. TrainPlateauPatienceEras() stretches it by the same sqrt(B). TRAIN_BATCH_SIZE 32 -> 8 so the patience stretch stays affordable (8 -> 23 eras per stage, not 8 -> 45). Both helpers are identities at B=1. Deploy gate: DEPLOY_MIN_SIDE_RECALL_PCT (10%) folded into tradeableOK. The perceptron reported Sell:0% recall in all 41 eras, cleared the floor on Buy alone at 36.6% vs 34% chance, deployed, and sprayed buy arrows. Folded into the ranking key rather than checked at deploy time so a one-sided era cannot become best-so-far in the first place. Deinit: the arrow purge now runs BEFORE ExtPanel.Destroy(), an unbounded CAppDialog teardown that sat ahead of it - the same ordering inversion the rule there exists to prevent. CONV was force-terminated 4.8 s into OnDeinit (vs ~1.1 s for the three that finished) having reached none of its cleanup, so its arrows stayed on the chart. Steps are now timed in the log. PurgeChart's verification rescan filtered on OBJ_ARROW, the same blind spot as the bulk delete, so "persisted 10 ... cleared 0" passed silently. It now walks every object type and reports the object counts when both are zero. Both build variants compile 0 errors / 0 warnings; both DLLs rebuilt. FORCES A RETRAIN (already forced by N1) and both DLLs must ship with the .ex5. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:02:35 -04:00
ExtPanel.Destroy(reason);
feat(altdata): EIA wired, 24-instrument symbol catalog, mapping dialog for unknown symbols EIA (user directive: "the NN might find patterns in it for both oil and regular symbols"). Weekly Petroleum Status Report via the v2 API - crude stocks ex-SPR, field production, refinery utilization - three features (1y percentile, 4w change, utilization) on EVERY catalog symbol, not just oil. EIA screened NULL on WTI's short 7y sample, so these ship as EXPLORATORY inputs: the deploy gate, not the screen, decides whether a model trained on them trades. Publication stamp observed+6d mirrors research/altdata/eia.py. Symbol handling was hardcoded to three if-blocks; it is now a catalog of 24 instruments x alias lists covering The5ers/FTMO/AvaTrade/Dukascopy/OANDA/IC Markets naming, with prefix matching for the broker suffix zoo (US500.cash, XAUUSDm, EURUSD.r). Adding an instrument is one AddSpec row. COT caches are named by CANONICAL so two brokers' names for one contract share a download. Unrecognised symbol -> a chart dialog (Panel\AltDataMapDialog.mqh, CAppDialog + dropdown) asks which instrument it is; the answer persists in symbol_map.cfg and "No alternative data" is a recorded choice, not a nag. Non-blocking by design: an unmapped symbol contributes 0 features and must never hold up a chart. Also: UrlEncodePart now escapes '%' - SoQL like-predicates use it as the wildcard and an unescaped one corrupts the query; docs/ gains the whitelist URLs, an API-key backup, and the catalog reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:18:29 -04:00
if(g_altMapDialogOpen)
{
g_altMapDialog.Destroy(reason);
g_altMapDialogOpen = false;
}
}
//--- FINAL SWEEP, after every owner-driven teardown has had its turn. Cheap and bounded: three
//--- prefix deletes plus one object-list scan, which is the shape of work this ordering rule
//--- permits at this point. It closes the ordinary case; OnInit closes the case where MetaTrader
//--- never let us finish.
fix: purge every EA object namespace on init and after deinit teardown Leftover objects survived deinit because the cleanup list had drifted. PurgeChart()'s own comment said it removed "our namespaced signal arrows plus the status-label objects" while the code removed arrows ONLY, and the panel prefix was swept at OnInit and nowhere else - so an ordinary deinit left the status line, and any panel straggler, on the chart. Three scattered call sites and a comment cannot be kept in step. There is now ONE list - WarriorChartPrefixes() - covering arrows, status label and panel, and one sweep, WarriorPurgeChartObjects(), used by every path. Add a prefix there when a new object family appears and every cleanup picks it up. Two call sites added: OnInit, before ANYTHING is drawn (including the status label it would otherwise delete). Chart objects live in the chart PROFILE, not in the EA, so they outlive the process: a deinit force-terminated at MetaTrader's ~4,500 ms budget, a crash, a terminal kill, or an .ex5 replaced while attached all strand objects no later deinit will ever own - and deleting the EA's files does not remove them, which is why they read as corruption. Arrows are included: LoadChartSignals restores them from their sidecar moments later and already opens with its own arrow sweep, so this only removes orphans the sidecar does not account for - the ones SaveChartSignals would otherwise ADOPT, since it rebuilds that sidecar by scanning the chart. OnDeinit, after ExtPanel.Destroy. Destroy walks an unbounded control tree and ClearStatusLabel clears text rather than guaranteeing object removal; either can leave a straggler and nothing looked afterwards. Bounded work - three prefix deletes and one object-list scan - so it respects the ordering rule that keeps the cheap visible cleanup ahead of the heavy save. Arrows excluded: ShutdownChartCleanup already persisted and removed them and re-deleting would race that write. The two are complementary: the deinit sweep closes the ordinary case, the OnInit purge closes the case where MetaTrader never let us finish. Only the second can help after a starved shutdown. Both sweeps rescan by name across EVERY object type and delete what the bulk call missed. ObjectsDeleteAll's return has already been observed disagreeing with a by-name scan of the same chart microseconds apart, and object commands are queued on the chart rather than applied inline, so a returned count is not evidence the objects are gone. Panel create site now uses WARRIOR_PANEL_PREFIX instead of a literal, so the name cannot drift away from the list that cleans it up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:19:26 -04:00
int deinitLeftover = 0;
int deinitPurged = WarriorPurgeChartObjects(0, true, deinitLeftover);
if(deinitPurged > 0)
PrintFormat("%s: final sweep removed %d EA object(s) that survived their own teardown%s.",
__FUNCTION__, deinitPurged,
(deinitLeftover > 0
? StringFormat(" (%d needed a by-name delete)", deinitLeftover) : ""));
ChartRedraw(0);
fix: the Adam second moment was never Adam - all four tiers Root cause of the B=32 regression, and it predates F4 entirely. Every Adam kernel stored v already square-rooted and then fed that stored value back in as if it were the variance: v_new = sqrt(b2 * v_old + (1 - b2) * g^2) That recursion has a fixed point at v ~= b2 = 0.999 for ANY gradient below unit scale, so the denominator stops tracking the gradient and Adam degrades into plain SGD with lr = lt. Measured against the shipped WarriorCPU.dll (batch_accum_check.cpp, TestOptimizerScaleInvariance), 4000 steps of a constant gradient: 3285x less displacement at |g|=1e-5 than at |g|=1, where a scale-invariant optimizer gives the same distance for both. After the fix all six magnitudes read 1.199 and v tracks |g| exactly. It hit conv/LSTM specifically because they sit behind a batch-norm with running variance ~2.6e+05, so their gradients arrive divided by ~500 - deep in the degraded regime - while the dense stack near the loss stayed in the working one. In situ on SP500 H1: lstm1 dW/W 2.62/10.0/7.14% -> 0.024/0.022/ 0.003%, conv1 decaying to 0.000% by era 30. NeuronBatchNorm.mqh already squared v back for gamma/beta and its comment named the kernels as wrong, which is exactly why gamma/beta kept training while the stages behind froze. Persisted .nnw needs no migration - v keeps its std-dev meaning. Also, the two ways F4 exposed it, both mine: - No LR compensation for B fewer steps per era. sqrt(B) for adaptive methods (Krizhevsky 2014; Granziol et al. 2022), applied once in InitialEtaForOptimizer(). Linear scaling (Goyal et al. 2017) is for SGD. - Plateau patience denominated in eras, so raising B made the ladder 32x more impatient in its only unit. PAI converged at era 41 on ~49k updates where the same config had been finding new bests at era 1028. TrainPlateauPatienceEras() stretches it by the same sqrt(B). TRAIN_BATCH_SIZE 32 -> 8 so the patience stretch stays affordable (8 -> 23 eras per stage, not 8 -> 45). Both helpers are identities at B=1. Deploy gate: DEPLOY_MIN_SIDE_RECALL_PCT (10%) folded into tradeableOK. The perceptron reported Sell:0% recall in all 41 eras, cleared the floor on Buy alone at 36.6% vs 34% chance, deployed, and sprayed buy arrows. Folded into the ranking key rather than checked at deploy time so a one-sided era cannot become best-so-far in the first place. Deinit: the arrow purge now runs BEFORE ExtPanel.Destroy(), an unbounded CAppDialog teardown that sat ahead of it - the same ordering inversion the rule there exists to prevent. CONV was force-terminated 4.8 s into OnDeinit (vs ~1.1 s for the three that finished) having reached none of its cleanup, so its arrows stayed on the chart. Steps are now timed in the log. PurgeChart's verification rescan filtered on OBJ_ARROW, the same blind spot as the bulk delete, so "persisted 10 ... cleared 0" passed silently. It now walks every object type and reports the object counts when both are zero. Both build variants compile 0 errors / 0 warnings; both DLLs rebuilt. FORCES A RETRAIN (already forced by N1) and both DLLs must ship with the .ex5. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:02:35 -04:00
ulong deinitTPanel = GetMicrosecondCount();
Print(__FUNCTION__ + ": cleanup timings - arrows " + DoubleToString((deinitTArrows - deinitT0) / 1000.0, 0) +
" ms, panel " + DoubleToString((deinitTPanel - deinitTArrows) / 1000.0, 0) +
" ms. MetaTrader force-terminates OnDeinit at roughly 4,500 ms TOTAL; if this line is missing "
"entirely, the budget expired before it and the step that overran is the one after the last "
"message that DID print.");
//--- The MarkShutdown()/ShutdownChartCleanup() pair that used to sit here has moved ABOVE the panel
//--- teardown - see the ordering note there. The original order put that heavy save ahead of the
//--- cheap visible cleanup.
fix: flush the in-flight era on shutdown; sweep orphaned chart objects on attach Chart objects live in the MT5 chart PROFILE, not in this EA's files. They survive a terminal restart, a recompile, and deleting every .nnw/.cfg/.stats/.arrows on disk. Only a deinit that RUNS TO COMPLETION removes them - and MetaTrader force-terminates OnDeinit at roughly 4,500 ms, so a run killed mid-cleanup orphans them permanently with no owner left to clean up after. That is the "deleted every file, recompiled, restarted, old arrows and a stale panel still there" report: nothing was wrong with the files and deleting them could not have helped. Both halves are fixed. STOP OVERRUNNING THE BUDGET. OnDeinit used to finalise the in-flight run (StopTraining -> FinalizeTrainRun: checkpoint restore, live-state re-seed) and then write two full nets per chart. On four charts that is the bulk of the budget, spent to preserve a PARTIAL era that was never scored, never checkpointed and never deployable. FlushTrainRun() discards it instead - drop the resumable bookkeeping, leave the net neutral (unfreeze BN, flush the batch, batch size 1), skip the save - and training resumes from the last completed era, which the era-end save and the periodic autosave have already put on disk. What is discarded is bounded by one era. A CONVERGED model keeps the old finalise-and-save path: its weights can carry online-learning updates made since the last era boundary, and for a deployed model no further era boundary is coming to persist them. MAKE CLEANUP SELF-HEALING. Every purge sat behind a branch - no model loaded, sidecar missing - so the common paths returned leaving whatever the previous instance stranded. LoadChartSignals now sweeps the arrow namespace unconditionally before restoring, so the post-init chart holds exactly what the sidecar holds whichever branch runs, and the panel gets the same treatment before Create() (CAppDialog namespaces its controls, so a killed Destroy strands the lot and the next attach draws a second panel on the corpse). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 13:08:40 -04:00
bool flushedAny = false;
fix(deinit): a full model write was running ahead of the cheap cleanup "Abnormal termination" is back, and this time it is not the arrows. The timing names the culprit exactly: 16:02:31.547 OnDeinit: shutting down 16:02:36.003 Abnormal termination <- 4.46 s, MetaTrader gave up 16:02:36.226 chart signals - persisted <- cleanup finished 0.2 s LATE OnDeinit called StopTraining() BEFORE the chart cleanup. StopTraining() finalises an in-flight run, and FinalizeTrainRun() restores the best checkpoint and then persists it - a full ~1MB model write per signal. So the expensive step ran ahead of the cheap bounded one, which is precisely the inversion the shutdown ordering exists to prevent. The previous fix put PersistWeightsOnShutdown last and missed that StopTraining smuggles a second save in at the front. Two changes: Cleanup now runs FIRST, then StopTraining, then the weight save. The visible teardown is cheap and bounded, so it always completes even when everything after it is killed. And the deploy-persist inside FinalizeTrainRun is suppressed during shutdown. RestoreWeights() is an in-MEMORY swap, so the best checkpoint is already the live net by that line, and PersistWeightsOnShutdown writes exactly those weights moments later. The old path wrote the same model twice per signal - eight full writes across four charts - for no benefit. A user-pressed Stop still persists immediately, because nothing else would. Compiles 0 errors / 0 warnings. Build tag deinit-order-v2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 16:06:40 -04:00
for(int i = 0; i < g_aiSignalCount; i++)
fix: flush the in-flight era on shutdown; sweep orphaned chart objects on attach Chart objects live in the MT5 chart PROFILE, not in this EA's files. They survive a terminal restart, a recompile, and deleting every .nnw/.cfg/.stats/.arrows on disk. Only a deinit that RUNS TO COMPLETION removes them - and MetaTrader force-terminates OnDeinit at roughly 4,500 ms, so a run killed mid-cleanup orphans them permanently with no owner left to clean up after. That is the "deleted every file, recompiled, restarted, old arrows and a stale panel still there" report: nothing was wrong with the files and deleting them could not have helped. Both halves are fixed. STOP OVERRUNNING THE BUDGET. OnDeinit used to finalise the in-flight run (StopTraining -> FinalizeTrainRun: checkpoint restore, live-state re-seed) and then write two full nets per chart. On four charts that is the bulk of the budget, spent to preserve a PARTIAL era that was never scored, never checkpointed and never deployable. FlushTrainRun() discards it instead - drop the resumable bookkeeping, leave the net neutral (unfreeze BN, flush the batch, batch size 1), skip the save - and training resumes from the last completed era, which the era-end save and the periodic autosave have already put on disk. What is discarded is bounded by one era. A CONVERGED model keeps the old finalise-and-save path: its weights can carry online-learning updates made since the last era boundary, and for a deployed model no further era boundary is coming to persist them. MAKE CLEANUP SELF-HEALING. Every purge sat behind a branch - no model loaded, sidecar missing - so the common paths returned leaving whatever the previous instance stranded. LoadChartSignals now sweeps the arrow namespace unconditionally before restoring, so the post-init chart holds exactly what the sidecar holds whichever branch runs, and the panel gets the same treatment before Create() (CAppDialog namespaces its controls, so a killed Destroy strands the lot and the next attach draws a second panel on the corpse). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 13:08:40 -04:00
{
if(g_aiSignals[i].TrainingComplete())
g_aiSignals[i].StopTraining();
else
flushedAny = (g_aiSignals[i].FlushTrainRun() || flushedAny);
}
if(flushedAny)
Print(__FUNCTION__ + ": discarded the in-flight era on at least one model rather than finalising it -"
" training resumes from the last completed era, which is already on disk. This is what keeps"
" the chart cleanup inside MetaTrader's deinit budget.");
//--- Persist every active AI signal's current in-memory weights/state, so a terminal restart,
//--- recompile, chart re-add or template swap resumes from here rather than from the last fully-
//--- completed training era only.
if(!isTesterRun)
{
for(int i = 0; i < g_aiSignalCount; i++)
{
fix: flush the in-flight era on shutdown; sweep orphaned chart objects on attach Chart objects live in the MT5 chart PROFILE, not in this EA's files. They survive a terminal restart, a recompile, and deleting every .nnw/.cfg/.stats/.arrows on disk. Only a deinit that RUNS TO COMPLETION removes them - and MetaTrader force-terminates OnDeinit at roughly 4,500 ms, so a run killed mid-cleanup orphans them permanently with no owner left to clean up after. That is the "deleted every file, recompiled, restarted, old arrows and a stale panel still there" report: nothing was wrong with the files and deleting them could not have helped. Both halves are fixed. STOP OVERRUNNING THE BUDGET. OnDeinit used to finalise the in-flight run (StopTraining -> FinalizeTrainRun: checkpoint restore, live-state re-seed) and then write two full nets per chart. On four charts that is the bulk of the budget, spent to preserve a PARTIAL era that was never scored, never checkpointed and never deployable. FlushTrainRun() discards it instead - drop the resumable bookkeeping, leave the net neutral (unfreeze BN, flush the batch, batch size 1), skip the save - and training resumes from the last completed era, which the era-end save and the periodic autosave have already put on disk. What is discarded is bounded by one era. A CONVERGED model keeps the old finalise-and-save path: its weights can carry online-learning updates made since the last era boundary, and for a deployed model no further era boundary is coming to persist them. MAKE CLEANUP SELF-HEALING. Every purge sat behind a branch - no model loaded, sidecar missing - so the common paths returned leaving whatever the previous instance stranded. LoadChartSignals now sweeps the arrow namespace unconditionally before restoring, so the post-init chart holds exactly what the sidecar holds whichever branch runs, and the panel gets the same treatment before Create() (CAppDialog namespaces its controls, so a killed Destroy strands the lot and the next attach draws a second panel on the corpse). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 13:08:40 -04:00
if(!g_aiSignals[i].TrainingComplete())
continue;
if(!g_aiSignals[i].PersistWeightsOnShutdown())
Print(__FUNCTION__ + ": WARNING - failed to persist weights for signal index " + IntegerToString(i) + " on shutdown (reason: " + reasonStr + ")");
}
}
g_aiSignalCount = 0;
dbm.Deinit();
Expert.Deinit();
//--- belt-and-suspenders: the status label was already cleared first, but re-clear in case a later path
//--- (e.g. Expert.Deinit's destructors) drew anything, so nothing is left on the chart after removal.
ClearStatusLabel();
s_deinitInProgress = false;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
//--- Expert.OnTimer() does the (comparatively expensive) DB-ranking work, originally paced by its
//--- own 1-hour EventSetTimer() interval; now that the timer fires every 500ms (see OnInit for why
//--- that interval), pace that work manually instead so it still only actually runs about once an hour.
#define DB_RANKING_INTERVAL_SECONDS 3600
datetime g_lastDbRankingRun = 0;
//--- Alt-data maintenance (see System\AltDataFetch.mqh): the EA downloads its own missing history
//--- at attach time and keeps appending forward while deployed, so online learning never depends on
//--- an external process.
feat(altdata): EIA wired, 24-instrument symbol catalog, mapping dialog for unknown symbols EIA (user directive: "the NN might find patterns in it for both oil and regular symbols"). Weekly Petroleum Status Report via the v2 API - crude stocks ex-SPR, field production, refinery utilization - three features (1y percentile, 4w change, utilization) on EVERY catalog symbol, not just oil. EIA screened NULL on WTI's short 7y sample, so these ship as EXPLORATORY inputs: the deploy gate, not the screen, decides whether a model trained on them trades. Publication stamp observed+6d mirrors research/altdata/eia.py. Symbol handling was hardcoded to three if-blocks; it is now a catalog of 24 instruments x alias lists covering The5ers/FTMO/AvaTrade/Dukascopy/OANDA/IC Markets naming, with prefix matching for the broker suffix zoo (US500.cash, XAUUSDm, EURUSD.r). Adding an instrument is one AddSpec row. COT caches are named by CANONICAL so two brokers' names for one contract share a download. Unrecognised symbol -> a chart dialog (Panel\AltDataMapDialog.mqh, CAppDialog + dropdown) asks which instrument it is; the answer persists in symbol_map.cfg and "No alternative data" is a recorded choice, not a nag. Non-blocking by design: an unmapped symbol contributes 0 features and must never hold up a chart. Also: UrlEncodePart now escapes '%' - SoQL like-predicates use it as the wildcard and an unescaped one corrupts the query; docs/ gains the whitelist URLs, an API-key backup, and the catalog reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:18:29 -04:00
//+------------------------------------------------------------------+
//| Unknown chart symbol -> ask which instrument it is, once. |
//| Non-blocking: the EA keeps initialising, training and trading |
//| while the dialog sits on the chart. An unmapped symbol just |
//| contributes 0 alt-data features. |
//+------------------------------------------------------------------+
void MaybeAskAltDataMapping(void)
{
if(!EnableAltData || g_altMapAsked || g_altMapDialogOpen)
return;
if(!g_altDataFetch.NeedsMapping(_Symbol))
return; // catalogued, or the user already recorded a choice
g_altMapAsked = true; // one prompt per attach, even if it is closed unanswered
int n = g_altDataFetch.CatalogCount();
string labels[], canon[];
ArrayResize(labels, n);
ArrayResize(canon, n);
for(int i = 0; i < n; i++)
{
labels[i] = g_altDataFetch.CatalogLabel(i);
canon[i] = g_altDataFetch.CatalogName(i);
}
PrintFormat("AltDataFetch: '%s' is not in the alt-data catalog - asking which instrument it maps "
"to. The EA runs normally either way; the answer is saved in symbol_map.cfg.", _Symbol);
if(g_altMapDialog.Show(_Symbol, labels, canon))
g_altMapDialogOpen = true;
else
Print("AltDataFetch: the mapping dialog could not be opened - map it by hand instead: add a "
"line like '" + _Symbol + "=SP500' to Common\\Files\\Warrior_EA\\AltData\\symbol_map.cfg "
"(or '" + _Symbol + "=NONE' to decline).");
}
//+------------------------------------------------------------------+
//| Apply the dialog's answer once the user has clicked. Called from |
//| OnChartEvent, after the dialog has seen the same event. Drive |
//| the FILTERED view's historical reconstruction. |
feat(chart): reconstruct the filtered view behind the handover point Completes the filtered view from 282b535, which only reached forward of attach. On a multi-hour training run that is the entire time you are looking at the chart, so the answer to "how would the whole bot have traded" was blank exactly when it was wanted. The sweep lives on the AGGREGATE signal, which is the only object holding every filter. AI members contribute their CACHED per-bar decision from the era scan - no inference re-runs, the cache already spans the chart - and the classic ladders are replayed with EvalShift(i), the same mechanism CSignalMETA's candidate sweep uses and exact because every classic pattern condition anchors on StartIndex(). Combination is the live one: weighted mean over voting filters, abstentions out of both sums, against Min_Vote_Open. THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part that is not obvious. Live journaling reads m_active_pattern_long/short from the PREVIOUS Direction() call. Replaying hundreds of past bars between two live bars leaves those slots holding whichever bar the sweep stopped on, so the next live bar journals that pattern under the current timestamp - a corrupted row in the very table pattern win rates are computed from, which is now also where vote weights come from. Save/RestoreVoteState() brackets every replayed call. CSignalMETA gets away without it only because its sweep runs once, at the first era, before any of that state matters. TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker rejected an order - it has no stops level, ATR warm-up or swing-history sync as they were at that moment - so it is an upper bound: honest about the vote, optimistic about placement. It therefore stops dead at the handover bar, which is latched ONCE so later rebuilds cannot creep it forward and start overwriting real decisions with guesses, and its arrows say "reconstructed (vote only - order validation not replayed)" in the tooltip. Someone comparing two arrows either side of that line has to be able to tell which is a record and which is a replay, and the chart is the only place they look. Re-armed on any era boundary (summed era counters), because that is when the answer changes - RankTiersFromOos has just re-derived every tier's vote weight - and only between sweeps, so a restart cannot leave the previous pass's tail undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on every classic filter, which is real indicator work on the chart thread, and an unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen. SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should reach the same distance or the raw and filtered views are not comparable. Known gap: on a classic-only chart the reconstruction is built once and not refreshed when the hourly DB ranking moves the classic weights. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
//+------------------------------------------------------------------+
#define OVERLAY_BARS_PER_SLICE 150
//--- Minimum wall-clock between overlay re-arms. Five minutes keeps the reconstruction current on
//--- any human timescale while cutting the churn ~15x.
fix(chart): display now reads era-end SNAPSHOTS - the live cache is wiped mid-era Full-pipeline analysis after "threshold 30, attained often, nothing drawn, still glued to buy". The log falsified the premise before any code did: 21:40:43 swept 4999, 794 voters, drew 491. Strongest 43.0% vs 30.0% 21:42:07 swept 4999, 0 voters, drew 0 21:51:30 swept 4999, 0 voters, drew 0 21:56:30 swept 4999, 922 voters, drew 382. Strongest 44.0% vs 30.0% The arrows WERE drawn - 491 of them, then 382 - and then erased. ONE root cause, three symptoms: every display path read m_arrowSignalCache, which is wiped to sentinel at each era start and only complete again when pass 3 finishes. With eras at ~30s and a sweep at ~17s: * ARROW FLICKER: a sweep landing mid-era found no voters anywhere, and its else-branch deleted the arrow on every voteless bar - erasing the previous sweep's entire output. The chart cycled populated -> blank -> populated; the user kept catching the blank phase. * READOUT GLUE: the newest-cache walk found only sentinel for ~90% of every era and fell through to dPrevSignal - the frozen purge-band edge bar that reads Buy. 659638e fixed which bar was frozen, not the freezing. * VOTER FLAP: 1299 -> 257 -> 1113 across back-to-back sweeps - each saw a different fraction of half-rebuilt caches. THE FIX, structural rather than another patch: 1. Era-end snapshots. RankTiersFromOos() runs at pass-3 completion - the one moment the cache is complete - and now copies it (raw signals, newest LOOKBACK+16 bars) into member-owned snapshot state, unconditionally, BEFORE its early return: an all-Neutral era is a snapshot worth showing, not an absence of one. Raw signals rather than votes, so a tier re-rank between eras reprices them at read time via LiveVoteContribution for free. 2. The sweep (SnapshotVoteAt) and the prospective readout both read snapshots; the readout's fallback chain is live-cache -> snapshot -> dPrevSignal, and the snapshot leg is the one that fires most of the time. 3. NO DATA IS NOT A VERDICT: a den==0 bar no longer deletes - only an actual sub-threshold vote takes an arrow down. This alone ends the wipe half of the flicker even where snapshots are missing (before the first era). 4. Arming moved from an era-counter diff (which fires at era BOUNDARIES, i.e. precisely when caches are about to be wiped) to g_warriorOverlayArmRequest, set by each RankTiersFromOos - "a member's snapshot just got fresher", the only event a redraw can act on. 60s rate limit collapses the four members' burst into one sweep. Classic-only charts arm once at start. 5. Census now reports the direction split - "922 had a voter (610 buy / 312 sell)" - so "the vote leans buy" is checkable from the log instead of inferred from arrow colours. Also visible in the log and worth knowing: the threshold flip-flopped 30 -> 40 -> 30 across the evening's re-inits (census lines at 21:42-21:51 ran at 40), so part of the observed blankness was configuration, not code. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:03:20 -04:00
#define OVERLAY_REARM_MIN_MS 60000
fix(chart): sweep hammered the news filter; peak was a fossil; neutrals invisible Careful read of the 21:14 log window (user report: peak stuck at 50, label sticky, neutrals never shown). Three distinct defects, one commit because they share the two files. 1. 15,508 "CalendarValueHistory failed" lines in 68 SECONDS - ~230/second. The overlay sweep replayed Direction() on EVERY non-AI filter, including the news/session/risk-guard veto filters. The news filter calls CalendarValueHistory per evaluation and MT5's calendar cannot answer more than ~30 days back (the known calendar cliff), so every historical bar logged a failure - real wall-clock burned inside a sweep whose whole point is to stay cheap. Veto filters keep m_pattern_count at its 0 default (the same test UpdateSignalsWeights keys on): they cast no weighted vote, and a prohibition cannot be reconstructed faithfully anyway - it joins order validation in the cannot-replay family. Skipped. Compounding it: at era ~200 the four members complete a barrier round every ~20s while a full 5,000-bar sweep takes ~17s of slices - the sweep finished and instantly re-armed, forever, against arrow caches half-rebuilt mid-era. That is why the census's "had a voter" flapped 1299 -> 257 -> 1113 across three back-to-back sweeps. Re-arms now rate-limited to one per 5 minutes. 2. Peak 50 was a FOSSIL. m_votePeak never reset, so it still held a value attained under the 25/50/75/100 DEFAULT tier weights from the attach window before the first re-rank - unreachable ever since the weights became measured (pooled 27-32 in the same log). A ceiling nothing can reach reads as "the models are underperforming their own history", which is backwards: the history was priced in different money. The peak now resets at the same regime boundary as the census (StartFilteredOverlay), and the label shows max(live peak, census strongest-vote) - the census number is the actual answer to "can Min_Vote_Open ever be reached", measured over ~5,000 bars under the CURRENT weights. 3. Neutrals were invisible. The prospective count lumped Neutral-deciding models in with voters, so "4 model(s)" read identically whether all four voted or three sat flat. Now "2 vote/2 flat", and an all-neutral bar reads "VOTE flat ... 0 vote/4 flat" instead of "--" - the models answered, and the answer was Neutral. Expected values, from this log's own re-ranks (all four members' fires land in T3; tier weights 27-32; module weights 0.27-0.32): a unanimous-buy bar reads ~29-30%, mixed membership 28-34. The reported "stuck at buy 28, climbed to 30, flashes of sell, now 33.4" is those weights doing exactly what they should. The stickiness between moves is pass 2/2.5/3 - only pass 1 writes dPrevSignal, so the label holds the last pass-1 bar's decision for the remainder of each era. Display-only, and honest: it is the model's most recent output. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 21:18:55 -04:00
uint g_lastOverlayArmTick = 0;
fix(vote): unranked members voted with the stock 25/50/75/100 ladder Two defects behind "arrows drawn while members are still mid-era". 1. THE DRAW. The filtered overlay armed on the FIRST member to finish pass 3 and leaned on a 60 s rate limit to "collapse the burst", assuming members finish seconds apart. They do not - on USDJPY one member was at sample 10496 of pass 2 while another was at 2304, minutes apart. A member with no era-end snapshot returns false from SnapshotVoteAt, and the sweep's `if(!hasData) continue;` skips it BEFORE `den += ModuleWeight()`, so the one finished model's tier weight became the entire vote and was drawn as a consensus arrow. An abstention is a member that looked at the bar and said nothing; a missing snapshot is a member that has not looked. The first must dilute the vote, the second must suppress the draw. The arm is now a readiness MASK - one bit per m_ensembleIndex, set at that member's pass-3 completion, cleared when a sweep arms - and a sweep waits for every enrolled member. Bounded at 10 minutes so a member that stops cannot freeze the chart, and the partial draw PRINTS which members were missing: the be39674 lesson is that a hold must never silence the thing that reports it. 2. THE VOTE ITSELF, which is the worse half and is not display-only. Tier weights are not persisted in the .nnw - they exist only as the output of a completed pass 3 - so before a member's first RankTiersFromOos() it holds the constructor's stock 25/50/75/100. Since 4858507 the vote currency is a WIN RATE, so an unranked tier-3 call enters the capability-weighted mean claiming a 100% win rate beside ranked members contributing ~25. Not a strong opinion: the wrong unit. One unranked member drags the ensemble over any threshold, on every fresh deploy and every resume. USDJPY has a measured ceiling of ~19 and was firing anyway. LiveVoteContribution() now abstains until self-ranked, which drops the member from the sum AND the divisor. One function, so live and the gate move together (2c443ba). Era 0 will therefore report 0 coverage until each member completes one era. The ensemble line says so explicitly rather than leaving it to look like the USDJPY unreachable-threshold case - the two are identical in the coverage number and completely different problems. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 08:55:04 -04:00
//--- Longest a sweep waits for a member that has stopped producing snapshots. Past this it draws
//--- with whoever is ready AND SAYS SO - a stale chart that never redraws is worse than a partial
//--- one that reports itself.
#define OVERLAY_PARTIAL_ARM_MS 600000
//+------------------------------------------------------------------+
//| Which ensemble members must have a snapshot before a sweep runs. |
//| |
//| The array index is the member's m_ensembleIndex, so the slot IS |
//| the bit. Zero means no ensemble is registered (a single-model |
//| configuration), and the gate is then a no-op rather than a lock. |
//+------------------------------------------------------------------+
uint EnrolledOverlayMask(void)
{
uint mask = 0;
int n = MathMin(ArraySize(g_warriorEnsemble), ENS_MAX_MEMBERS);
for(int i = 0; i < n; i++)
if(CheckPointer(g_warriorEnsemble[i]) != POINTER_INVALID)
mask |= (((uint)1) << i);
return mask;
}
feat(chart): reconstruct the filtered view behind the handover point Completes the filtered view from 282b535, which only reached forward of attach. On a multi-hour training run that is the entire time you are looking at the chart, so the answer to "how would the whole bot have traded" was blank exactly when it was wanted. The sweep lives on the AGGREGATE signal, which is the only object holding every filter. AI members contribute their CACHED per-bar decision from the era scan - no inference re-runs, the cache already spans the chart - and the classic ladders are replayed with EvalShift(i), the same mechanism CSignalMETA's candidate sweep uses and exact because every classic pattern condition anchors on StartIndex(). Combination is the live one: weighted mean over voting filters, abstentions out of both sums, against Min_Vote_Open. THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part that is not obvious. Live journaling reads m_active_pattern_long/short from the PREVIOUS Direction() call. Replaying hundreds of past bars between two live bars leaves those slots holding whichever bar the sweep stopped on, so the next live bar journals that pattern under the current timestamp - a corrupted row in the very table pattern win rates are computed from, which is now also where vote weights come from. Save/RestoreVoteState() brackets every replayed call. CSignalMETA gets away without it only because its sweep runs once, at the first era, before any of that state matters. TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker rejected an order - it has no stops level, ATR warm-up or swing-history sync as they were at that moment - so it is an upper bound: honest about the vote, optimistic about placement. It therefore stops dead at the handover bar, which is latched ONCE so later rebuilds cannot creep it forward and start overwriting real decisions with guesses, and its arrows say "reconstructed (vote only - order validation not replayed)" in the tooltip. Someone comparing two arrows either side of that line has to be able to tell which is a record and which is a replay, and the chart is the only place they look. Re-armed on any era boundary (summed era counters), because that is when the answer changes - RankTiersFromOos has just re-derived every tier's vote weight - and only between sweeps, so a restart cannot leave the previous pass's tail undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on every classic filter, which is real indicator work on the chart thread, and an unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen. SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should reach the same distance or the raw and filtered views are not comparable. Known gap: on a classic-only chart the reconstruction is built once and not refreshed when the hourly DB ranking moves the classic weights. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
void AdvanceFilteredSignalOverlay(void)
{
if(DrawUnfilteredSignals)
return; // raw view owns the chart; nothing to reconstruct
fix(chart): display now reads era-end SNAPSHOTS - the live cache is wiped mid-era Full-pipeline analysis after "threshold 30, attained often, nothing drawn, still glued to buy". The log falsified the premise before any code did: 21:40:43 swept 4999, 794 voters, drew 491. Strongest 43.0% vs 30.0% 21:42:07 swept 4999, 0 voters, drew 0 21:51:30 swept 4999, 0 voters, drew 0 21:56:30 swept 4999, 922 voters, drew 382. Strongest 44.0% vs 30.0% The arrows WERE drawn - 491 of them, then 382 - and then erased. ONE root cause, three symptoms: every display path read m_arrowSignalCache, which is wiped to sentinel at each era start and only complete again when pass 3 finishes. With eras at ~30s and a sweep at ~17s: * ARROW FLICKER: a sweep landing mid-era found no voters anywhere, and its else-branch deleted the arrow on every voteless bar - erasing the previous sweep's entire output. The chart cycled populated -> blank -> populated; the user kept catching the blank phase. * READOUT GLUE: the newest-cache walk found only sentinel for ~90% of every era and fell through to dPrevSignal - the frozen purge-band edge bar that reads Buy. 659638e fixed which bar was frozen, not the freezing. * VOTER FLAP: 1299 -> 257 -> 1113 across back-to-back sweeps - each saw a different fraction of half-rebuilt caches. THE FIX, structural rather than another patch: 1. Era-end snapshots. RankTiersFromOos() runs at pass-3 completion - the one moment the cache is complete - and now copies it (raw signals, newest LOOKBACK+16 bars) into member-owned snapshot state, unconditionally, BEFORE its early return: an all-Neutral era is a snapshot worth showing, not an absence of one. Raw signals rather than votes, so a tier re-rank between eras reprices them at read time via LiveVoteContribution for free. 2. The sweep (SnapshotVoteAt) and the prospective readout both read snapshots; the readout's fallback chain is live-cache -> snapshot -> dPrevSignal, and the snapshot leg is the one that fires most of the time. 3. NO DATA IS NOT A VERDICT: a den==0 bar no longer deletes - only an actual sub-threshold vote takes an arrow down. This alone ends the wipe half of the flicker even where snapshots are missing (before the first era). 4. Arming moved from an era-counter diff (which fires at era BOUNDARIES, i.e. precisely when caches are about to be wiped) to g_warriorOverlayArmRequest, set by each RankTiersFromOos - "a member's snapshot just got fresher", the only event a redraw can act on. 60s rate limit collapses the four members' burst into one sweep. Classic-only charts arm once at start. 5. Census now reports the direction split - "922 had a voter (610 buy / 312 sell)" - so "the vote leans buy" is checkable from the log instead of inferred from arrow colours. Also visible in the log and worth knowing: the threshold flip-flopped 30 -> 40 -> 30 across the evening's re-inits (census lines at 21:42-21:51 ran at 40), so part of the observed blankness was configuration, not code. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:03:20 -04:00
//--- ARM ON THE MEMBERS' OWN SIGNAL, not on an era-counter diff. g_warriorOverlayArmRequest is
//--- set by RankTiersFromOos() at pass-3 completion - the moment a member's era-end snapshot
//--- became fresher - which is the only event a redraw can act on.
fix(chart): display now reads era-end SNAPSHOTS - the live cache is wiped mid-era Full-pipeline analysis after "threshold 30, attained often, nothing drawn, still glued to buy". The log falsified the premise before any code did: 21:40:43 swept 4999, 794 voters, drew 491. Strongest 43.0% vs 30.0% 21:42:07 swept 4999, 0 voters, drew 0 21:51:30 swept 4999, 0 voters, drew 0 21:56:30 swept 4999, 922 voters, drew 382. Strongest 44.0% vs 30.0% The arrows WERE drawn - 491 of them, then 382 - and then erased. ONE root cause, three symptoms: every display path read m_arrowSignalCache, which is wiped to sentinel at each era start and only complete again when pass 3 finishes. With eras at ~30s and a sweep at ~17s: * ARROW FLICKER: a sweep landing mid-era found no voters anywhere, and its else-branch deleted the arrow on every voteless bar - erasing the previous sweep's entire output. The chart cycled populated -> blank -> populated; the user kept catching the blank phase. * READOUT GLUE: the newest-cache walk found only sentinel for ~90% of every era and fell through to dPrevSignal - the frozen purge-band edge bar that reads Buy. 659638e fixed which bar was frozen, not the freezing. * VOTER FLAP: 1299 -> 257 -> 1113 across back-to-back sweeps - each saw a different fraction of half-rebuilt caches. THE FIX, structural rather than another patch: 1. Era-end snapshots. RankTiersFromOos() runs at pass-3 completion - the one moment the cache is complete - and now copies it (raw signals, newest LOOKBACK+16 bars) into member-owned snapshot state, unconditionally, BEFORE its early return: an all-Neutral era is a snapshot worth showing, not an absence of one. Raw signals rather than votes, so a tier re-rank between eras reprices them at read time via LiveVoteContribution for free. 2. The sweep (SnapshotVoteAt) and the prospective readout both read snapshots; the readout's fallback chain is live-cache -> snapshot -> dPrevSignal, and the snapshot leg is the one that fires most of the time. 3. NO DATA IS NOT A VERDICT: a den==0 bar no longer deletes - only an actual sub-threshold vote takes an arrow down. This alone ends the wipe half of the flicker even where snapshots are missing (before the first era). 4. Arming moved from an era-counter diff (which fires at era BOUNDARIES, i.e. precisely when caches are about to be wiped) to g_warriorOverlayArmRequest, set by each RankTiersFromOos - "a member's snapshot just got fresher", the only event a redraw can act on. 60s rate limit collapses the four members' burst into one sweep. Classic-only charts arm once at start. 5. Census now reports the direction split - "922 had a voter (610 buy / 312 sell)" - so "the vote leans buy" is checkable from the log instead of inferred from arrow colours. Also visible in the log and worth knowing: the threshold flip-flopped 30 -> 40 -> 30 across the evening's re-inits (census lines at 21:42-21:51 ran at 40), so part of the observed blankness was configuration, not code. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:03:20 -04:00
bool wantArm = g_warriorOverlayArmRequest || (g_aiSignalCount == 0 && g_lastOverlayArmTick == 0);
fix(vote): unranked members voted with the stock 25/50/75/100 ladder Two defects behind "arrows drawn while members are still mid-era". 1. THE DRAW. The filtered overlay armed on the FIRST member to finish pass 3 and leaned on a 60 s rate limit to "collapse the burst", assuming members finish seconds apart. They do not - on USDJPY one member was at sample 10496 of pass 2 while another was at 2304, minutes apart. A member with no era-end snapshot returns false from SnapshotVoteAt, and the sweep's `if(!hasData) continue;` skips it BEFORE `den += ModuleWeight()`, so the one finished model's tier weight became the entire vote and was drawn as a consensus arrow. An abstention is a member that looked at the bar and said nothing; a missing snapshot is a member that has not looked. The first must dilute the vote, the second must suppress the draw. The arm is now a readiness MASK - one bit per m_ensembleIndex, set at that member's pass-3 completion, cleared when a sweep arms - and a sweep waits for every enrolled member. Bounded at 10 minutes so a member that stops cannot freeze the chart, and the partial draw PRINTS which members were missing: the be39674 lesson is that a hold must never silence the thing that reports it. 2. THE VOTE ITSELF, which is the worse half and is not display-only. Tier weights are not persisted in the .nnw - they exist only as the output of a completed pass 3 - so before a member's first RankTiersFromOos() it holds the constructor's stock 25/50/75/100. Since 4858507 the vote currency is a WIN RATE, so an unranked tier-3 call enters the capability-weighted mean claiming a 100% win rate beside ranked members contributing ~25. Not a strong opinion: the wrong unit. One unranked member drags the ensemble over any threshold, on every fresh deploy and every resume. USDJPY has a measured ceiling of ~19 and was firing anyway. LiveVoteContribution() now abstains until self-ranked, which drops the member from the sum AND the divisor. One function, so live and the gate move together (2c443ba). Era 0 will therefore report 0 coverage until each member completes one era. The ensemble line says so explicitly rather than leaving it to look like the USDJPY unreachable-threshold case - the two are identical in the coverage number and completely different problems. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 08:55:04 -04:00
//--- EVERY ENROLLED MEMBER FIRST. A member still mid-era has no snapshot, the sweep skips it
//--- before the divisor, and the remaining member's own tier weight becomes the whole vote - a
//--- consensus arrow drawn from one model. See g_warriorOverlayReadyMask.
uint enrolled = EnrolledOverlayMask();
bool everyoneReady = (enrolled == 0) || ((g_warriorOverlayReadyMask & enrolled) == enrolled);
if(wantArm && !everyoneReady)
{
if(g_warriorOverlayArmSince == 0)
g_warriorOverlayArmSince = GetTickCount();
//--- Bounded, so a member that stopped cannot freeze the chart. Drawing partial is allowed;
//--- drawing partial SILENTLY is not.
if(GetTickCount() - g_warriorOverlayArmSince >= OVERLAY_PARTIAL_ARM_MS)
{
PrintFormat("Warrior: filtered overlay drawing with a PARTIAL ensemble after %d s - members"
" ready 0x%X of 0x%X enrolled. The missing members have produced no era-end"
" snapshot, so they abstain from every bar in this sweep and the vote is"
" diluted toward the members that did finish. Expect fewer arrows than the live"
" vote would cast, not different ones.",
OVERLAY_PARTIAL_ARM_MS / 1000, g_warriorOverlayReadyMask, enrolled);
everyoneReady = true;
}
}
fix(chart): display now reads era-end SNAPSHOTS - the live cache is wiped mid-era Full-pipeline analysis after "threshold 30, attained often, nothing drawn, still glued to buy". The log falsified the premise before any code did: 21:40:43 swept 4999, 794 voters, drew 491. Strongest 43.0% vs 30.0% 21:42:07 swept 4999, 0 voters, drew 0 21:51:30 swept 4999, 0 voters, drew 0 21:56:30 swept 4999, 922 voters, drew 382. Strongest 44.0% vs 30.0% The arrows WERE drawn - 491 of them, then 382 - and then erased. ONE root cause, three symptoms: every display path read m_arrowSignalCache, which is wiped to sentinel at each era start and only complete again when pass 3 finishes. With eras at ~30s and a sweep at ~17s: * ARROW FLICKER: a sweep landing mid-era found no voters anywhere, and its else-branch deleted the arrow on every voteless bar - erasing the previous sweep's entire output. The chart cycled populated -> blank -> populated; the user kept catching the blank phase. * READOUT GLUE: the newest-cache walk found only sentinel for ~90% of every era and fell through to dPrevSignal - the frozen purge-band edge bar that reads Buy. 659638e fixed which bar was frozen, not the freezing. * VOTER FLAP: 1299 -> 257 -> 1113 across back-to-back sweeps - each saw a different fraction of half-rebuilt caches. THE FIX, structural rather than another patch: 1. Era-end snapshots. RankTiersFromOos() runs at pass-3 completion - the one moment the cache is complete - and now copies it (raw signals, newest LOOKBACK+16 bars) into member-owned snapshot state, unconditionally, BEFORE its early return: an all-Neutral era is a snapshot worth showing, not an absence of one. Raw signals rather than votes, so a tier re-rank between eras reprices them at read time via LiveVoteContribution for free. 2. The sweep (SnapshotVoteAt) and the prospective readout both read snapshots; the readout's fallback chain is live-cache -> snapshot -> dPrevSignal, and the snapshot leg is the one that fires most of the time. 3. NO DATA IS NOT A VERDICT: a den==0 bar no longer deletes - only an actual sub-threshold vote takes an arrow down. This alone ends the wipe half of the flicker even where snapshots are missing (before the first era). 4. Arming moved from an era-counter diff (which fires at era BOUNDARIES, i.e. precisely when caches are about to be wiped) to g_warriorOverlayArmRequest, set by each RankTiersFromOos - "a member's snapshot just got fresher", the only event a redraw can act on. 60s rate limit collapses the four members' burst into one sweep. Classic-only charts arm once at start. 5. Census now reports the direction split - "922 had a voter (610 buy / 312 sell)" - so "the vote leans buy" is checkable from the log instead of inferred from arrow colours. Also visible in the log and worth knowing: the threshold flip-flopped 30 -> 40 -> 30 across the evening's re-inits (census lines at 21:42-21:51 ran at 40), so part of the observed blankness was configuration, not code. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:03:20 -04:00
//--- Re-arm only between sweeps (mid-flight restart would strand the tail undrawn), and at most
fix(vote): unranked members voted with the stock 25/50/75/100 ladder Two defects behind "arrows drawn while members are still mid-era". 1. THE DRAW. The filtered overlay armed on the FIRST member to finish pass 3 and leaned on a 60 s rate limit to "collapse the burst", assuming members finish seconds apart. They do not - on USDJPY one member was at sample 10496 of pass 2 while another was at 2304, minutes apart. A member with no era-end snapshot returns false from SnapshotVoteAt, and the sweep's `if(!hasData) continue;` skips it BEFORE `den += ModuleWeight()`, so the one finished model's tier weight became the entire vote and was drawn as a consensus arrow. An abstention is a member that looked at the bar and said nothing; a missing snapshot is a member that has not looked. The first must dilute the vote, the second must suppress the draw. The arm is now a readiness MASK - one bit per m_ensembleIndex, set at that member's pass-3 completion, cleared when a sweep arms - and a sweep waits for every enrolled member. Bounded at 10 minutes so a member that stops cannot freeze the chart, and the partial draw PRINTS which members were missing: the be39674 lesson is that a hold must never silence the thing that reports it. 2. THE VOTE ITSELF, which is the worse half and is not display-only. Tier weights are not persisted in the .nnw - they exist only as the output of a completed pass 3 - so before a member's first RankTiersFromOos() it holds the constructor's stock 25/50/75/100. Since 4858507 the vote currency is a WIN RATE, so an unranked tier-3 call enters the capability-weighted mean claiming a 100% win rate beside ranked members contributing ~25. Not a strong opinion: the wrong unit. One unranked member drags the ensemble over any threshold, on every fresh deploy and every resume. USDJPY has a measured ceiling of ~19 and was firing anyway. LiveVoteContribution() now abstains until self-ranked, which drops the member from the sum AND the divisor. One function, so live and the gate move together (2c443ba). Era 0 will therefore report 0 coverage until each member completes one era. The ensemble line says so explicitly rather than leaving it to look like the USDJPY unreachable-threshold case - the two are identical in the coverage number and completely different problems. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 08:55:04 -04:00
//--- once a minute once everyone is in.
if(wantArm && everyoneReady && !Expert.FilteredOverlayPending()
fix(chart): sweep hammered the news filter; peak was a fossil; neutrals invisible Careful read of the 21:14 log window (user report: peak stuck at 50, label sticky, neutrals never shown). Three distinct defects, one commit because they share the two files. 1. 15,508 "CalendarValueHistory failed" lines in 68 SECONDS - ~230/second. The overlay sweep replayed Direction() on EVERY non-AI filter, including the news/session/risk-guard veto filters. The news filter calls CalendarValueHistory per evaluation and MT5's calendar cannot answer more than ~30 days back (the known calendar cliff), so every historical bar logged a failure - real wall-clock burned inside a sweep whose whole point is to stay cheap. Veto filters keep m_pattern_count at its 0 default (the same test UpdateSignalsWeights keys on): they cast no weighted vote, and a prohibition cannot be reconstructed faithfully anyway - it joins order validation in the cannot-replay family. Skipped. Compounding it: at era ~200 the four members complete a barrier round every ~20s while a full 5,000-bar sweep takes ~17s of slices - the sweep finished and instantly re-armed, forever, against arrow caches half-rebuilt mid-era. That is why the census's "had a voter" flapped 1299 -> 257 -> 1113 across three back-to-back sweeps. Re-arms now rate-limited to one per 5 minutes. 2. Peak 50 was a FOSSIL. m_votePeak never reset, so it still held a value attained under the 25/50/75/100 DEFAULT tier weights from the attach window before the first re-rank - unreachable ever since the weights became measured (pooled 27-32 in the same log). A ceiling nothing can reach reads as "the models are underperforming their own history", which is backwards: the history was priced in different money. The peak now resets at the same regime boundary as the census (StartFilteredOverlay), and the label shows max(live peak, census strongest-vote) - the census number is the actual answer to "can Min_Vote_Open ever be reached", measured over ~5,000 bars under the CURRENT weights. 3. Neutrals were invisible. The prospective count lumped Neutral-deciding models in with voters, so "4 model(s)" read identically whether all four voted or three sat flat. Now "2 vote/2 flat", and an all-neutral bar reads "VOTE flat ... 0 vote/4 flat" instead of "--" - the models answered, and the answer was Neutral. Expected values, from this log's own re-ranks (all four members' fires land in T3; tier weights 27-32; module weights 0.27-0.32): a unanimous-buy bar reads ~29-30%, mixed membership 28-34. The reported "stuck at buy 28, climbed to 30, flashes of sell, now 33.4" is those weights doing exactly what they should. The stickiness between moves is pass 2/2.5/3 - only pass 1 writes dPrevSignal, so the label holds the last pass-1 bar's decision for the remainder of each era. Display-only, and honest: it is the model's most recent output. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 21:18:55 -04:00
&& (g_lastOverlayArmTick == 0 || GetTickCount() - g_lastOverlayArmTick >= OVERLAY_REARM_MIN_MS))
feat(chart): reconstruct the filtered view behind the handover point Completes the filtered view from 282b535, which only reached forward of attach. On a multi-hour training run that is the entire time you are looking at the chart, so the answer to "how would the whole bot have traded" was blank exactly when it was wanted. The sweep lives on the AGGREGATE signal, which is the only object holding every filter. AI members contribute their CACHED per-bar decision from the era scan - no inference re-runs, the cache already spans the chart - and the classic ladders are replayed with EvalShift(i), the same mechanism CSignalMETA's candidate sweep uses and exact because every classic pattern condition anchors on StartIndex(). Combination is the live one: weighted mean over voting filters, abstentions out of both sums, against Min_Vote_Open. THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part that is not obvious. Live journaling reads m_active_pattern_long/short from the PREVIOUS Direction() call. Replaying hundreds of past bars between two live bars leaves those slots holding whichever bar the sweep stopped on, so the next live bar journals that pattern under the current timestamp - a corrupted row in the very table pattern win rates are computed from, which is now also where vote weights come from. Save/RestoreVoteState() brackets every replayed call. CSignalMETA gets away without it only because its sweep runs once, at the first era, before any of that state matters. TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker rejected an order - it has no stops level, ATR warm-up or swing-history sync as they were at that moment - so it is an upper bound: honest about the vote, optimistic about placement. It therefore stops dead at the handover bar, which is latched ONCE so later rebuilds cannot creep it forward and start overwriting real decisions with guesses, and its arrows say "reconstructed (vote only - order validation not replayed)" in the tooltip. Someone comparing two arrows either side of that line has to be able to tell which is a record and which is a replay, and the chart is the only place they look. Re-armed on any era boundary (summed era counters), because that is when the answer changes - RankTiersFromOos has just re-derived every tier's vote weight - and only between sweeps, so a restart cannot leave the previous pass's tail undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on every classic filter, which is real indicator work on the chart thread, and an unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen. SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should reach the same distance or the raw and filtered views are not comparable. Known gap: on a classic-only chart the reconstruction is built once and not refreshed when the hourly DB ranking moves the classic weights. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
{
fix(chart): display now reads era-end SNAPSHOTS - the live cache is wiped mid-era Full-pipeline analysis after "threshold 30, attained often, nothing drawn, still glued to buy". The log falsified the premise before any code did: 21:40:43 swept 4999, 794 voters, drew 491. Strongest 43.0% vs 30.0% 21:42:07 swept 4999, 0 voters, drew 0 21:51:30 swept 4999, 0 voters, drew 0 21:56:30 swept 4999, 922 voters, drew 382. Strongest 44.0% vs 30.0% The arrows WERE drawn - 491 of them, then 382 - and then erased. ONE root cause, three symptoms: every display path read m_arrowSignalCache, which is wiped to sentinel at each era start and only complete again when pass 3 finishes. With eras at ~30s and a sweep at ~17s: * ARROW FLICKER: a sweep landing mid-era found no voters anywhere, and its else-branch deleted the arrow on every voteless bar - erasing the previous sweep's entire output. The chart cycled populated -> blank -> populated; the user kept catching the blank phase. * READOUT GLUE: the newest-cache walk found only sentinel for ~90% of every era and fell through to dPrevSignal - the frozen purge-band edge bar that reads Buy. 659638e fixed which bar was frozen, not the freezing. * VOTER FLAP: 1299 -> 257 -> 1113 across back-to-back sweeps - each saw a different fraction of half-rebuilt caches. THE FIX, structural rather than another patch: 1. Era-end snapshots. RankTiersFromOos() runs at pass-3 completion - the one moment the cache is complete - and now copies it (raw signals, newest LOOKBACK+16 bars) into member-owned snapshot state, unconditionally, BEFORE its early return: an all-Neutral era is a snapshot worth showing, not an absence of one. Raw signals rather than votes, so a tier re-rank between eras reprices them at read time via LiveVoteContribution for free. 2. The sweep (SnapshotVoteAt) and the prospective readout both read snapshots; the readout's fallback chain is live-cache -> snapshot -> dPrevSignal, and the snapshot leg is the one that fires most of the time. 3. NO DATA IS NOT A VERDICT: a den==0 bar no longer deletes - only an actual sub-threshold vote takes an arrow down. This alone ends the wipe half of the flicker even where snapshots are missing (before the first era). 4. Arming moved from an era-counter diff (which fires at era BOUNDARIES, i.e. precisely when caches are about to be wiped) to g_warriorOverlayArmRequest, set by each RankTiersFromOos - "a member's snapshot just got fresher", the only event a redraw can act on. 60s rate limit collapses the four members' burst into one sweep. Classic-only charts arm once at start. 5. Census now reports the direction split - "922 had a voter (610 buy / 312 sell)" - so "the vote leans buy" is checkable from the log instead of inferred from arrow colours. Also visible in the log and worth knowing: the threshold flip-flopped 30 -> 40 -> 30 across the evening's re-inits (census lines at 21:42-21:51 ran at 40), so part of the observed blankness was configuration, not code. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:03:20 -04:00
g_warriorOverlayArmRequest = false;
fix(vote): unranked members voted with the stock 25/50/75/100 ladder Two defects behind "arrows drawn while members are still mid-era". 1. THE DRAW. The filtered overlay armed on the FIRST member to finish pass 3 and leaned on a 60 s rate limit to "collapse the burst", assuming members finish seconds apart. They do not - on USDJPY one member was at sample 10496 of pass 2 while another was at 2304, minutes apart. A member with no era-end snapshot returns false from SnapshotVoteAt, and the sweep's `if(!hasData) continue;` skips it BEFORE `den += ModuleWeight()`, so the one finished model's tier weight became the entire vote and was drawn as a consensus arrow. An abstention is a member that looked at the bar and said nothing; a missing snapshot is a member that has not looked. The first must dilute the vote, the second must suppress the draw. The arm is now a readiness MASK - one bit per m_ensembleIndex, set at that member's pass-3 completion, cleared when a sweep arms - and a sweep waits for every enrolled member. Bounded at 10 minutes so a member that stops cannot freeze the chart, and the partial draw PRINTS which members were missing: the be39674 lesson is that a hold must never silence the thing that reports it. 2. THE VOTE ITSELF, which is the worse half and is not display-only. Tier weights are not persisted in the .nnw - they exist only as the output of a completed pass 3 - so before a member's first RankTiersFromOos() it holds the constructor's stock 25/50/75/100. Since 4858507 the vote currency is a WIN RATE, so an unranked tier-3 call enters the capability-weighted mean claiming a 100% win rate beside ranked members contributing ~25. Not a strong opinion: the wrong unit. One unranked member drags the ensemble over any threshold, on every fresh deploy and every resume. USDJPY has a measured ceiling of ~19 and was firing anyway. LiveVoteContribution() now abstains until self-ranked, which drops the member from the sum AND the divisor. One function, so live and the gate move together (2c443ba). Era 0 will therefore report 0 coverage until each member completes one era. The ensemble line says so explicitly rather than leaving it to look like the USDJPY unreachable-threshold case - the two are identical in the coverage number and completely different problems. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 08:55:04 -04:00
g_warriorOverlayReadyMask = 0;
g_warriorOverlayArmSince = 0;
fix(chart): sweep hammered the news filter; peak was a fossil; neutrals invisible Careful read of the 21:14 log window (user report: peak stuck at 50, label sticky, neutrals never shown). Three distinct defects, one commit because they share the two files. 1. 15,508 "CalendarValueHistory failed" lines in 68 SECONDS - ~230/second. The overlay sweep replayed Direction() on EVERY non-AI filter, including the news/session/risk-guard veto filters. The news filter calls CalendarValueHistory per evaluation and MT5's calendar cannot answer more than ~30 days back (the known calendar cliff), so every historical bar logged a failure - real wall-clock burned inside a sweep whose whole point is to stay cheap. Veto filters keep m_pattern_count at its 0 default (the same test UpdateSignalsWeights keys on): they cast no weighted vote, and a prohibition cannot be reconstructed faithfully anyway - it joins order validation in the cannot-replay family. Skipped. Compounding it: at era ~200 the four members complete a barrier round every ~20s while a full 5,000-bar sweep takes ~17s of slices - the sweep finished and instantly re-armed, forever, against arrow caches half-rebuilt mid-era. That is why the census's "had a voter" flapped 1299 -> 257 -> 1113 across three back-to-back sweeps. Re-arms now rate-limited to one per 5 minutes. 2. Peak 50 was a FOSSIL. m_votePeak never reset, so it still held a value attained under the 25/50/75/100 DEFAULT tier weights from the attach window before the first re-rank - unreachable ever since the weights became measured (pooled 27-32 in the same log). A ceiling nothing can reach reads as "the models are underperforming their own history", which is backwards: the history was priced in different money. The peak now resets at the same regime boundary as the census (StartFilteredOverlay), and the label shows max(live peak, census strongest-vote) - the census number is the actual answer to "can Min_Vote_Open ever be reached", measured over ~5,000 bars under the CURRENT weights. 3. Neutrals were invisible. The prospective count lumped Neutral-deciding models in with voters, so "4 model(s)" read identically whether all four voted or three sat flat. Now "2 vote/2 flat", and an all-neutral bar reads "VOTE flat ... 0 vote/4 flat" instead of "--" - the models answered, and the answer was Neutral. Expected values, from this log's own re-ranks (all four members' fires land in T3; tier weights 27-32; module weights 0.27-0.32): a unanimous-buy bar reads ~29-30%, mixed membership 28-34. The reported "stuck at buy 28, climbed to 30, flashes of sell, now 33.4" is those weights doing exactly what they should. The stickiness between moves is pass 2/2.5/3 - only pass 1 writes dPrevSignal, so the label holds the last pass-1 bar's decision for the remainder of each era. Display-only, and honest: it is the model's most recent output. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 21:18:55 -04:00
g_lastOverlayArmTick = GetTickCount();
feat(chart): reconstruct the filtered view behind the handover point Completes the filtered view from 282b535, which only reached forward of attach. On a multi-hour training run that is the entire time you are looking at the chart, so the answer to "how would the whole bot have traded" was blank exactly when it was wanted. The sweep lives on the AGGREGATE signal, which is the only object holding every filter. AI members contribute their CACHED per-bar decision from the era scan - no inference re-runs, the cache already spans the chart - and the classic ladders are replayed with EvalShift(i), the same mechanism CSignalMETA's candidate sweep uses and exact because every classic pattern condition anchors on StartIndex(). Combination is the live one: weighted mean over voting filters, abstentions out of both sums, against Min_Vote_Open. THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part that is not obvious. Live journaling reads m_active_pattern_long/short from the PREVIOUS Direction() call. Replaying hundreds of past bars between two live bars leaves those slots holding whichever bar the sweep stopped on, so the next live bar journals that pattern under the current timestamp - a corrupted row in the very table pattern win rates are computed from, which is now also where vote weights come from. Save/RestoreVoteState() brackets every replayed call. CSignalMETA gets away without it only because its sweep runs once, at the first era, before any of that state matters. TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker rejected an order - it has no stops level, ATR warm-up or swing-history sync as they were at that moment - so it is an upper bound: honest about the vote, optimistic about placement. It therefore stops dead at the handover bar, which is latched ONCE so later rebuilds cannot creep it forward and start overwriting real decisions with guesses, and its arrows say "reconstructed (vote only - order validation not replayed)" in the tooltip. Someone comparing two arrows either side of that line has to be able to tell which is a record and which is a replay, and the chart is the only place they look. Re-armed on any era boundary (summed era counters), because that is when the answer changes - RankTiersFromOos has just re-derived every tier's vote weight - and only between sweeps, so a restart cannot leave the previous pass's tail undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on every classic filter, which is real indicator work on the chart thread, and an unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen. SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should reach the same distance or the raw and filtered views are not comparable. Known gap: on a classic-only chart the reconstruction is built once and not refreshed when the hourly DB ranking moves the classic weights. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
Expert.ArmFilteredOverlay();
}
Expert.AdvanceFilteredOverlay(OVERLAY_BARS_PER_SLICE);
}
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
feat(altdata): EIA wired, 24-instrument symbol catalog, mapping dialog for unknown symbols EIA (user directive: "the NN might find patterns in it for both oil and regular symbols"). Weekly Petroleum Status Report via the v2 API - crude stocks ex-SPR, field production, refinery utilization - three features (1y percentile, 4w change, utilization) on EVERY catalog symbol, not just oil. EIA screened NULL on WTI's short 7y sample, so these ship as EXPLORATORY inputs: the deploy gate, not the screen, decides whether a model trained on them trades. Publication stamp observed+6d mirrors research/altdata/eia.py. Symbol handling was hardcoded to three if-blocks; it is now a catalog of 24 instruments x alias lists covering The5ers/FTMO/AvaTrade/Dukascopy/OANDA/IC Markets naming, with prefix matching for the broker suffix zoo (US500.cash, XAUUSDm, EURUSD.r). Adding an instrument is one AddSpec row. COT caches are named by CANONICAL so two brokers' names for one contract share a download. Unrecognised symbol -> a chart dialog (Panel\AltDataMapDialog.mqh, CAppDialog + dropdown) asks which instrument it is; the answer persists in symbol_map.cfg and "No alternative data" is a recorded choice, not a nag. Non-blocking by design: an unmapped symbol contributes 0 features and must never hold up a chart. Also: UrlEncodePart now escapes '%' - SoQL like-predicates use it as the wildcard and an unescaped one corrupts the query; docs/ gains the whitelist URLs, an API-key backup, and the catalog reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:18:29 -04:00
void PollAltDataMapDialog(void)
{
if(!g_altMapDialogOpen || !g_altMapDialog.Done())
return;
string choice = g_altMapDialog.Result();
g_altDataFetch.SaveUserMapping(_Symbol, choice);
g_altMapDialog.Destroy(REASON_REMOVE);
g_altMapDialogOpen = false;
if(choice == "NONE")
{
Print("AltDataFetch: recorded 'no alternative data' for " + _Symbol + " - it will not ask "
"again. Remove that line from symbol_map.cfg to be asked at the next attach.");
return;
}
//--- Download now rather than waiting up to 30 minutes for the next upkeep tick.
g_lastAltDataRun = TimeCurrent();
if(g_altDataFetch.Update(_Symbol))
for(int i = 0; i < g_aiSignalCount; i++)
g_aiSignals[i].AltDataReload();
Print("AltDataFetch: alt data for " + _Symbol + " is being maintained now. A model that was "
"already created without these features keeps its pinned input width - re-attach the EA "
"to build models that actually train on them.");
}
void OnTimer()
{
//--- STOP FIRST. Nothing below this line matters to a program that is being unloaded: the training
//--- poll, the alt-data upkeep (blocking WebRequests) and the DB ranking pass are all work whose
fix(shutdown): make ExitPolicy public, and stop every long loop the moment MT5 asks Two things, one of which was a compile error. 1. ExitPolicy() was declared in the protected block but is pushed in from Warrior_EA.mq5:770. Moved to public beside the other EA-facing setters. 2. Chart objects surviving OnDeinit. The 4,500 ms teardown budget is measured from the STOP REQUEST, not from OnDeinit's first line, and OnDeinit cannot begin until whatever is in flight returns - so a scan still running after _StopFlag is raised does not delay the cleanup, it SPENDS it, and the purge never gets its turn. New CExpertSignalAIBase::ShutdownRequested() = IsStopped() || m_shutdownInProgress. Deliberately NOT m_trainingStopRequested: that latches, and a latched flag would permanently disable scans that must run again on the next Start. Guarded, longest first: - TuneIndicatorsByFilter - per candidate, restoring the OPERATOR's settings on the way out (best[] is mutated in place; the tuner otherwise keeps the last trial's parameters, which nothing chose). - ReportBarrierGeometryScan - per pairing, breaking to ONE restore point so m_barrierScanLiveLabels can never be left true (that makes ComputeLabelForBar read the last candidate's multiples as the configured geometry). - ReportFeatureLabelInformation / ReportExcursionInformation / lag profile - nulls ABANDON rather than truncate: fewer draws is not a smaller null, it is a wrong one, and p shifts toward significance. m_dirEvidence staying false is the safe direction. - SimulateExitPolicyOutcomes - zeroes its accumulators so the divergence line is dropped instead of latching a partial expectancy as the run's only report. - ReportGeometryExpectancyScan - per ladder rung. - HttpGet - one choke point for up to a dozen blocking WebRequests per first-pass Update(). An in-flight request cannot be cancelled; refusing to start another is the whole remedy. - PollTraining, OnChartEventHandler's study event, TuneIndicatorsAndTrain - entry points, so a queued event cannot open an era during teardown. TuneIndicatorsAndTrain's guard is the first statement, ahead of the m_tuneFilterDone / g_ensembleChartTuneDone latches. - OnTick / OnTimer / OnChartEvent. Training's own bar loops already honoured this (pass 1 per bar, passes 2/2.5/3 yield on a 120 ms budget); the warm-up scans did not, and they are the longest uninterruptible stretches the EA has. StopTraining() is unchanged: the operator's Stop still finalises synchronously. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:03:11 -04:00
//--- results are about to be discarded.
if(IsStopped())
return;
//--- Also here, not only in OnTick(): this chart's own symbol can go minutes without a quote while
//--- an open position on ANOTHER symbol moves account equity through the limit. Equity is
//--- account-wide, so the budget must be re-checked on the clock, not only on this symbol's ticks.
g_riskBudget.Update();
//--- keeps training progressing on wall-clock time even with no ticks at all (market closed) -
//--- OnTickHandler's own scheduling only ever runs when a tick actually arrives
for(int i = 0; i < g_aiSignalCount; i++)
g_aiSignals[i].PollTraining();
feat(chart): reconstruct the filtered view behind the handover point Completes the filtered view from 282b535, which only reached forward of attach. On a multi-hour training run that is the entire time you are looking at the chart, so the answer to "how would the whole bot have traded" was blank exactly when it was wanted. The sweep lives on the AGGREGATE signal, which is the only object holding every filter. AI members contribute their CACHED per-bar decision from the era scan - no inference re-runs, the cache already spans the chart - and the classic ladders are replayed with EvalShift(i), the same mechanism CSignalMETA's candidate sweep uses and exact because every classic pattern condition anchors on StartIndex(). Combination is the live one: weighted mean over voting filters, abstentions out of both sums, against Min_Vote_Open. THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part that is not obvious. Live journaling reads m_active_pattern_long/short from the PREVIOUS Direction() call. Replaying hundreds of past bars between two live bars leaves those slots holding whichever bar the sweep stopped on, so the next live bar journals that pattern under the current timestamp - a corrupted row in the very table pattern win rates are computed from, which is now also where vote weights come from. Save/RestoreVoteState() brackets every replayed call. CSignalMETA gets away without it only because its sweep runs once, at the first era, before any of that state matters. TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker rejected an order - it has no stops level, ATR warm-up or swing-history sync as they were at that moment - so it is an upper bound: honest about the vote, optimistic about placement. It therefore stops dead at the handover bar, which is latched ONCE so later rebuilds cannot creep it forward and start overwriting real decisions with guesses, and its arrows say "reconstructed (vote only - order validation not replayed)" in the tooltip. Someone comparing two arrows either side of that line has to be able to tell which is a record and which is a replay, and the chart is the only place they look. Re-armed on any era boundary (summed era counters), because that is when the answer changes - RankTiersFromOos has just re-derived every tier's vote weight - and only between sweeps, so a restart cannot leave the previous pass's tail undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on every classic filter, which is real indicator work on the chart thread, and an unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen. SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should reach the same distance or the raw and filtered views are not comparable. Known gap: on a classic-only chart the reconstruction is built once and not refreshed when the hourly DB ranking moves the classic weights. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
//--- FILTERED VIEW: keep the reconstructed history in step with the models. One slice per timer
//--- tick, same cadence and same reasoning as the chunked signal rescan above it.
AdvanceFilteredSignalOverlay();
fix(chart): the vote readout was repainted once per bar, not once per timer tick "Still stuck at 0" after a042cb4 - and the .ex5 timestamp confirmed the new build was running, so this was not a stale binary. The readout was only ever written inside Direction(), and with Expert_EveryTick=false the stock CExpert::Refresh() gates Processing() - and therefore Direction() - to NEW-BAR ticks (verified in the terminal's own Include\Expert\Expert.mqh: Refresh() returns false unless the tick lands on a period boundary). On an H4 chart that is one repaint every four hours. The label was written exactly once at attach - before any model had produced a decision, so it read 0.0 with 4 models - and then sat frozen while the models trained underneath it. "Stuck at 0" was the label's refresh RATE, not the vote's value. The prospective fallback in a042cb4 was correct and running; it just had no way to reach the screen until the next bar open. The prospective computation is extracted into RefreshVoteReadout(), called from OnTimer through CExpertCustom every timer tick. It defers to the trade path whenever the last real Direction() had live voters (m_lastLiveVoters latch): a live vote is authoritative for its whole bar, and repainting prospective numbers over it would overwrite a tradable reading with an untradable one. Cheap by construction - a handful of filters, plain arithmetic on already-computed members, no indicator reads - so it belongs on the 500ms timer without a throttle. Expect the label to move at timer cadence now, tracking pass-1's walk through the training window (dPrevSignal holds the last trained bar's output during an era), dimmed and labelled "training, not tradable yet". NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:56:06 -04:00
//--- Keep the vote readout tracking the models at timer cadence - Direction() only runs on
//--- new-bar ticks (stock CExpert::Refresh gates it), which on H4 is once every four hours.
Expert.RefreshVoteReadout();
//--- Finish the Show Signals sequence once every instance queued by ToggleSignalsVisibility has
//--- drained its chunked rescan (each is advanced one slice per PollTraining call above).
FinalizeSignalsRescanIfDone();
//--- Training can finish and deploy ITSELF (the plateau ladder finalising the best checkpoint,
//--- or the era-cap deploy) with no button ever pressed, and RefreshControlPanelLabels()
//--- otherwise only runs in response to a click - which would leave the panel offering "Deploy
//--- Model" on an already-deployed model until the user happened to click something.
bool deployedNow = AllTrainingDeployed();
if(deployedNow != g_lastDeployedState)
{
g_lastDeployedState = deployedNow;
RefreshControlPanelLabels();
}
//--- Alt-data upkeep, before the UseDatabaseRanking early-return so it runs regardless of
//--- that input. First pass backfills any missing history (one blocking WebRequest per stale
//--- source, seconds); steady state is two date compares every 30 minutes.
if(!MQLInfoInteger(MQL_TESTER) && !MQLInfoInteger(MQL_OPTIMIZATION))
{
feat(altdata): EIA wired, 24-instrument symbol catalog, mapping dialog for unknown symbols EIA (user directive: "the NN might find patterns in it for both oil and regular symbols"). Weekly Petroleum Status Report via the v2 API - crude stocks ex-SPR, field production, refinery utilization - three features (1y percentile, 4w change, utilization) on EVERY catalog symbol, not just oil. EIA screened NULL on WTI's short 7y sample, so these ship as EXPLORATORY inputs: the deploy gate, not the screen, decides whether a model trained on them trades. Publication stamp observed+6d mirrors research/altdata/eia.py. Symbol handling was hardcoded to three if-blocks; it is now a catalog of 24 instruments x alias lists covering The5ers/FTMO/AvaTrade/Dukascopy/OANDA/IC Markets naming, with prefix matching for the broker suffix zoo (US500.cash, XAUUSDm, EURUSD.r). Adding an instrument is one AddSpec row. COT caches are named by CANONICAL so two brokers' names for one contract share a download. Unrecognised symbol -> a chart dialog (Panel\AltDataMapDialog.mqh, CAppDialog + dropdown) asks which instrument it is; the answer persists in symbol_map.cfg and "No alternative data" is a recorded choice, not a nag. Non-blocking by design: an unmapped symbol contributes 0 features and must never hold up a chart. Also: UrlEncodePart now escapes '%' - SoQL like-predicates use it as the wildcard and an unescaped one corrupts the query; docs/ gains the whitelist URLs, an API-key backup, and the catalog reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:18:29 -04:00
MaybeAskAltDataMapping();
datetime altNow = TimeCurrent();
if(g_lastAltDataRun == 0 || altNow - g_lastAltDataRun >= ALTDATA_CHECK_SECONDS)
{
g_lastAltDataRun = altNow;
if(g_altDataFetch.Update(_Symbol))
for(int i = 0; i < g_aiSignalCount; i++)
g_aiSignals[i].AltDataReload();
}
}
if(!UseDatabaseRanking)
return;
datetime now = TimeCurrent();
//--- One-shot bypass of the hourly throttle below (see g_forcePatternWeightsRefresh's declaration
//--- comment): a model that just finished its backfill has real DB history sitting unranked, and
//--- waiting up to an hour for it to reach UpdateSignalsWeights() is exactly the "not ready to trade
//--- the instant training finishes" gap this whole feature exists to close.
bool forceNow = g_forcePatternWeightsRefresh;
if(forceNow)
g_forcePatternWeightsRefresh = false;
if(!forceNow && g_lastDbRankingRun != 0 && now - g_lastDbRankingRun < DB_RANKING_INTERVAL_SECONDS)
return;
g_lastDbRankingRun = now;
Expert.OnTimer();
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CheckAlgoTradingState(void)
{
bool allowed = (bool)TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && (bool)MQLInfoInteger(MQL_TRADE_ALLOWED);
if(allowed != g_lastAlgoTradingAllowed)
{
if(allowed)
Print(__FUNCTION__ + ": AlgoTrading re-enabled - order placement resumed (training/signals were unaffected while disabled)");
else
Print(__FUNCTION__ + ": AlgoTrading disabled (terminal toggle off, or EA's own permission revoked) - no new orders will be sent until re-enabled; training/signal generation continues unaffected");
g_lastAlgoTradingAllowed = allowed;
}
}
void AutosaveWeightsIfDue(void)
{
//--- NEVER autosave inside the Strategy Tester / optimizer. The whole reason this exists is that
//--- a live terminal can be killed without OnDeinit running - a tester run has no such exposure.
//--- A tester run is inference-only anyway (see m_inferenceOnly) - the weights never change, so
//--- there is literally nothing to persist.
if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_FORWARD))
return;
datetime lastBarDate = (datetime)SeriesInfoInteger(_Symbol, _Period, SERIES_LASTBAR_DATE);
//--- <=0 is a transient history-sync hiccup, not "no new bar" - skip this tick and try again on the
//--- next one rather than risk locking onto a bad watermark (same guard philosophy as
//--- ScheduleTrainingIfNeeded's own lastBarDate read).
if(lastBarDate <= 0 || lastBarDate == g_lastAutosaveBarTime)
return;
g_lastAutosaveBarTime = lastBarDate;
for(int i = 0; i < g_aiSignalCount; i++)
g_aiSignals[i].SaveWeightsNow();
}
void OnTick()
{
research: export the feature matrix and a raw OHLCV grid for offline work The bottleneck on this project has never been the modelling - it is that every hypothesis costs a compile, a deploy, an attach and a log read, and answers exactly one question. Days have gone into questions that are seconds of arithmetic once the data is in hand. Adds a RESEARCH-ONLY build, gated behind WARRIOR_EXPORT_FEATURES and never compiled into a shipped binary, which writes two things to Common\Files\Warrior_EA\Research\ and then does nothing at all: <symbol>_<tf>_features.csv - one row per bar: index, time, OHLC, ATR, and the m_neuronsCount feature values. Exactly what the network sees. The raw bars ride along on purpose: with OHLC and ATR offline, every barrier geometry, horizon and in-trade target is recomputable without MetaTrader in the loop. <symbol>_<tf>_rates.csv - raw OHLCV across a grid of 8 symbols x 5 timeframes. The 26 engineered features only exist for the attached chart (indicator handles bind to PERIOD_CURRENT); raw rates do not, so ONE attach yields the whole research grid. The bar time also makes session/hour/day-of-week derivable - the only inputs in play that are not a transform of the same OHLCV series. Safety, because this binary gets attached to a chart on a LIVE ACCOUNT to reach real history: - OnTick returns immediately, so Expert.OnTick() - the entire trading path - is unreachable regardless of the AlgoTrading toggle, the signal state or the inputs. Structurally incapable of sending an order, not merely unlikely to. - No config lock. It never trains and never saves a model, so it has nothing to protect against a concurrent chart - and taking the lock would make it refuse to start exactly when the config it wants to read is already open, which is when it is most useful. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 15:49:57 -04:00
#ifdef WARRIOR_EXPORT_FEATURES
//--- RESEARCH BUILD: writes the feature matrix at init and does NOTHING else, ever. Returning here
//--- means Expert.OnTick() (the entire trading path) is never reached, independently of the
//--- AlgoTrading toggle, the signal state or the inputs.
research: export the feature matrix and a raw OHLCV grid for offline work The bottleneck on this project has never been the modelling - it is that every hypothesis costs a compile, a deploy, an attach and a log read, and answers exactly one question. Days have gone into questions that are seconds of arithmetic once the data is in hand. Adds a RESEARCH-ONLY build, gated behind WARRIOR_EXPORT_FEATURES and never compiled into a shipped binary, which writes two things to Common\Files\Warrior_EA\Research\ and then does nothing at all: <symbol>_<tf>_features.csv - one row per bar: index, time, OHLC, ATR, and the m_neuronsCount feature values. Exactly what the network sees. The raw bars ride along on purpose: with OHLC and ATR offline, every barrier geometry, horizon and in-trade target is recomputable without MetaTrader in the loop. <symbol>_<tf>_rates.csv - raw OHLCV across a grid of 8 symbols x 5 timeframes. The 26 engineered features only exist for the attached chart (indicator handles bind to PERIOD_CURRENT); raw rates do not, so ONE attach yields the whole research grid. The bar time also makes session/hour/day-of-week derivable - the only inputs in play that are not a transform of the same OHLCV series. Safety, because this binary gets attached to a chart on a LIVE ACCOUNT to reach real history: - OnTick returns immediately, so Expert.OnTick() - the entire trading path - is unreachable regardless of the AlgoTrading toggle, the signal state or the inputs. Structurally incapable of sending an order, not merely unlikely to. - No config lock. It never trains and never saves a model, so it has nothing to protect against a concurrent chart - and taking the lock would make it refuse to start exactly when the config it wants to read is already open, which is when it is most useful. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 15:49:57 -04:00
return;
#endif
//--- STOP FIRST - same reasoning as OnTimer's guard. Deliberately AHEAD of the risk-budget update
//--- too: a program that is unloading places no orders, so there is nothing left for the budget to
//--- protect.
fix(shutdown): make ExitPolicy public, and stop every long loop the moment MT5 asks Two things, one of which was a compile error. 1. ExitPolicy() was declared in the protected block but is pushed in from Warrior_EA.mq5:770. Moved to public beside the other EA-facing setters. 2. Chart objects surviving OnDeinit. The 4,500 ms teardown budget is measured from the STOP REQUEST, not from OnDeinit's first line, and OnDeinit cannot begin until whatever is in flight returns - so a scan still running after _StopFlag is raised does not delay the cleanup, it SPENDS it, and the purge never gets its turn. New CExpertSignalAIBase::ShutdownRequested() = IsStopped() || m_shutdownInProgress. Deliberately NOT m_trainingStopRequested: that latches, and a latched flag would permanently disable scans that must run again on the next Start. Guarded, longest first: - TuneIndicatorsByFilter - per candidate, restoring the OPERATOR's settings on the way out (best[] is mutated in place; the tuner otherwise keeps the last trial's parameters, which nothing chose). - ReportBarrierGeometryScan - per pairing, breaking to ONE restore point so m_barrierScanLiveLabels can never be left true (that makes ComputeLabelForBar read the last candidate's multiples as the configured geometry). - ReportFeatureLabelInformation / ReportExcursionInformation / lag profile - nulls ABANDON rather than truncate: fewer draws is not a smaller null, it is a wrong one, and p shifts toward significance. m_dirEvidence staying false is the safe direction. - SimulateExitPolicyOutcomes - zeroes its accumulators so the divergence line is dropped instead of latching a partial expectancy as the run's only report. - ReportGeometryExpectancyScan - per ladder rung. - HttpGet - one choke point for up to a dozen blocking WebRequests per first-pass Update(). An in-flight request cannot be cancelled; refusing to start another is the whole remedy. - PollTraining, OnChartEventHandler's study event, TuneIndicatorsAndTrain - entry points, so a queued event cannot open an era during teardown. TuneIndicatorsAndTrain's guard is the first statement, ahead of the m_tuneFilterDone / g_ensembleChartTuneDone latches. - OnTick / OnTimer / OnChartEvent. Training's own bar loops already honoured this (pass 1 per bar, passes 2/2.5/3 yield on a 120 ms budget); the warm-up scans did not, and they are the longest uninterruptible stretches the EA has. StopTraining() is unchanged: the operator's Stop still finalises synchronously. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:03:11 -04:00
if(IsStopped())
return;
CheckAlgoTradingState();
//--- FIRST, and before Expert.OnTick() can open anything.
g_riskBudget.Update();
AutosaveWeightsIfDue();
Expert.OnTick();
fix: four risk-layer holes a funded account would eventually find 1. The expectancy stop was stone dead at shipped defaults. Its only feed - RecordTradeResult inside CTradeJournalManager::Update() - ran solely under UseDatabaseRanking, which ships false, so the da54639 halt was armed (ExpectancyMinTrades=40) and never received a single closed trade. A risk rule must not be a side effect of an analytics toggle: the journal gains InitTrackingOnly(), Update() runs unconditionally from OnTick and skips only the DB insert when no DB was initialized. 2. Below-minimum lots were silently bumped UP to SYMBOL_VOLUME_MIN by TCNormalizeVolume - correct for a user-entered fixed lot, but in the risk-sizing path it turned a budget-capped 0.05 into 0.10 on min-0.10/ step-0.01 symbols: double the intended risk, after CapRiskAmount already clamped, exactly the routine-stop-out-breaches-the-daily-limit scenario the budget exists to close. CMoneyRiskBase now refuses the trade when the risk-derived lot is below the broker minimum. 3. All trading was async fire-and-forget (SetAsyncMode(true)) with no OnTradeTransaction handler and no retry: server retcodes were never observed. Fail-safe for entries, not for closes - a silently rejected close rode the position until the next bar (or next day for the timed close window). Now synchronous, matching the risk-budget flatten's own already-synchronous CTrade; on an H1 EA the latency is irrelevant. 4. FIXED_LOT bypassed the budget entirely (no CapRiskAmount, no OpenRiskAtStops) - pre-halt it could commit more than the remaining daily allowance. A fixed lot cannot be scaled, so the rule is binary: its loss-to-stop fits the remaining allowance whole or the trade is refused; unpriceable risk (no SL) is refused while the budget is enabled. Compile: 0 errors, 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:14:26 -04:00
//--- Unconditional since 2026-08-11: Update() feeds the expectancy stop from every closed trade
//--- and only touches the journal DB when one was initialized (UseDatabaseRanking).
journal.Update();
//--- new arrows are always created visible; if signals are currently hidden, re-hide
//--- any that were drawn this tick (cheap - only runs while the toggle is in the "hidden" state)
if(!g_signalsVisible)
ApplySignalsVisibility();
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
const long &lparam,
const double &dparam,
const string &sparam)
{
//--- STOP FIRST, and this handler matters more than the other two: training is driven by a CUSTOM
//--- CHART EVENT (see CExpertSignalAIBase::OnChartEventHandler -> TuneIndicatorsAndTrain), so an
//--- event already queued when the stop request lands would start a full era-0 warm-up - the MI
//--- suite, the geometry scan, a relabel - inside the teardown window.
fix(shutdown): make ExitPolicy public, and stop every long loop the moment MT5 asks Two things, one of which was a compile error. 1. ExitPolicy() was declared in the protected block but is pushed in from Warrior_EA.mq5:770. Moved to public beside the other EA-facing setters. 2. Chart objects surviving OnDeinit. The 4,500 ms teardown budget is measured from the STOP REQUEST, not from OnDeinit's first line, and OnDeinit cannot begin until whatever is in flight returns - so a scan still running after _StopFlag is raised does not delay the cleanup, it SPENDS it, and the purge never gets its turn. New CExpertSignalAIBase::ShutdownRequested() = IsStopped() || m_shutdownInProgress. Deliberately NOT m_trainingStopRequested: that latches, and a latched flag would permanently disable scans that must run again on the next Start. Guarded, longest first: - TuneIndicatorsByFilter - per candidate, restoring the OPERATOR's settings on the way out (best[] is mutated in place; the tuner otherwise keeps the last trial's parameters, which nothing chose). - ReportBarrierGeometryScan - per pairing, breaking to ONE restore point so m_barrierScanLiveLabels can never be left true (that makes ComputeLabelForBar read the last candidate's multiples as the configured geometry). - ReportFeatureLabelInformation / ReportExcursionInformation / lag profile - nulls ABANDON rather than truncate: fewer draws is not a smaller null, it is a wrong one, and p shifts toward significance. m_dirEvidence staying false is the safe direction. - SimulateExitPolicyOutcomes - zeroes its accumulators so the divergence line is dropped instead of latching a partial expectancy as the run's only report. - ReportGeometryExpectancyScan - per ladder rung. - HttpGet - one choke point for up to a dozen blocking WebRequests per first-pass Update(). An in-flight request cannot be cancelled; refusing to start another is the whole remedy. - PollTraining, OnChartEventHandler's study event, TuneIndicatorsAndTrain - entry points, so a queued event cannot open an era during teardown. TuneIndicatorsAndTrain's guard is the first statement, ahead of the m_tuneFilterDone / g_ensembleChartTuneDone latches. - OnTick / OnTimer / OnChartEvent. Training's own bar loops already honoured this (pass 1 per bar, passes 2/2.5/3 yield on a 120 ms budget); the warm-up scans did not, and they are the longest uninterruptible stretches the EA has. StopTraining() is unchanged: the operator's Stop still finalises synchronously. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:03:11 -04:00
if(IsStopped())
return;
//--- canonical CAppDialog usage (Controls\Dialog.mqh): forward every event to the dialog first, since
//--- that's what drives its own click/drag hit-testing (via CHARTEVENT_MOUSE_MOVE) as well as our
//--- buttons' EVENT_MAP handlers (see ControlPanel.mqh) - then pick up whatever button action, if any,
//--- that just recorded.
ExtPanel.ChartEvent(id, lparam, dparam, sparam);
HandleControlPanelAction(ExtPanel.ConsumeAction());
feat(altdata): EIA wired, 24-instrument symbol catalog, mapping dialog for unknown symbols EIA (user directive: "the NN might find patterns in it for both oil and regular symbols"). Weekly Petroleum Status Report via the v2 API - crude stocks ex-SPR, field production, refinery utilization - three features (1y percentile, 4w change, utilization) on EVERY catalog symbol, not just oil. EIA screened NULL on WTI's short 7y sample, so these ship as EXPLORATORY inputs: the deploy gate, not the screen, decides whether a model trained on them trades. Publication stamp observed+6d mirrors research/altdata/eia.py. Symbol handling was hardcoded to three if-blocks; it is now a catalog of 24 instruments x alias lists covering The5ers/FTMO/AvaTrade/Dukascopy/OANDA/IC Markets naming, with prefix matching for the broker suffix zoo (US500.cash, XAUUSDm, EURUSD.r). Adding an instrument is one AddSpec row. COT caches are named by CANONICAL so two brokers' names for one contract share a download. Unrecognised symbol -> a chart dialog (Panel\AltDataMapDialog.mqh, CAppDialog + dropdown) asks which instrument it is; the answer persists in symbol_map.cfg and "No alternative data" is a recorded choice, not a nag. Non-blocking by design: an unmapped symbol contributes 0 features and must never hold up a chart. Also: UrlEncodePart now escapes '%' - SoQL like-predicates use it as the wildcard and an unescaped one corrupts the query; docs/ gains the whitelist URLs, an API-key backup, and the catalog reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:18:29 -04:00
//--- The alt-data mapping dialog, when open, drives its own hit-testing the same way.
if(g_altMapDialogOpen)
{
g_altMapDialog.ChartEvent(id, lparam, dparam, sparam);
PollAltDataMapDialog();
}
Expert.OnChartEvent(id, lparam, dparam, sparam);
//--- CHARTEVENT_CHART_CHANGE covers resize, scroll and DPI/zoom changes - anything that can
//--- move the visible area out from under a dialog left near an edge from a previous drag.
if(id == CHARTEVENT_CHART_CHANGE)
ClampControlPanelToChart();
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool AddFilterToSignal(CExpertSignalCustom * signal, CExpertSignalCustom * filter)
{
if(filter == NULL)
{
Print(__FUNCTION__ + "Error creating filters");
return false;
}
return signal.AddFilter(filter);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool InitializeSignal(CExpertSignalCustom * signal_obj)
{
if(signal_obj == NULL)
{
Print(__FUNCTION__ + ": error creating signal");
return false;
}
Expert.InitSignal(signal_obj);
signal_obj.Entry_Multiplier(Entry_Multiplier);
signal_obj.Expiration(Signal_Expiration);
//--- ATR unit lookback, pinned - decoupled from the derived input window, see Inputs.mqh.
signal_obj.Periods(ATR_FEATURE_PERIOD);
signal_obj.SLMode((int)SL_Mode);
signal_obj.TPMode((int)TP_Mode);
signal_obj.ConfidenceSource((int)Confidence_Source);
//--- Gates AddFilter()'s DB pattern-table creation and Direction()'s per-tick DB signal buffering
//--- (ExpertSignalCustom.mqh:286/555) - without this, UseDatabaseRanking only skipped the Weight(1)
//--- default below and never actually populated the win-rate tables UpdateSignalsWeights() reads from.
signal_obj.UseDatabase(UseDatabaseRanking);
//--- Pattern-table row cap; raised via the input for meta-label corpus builds (design doc S1).
signal_obj.MaxTableRows(DB_MaxRowsPerTable);
//--- Same two inputs feed BOTH engines, and now on ONE scale each. Close drives the rule-based
//--- exit, and
//--- ONLY that: an AI-certified position holds to its barrier. See Signal_ThresholdOpen's
//--- declaration comment (Variables\Inputs.mqh) for the shared 0-100 conviction scale, and for
//--- why open and close must stay independent of each other.
feat(ui): thresholds pick from a dropdown, and the finder arrows are back beside the level lines Two UX changes the operator asked for. THRESHOLDS. Signal_ThresholdOpen/Close were raw ints with the legal range written in the label ("[0...100, 101 = never]") - the one input style this codebase converted away from everywhere else. Open now takes the existing PERCENTAGE_PRESETS, whose comment already declared itself to be "Signal_ ThresholdOpen's scale" but was never wired to it; Close takes a new SIGNAL_CLOSE_PRESETS carrying the same rungs plus CLOSE_DISABLED = 101, which is why it cannot just reuse the other enum. Member names are prefixed because MQL5 enum members share ONE flat namespace - a bare PCT_25 in the second enum would silently resolve to the first one's, warning only. Values are unchanged, so existing .set files keep their settings. Both call sites now cast explicitly at the CExpertSignal boundary rather than leaning on an implicit enum-to-int conversion that only warns. ARROWS. 2026-08-19 replaced the low/high arrows WITH trigger-price lines; that was a swap where it should have been an addition, and it cost the zoomed-out view. A mark is now both objects: the line is the precise entry/exit level, the arrow off the candle's extreme is the finder that says there is something here to zoom into. The arrow's name is the line's plus a suffix, so it stays inside SIG_ARROW_PREFIX and every prefix-scoped purge already reaches it. The two type-filtered sweeps had to widen or they would clear one half and leave the other: the Hide/Show visibility loop and the pre-rescan scoped delete both walked OBJ_TREND only. Both are typed-blind and prefix-scoped now - the same widening this file's 2026-08-09 note describes, for the same reason it gives. Deletes go through one WarriorDeleteSignalMark() so an arrow cannot outlive the line it belongs to, and the sidecar deliberately still records one row per mark off the line (the half carrying the price), with the restore redrawing the pair. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 13:50:33 -04:00
signal_obj.ThresholdOpen((int)Signal_ThresholdOpen);
signal_obj.ThresholdClose((int)Signal_ThresholdClose);
//--- The hold-to-barrier exit policy went with the fractal target (withdrawn - see Inputs.mqh).
//--- Barrier-target models keep the vote exits and always did: their label IS the vote's own
//--- horizon.
//--- HYBRID is now one fused signal, so no separate AI quorum is needed here.
return true;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
// Initialize Trailing
bool InitializeTrailing()
{
if(TrailingStrategy == TRAILING_STRATEGY_NONE)
{
// No trailing strategy selected
return true;
}
else
if(TrailingStrategy == TRAILING_STRATEGY_ATR_x1 ||
TrailingStrategy == TRAILING_STRATEGY_ATR_x2 ||
TrailingStrategy == TRAILING_STRATEGY_ATR_x3)
{
// ATR Trailing Strategy
double multiplier = 0;
if(TrailingStrategy == TRAILING_STRATEGY_ATR_x1)
multiplier = 1;
else
if(TrailingStrategy == TRAILING_STRATEGY_ATR_x2)
multiplier = 2;
else
if(TrailingStrategy == TRAILING_STRATEGY_ATR_x3)
multiplier = 3;
CTrailingATR *trailing = new CTrailingATR;
if(trailing == NULL)
{
Print(__FUNCTION__ + ": error creating trailing");
return false;
}
// Set ATR Multiplier
trailing.Multiplier(multiplier);
if(!Expert.InitTrailing(trailing))
{
Print(__FUNCTION__ + ": error initializing trailing");
return false;
}
}
else
if(TrailingStrategy == TRAILING_STRATEGY_INTELLIGENT)
{
// Confidence-adaptive ATR trailing (see Trailing\TrailingIntelligent.mqh) - the ATR
// multiple is set per-check from live AI confidence, so no fixed Multiplier() here.
CTrailingIntelligent *trailing = new CTrailingIntelligent;
if(trailing == NULL)
{
Print(__FUNCTION__ + ": error creating intelligent trailing");
return false;
}
if(!Expert.InitTrailing(trailing))
{
Print(__FUNCTION__ + ": error initializing intelligent trailing");
return false;
}
}
// Add more trailing strategies if needed
return true;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool InitializeMoneyManagement()
{
string functionName = __FUNCTION__;
if(MM_STRATEGY == FIXED_RISK)
{
CMoneyFixedRisk *money = new CMoneyFixedRisk;
if(money == NULL)
{
Print(functionName + ": error creating money");
return false;
}
if(!Expert.InitMoney(money))
{
Print(functionName + ": error initializing money");
return false;
}
money.Percent(Money_Risk_Percent);
}
else
if(MM_STRATEGY == FIXED_LOT)
{
CMoneyFixedLot *money = new CMoneyFixedLot;
if(money == NULL)
{
Print(functionName + ": error creating money");
return false;
}
if(!Expert.InitMoney(money))
{
Print(functionName + ": error initializing money");
return false;
}
money.Lots(Money_FixLot_Lots);
}
else
if(MM_STRATEGY == INTELLIGENT)
{
CMoneyIntelligent *money = new CMoneyIntelligent;
if(money == NULL)
{
Print(functionName + ": error creating money");
return false;
}
if(!Expert.InitMoney(money))
{
Print(functionName + ": error initializing money");
return false;
}
money.Percent(Money_Risk_Percent);
//--- Intelligent MM is AI-driven by definition now (the old Use_AI_Lot_Sizing toggle was
//--- removed as redundant) - always scale risk% by confidence via the Kelly path.
money.UseAIConfidenceLotSizing(true);
money.ConfidenceSource((int)Confidence_Source);
}
// Add more money management strategies if needed
return true;
}
//+------------------------------------------------------------------+